Compare commits

..

50 Commits

Author SHA1 Message Date
0a71bef851 feat:更新账单类别UI 2026-08-03 21:35:20 +08:00
8ecc38befd feat:更新账单类别选项框 2026-08-03 19:12:21 +08:00
e205d1ddf4 feat:增加Drawer组件 2026-07-30 17:00:01 +08:00
be05e4695c feat:增加汽车图标 2026-07-21 21:30:13 +08:00
4b5f853603 feat:增加汽车事件模块 2026-07-20 23:19:14 +08:00
6ff52e9d50 feat:增加账单运动类别 2026-07-05 21:07:22 +08:00
3535d33c4b feat:增加美食分析助手 2026-07-04 20:25:51 +08:00
0ec0bba2fa feat:更新支出单词 2026-07-02 21:44:03 +08:00
98f2842869 feat:增加日程计划模块 2026-06-04 18:30:13 +08:00
f7e98f2a28 feat:更新账单记录组件 2026-06-03 09:57:47 +08:00
b9f4936dcc feat:增加数字键盘组件 2026-06-03 07:50:55 +08:00
3201a1c32b feat:增加物品过期时间显示 2026-05-21 22:05:15 +08:00
6aed1854ca feat:更新图标样式 2026-04-16 21:55:38 +08:00
f8a605b8c0 fix:修复视频无法播放的问题 2026-04-07 22:49:07 +08:00
08999c608d fix:修复布局问题 2026-04-06 22:21:11 +08:00
7eeac7c465 feat:更新查看原图图片格式 2026-04-06 17:40:11 +08:00
420f250bac feat:更新证书查询条件 2026-04-02 23:10:17 +08:00
3e59b58f84 feat:增加刷新功能 2026-04-02 23:00:51 +08:00
ac914f6c05 feat:增加奖状证书模块 2026-04-01 23:24:06 +08:00
11887bf3d5 feat:更新运动健康模块 2026-03-15 23:14:35 +08:00
1fb76fc220 feat:更新上传文件组件 2026-03-15 21:52:49 +08:00
d7d167f3e2 feat:去除电脑端登录限制 2026-03-15 10:29:58 +08:00
5b7dcd614a feat:更改代理地址 2026-02-14 16:56:54 +08:00
4dc541da85 feat:增加图片存储模式 2026-02-14 16:55:52 +08:00
223a59cd8a feat:更新图片模块 2026-02-12 23:24:27 +08:00
2fbae7f80f feat:增加vant预览图片组件 2026-02-12 19:23:57 +08:00
8940b3972a feat:增加vant组件库 2026-02-12 17:27:14 +08:00
1b8bd5a7e2 feat:更新美食和旅游模块 2026-02-09 23:16:19 +08:00
45342fb55c feat:更新上传图片和视频组件 2026-02-04 22:24:39 +08:00
e5e1e5e2b3 feat:更新上传图片组件 2026-02-03 23:16:19 +08:00
94270b923c feat:更新美食模块图片功能 2026-02-02 22:54:12 +08:00
aa7abbdde3 Merge branch 'master' of https://cxxgit.iepose.cn/Cxx0822/sweet-hut-ui 2026-02-01 22:35:02 +08:00
7b96e60b8d feat:更新图片存储路径 2026-02-01 22:34:38 +08:00
71639cad51 Merge branch 'master' of https://cxxgit.iepose.cn/Cxx0822/sweet-hut-ui 2025-12-04 11:01:45 +08:00
d87732dbf7 feat:更新依赖 2025-12-04 11:01:37 +08:00
c0cfbbaa15 feat:禁用日期选择输入模式 2025-11-17 22:23:59 +08:00
f5e75b745f feat:增加健康模块睡眠时间监控 2025-11-17 19:58:59 +08:00
4d7946759c feat:增加体重图表自适应坐标轴 2025-11-17 09:39:40 +08:00
2bcc61fdaa feat:调整体重趋势图刻度 2025-11-13 10:16:26 +08:00
63a1e126ad feat:增加健康统计图表功能 2025-11-12 19:41:11 +08:00
f62613205c fix:修复日记显示不全的问题 2025-11-11 23:33:19 +08:00
5ec0781a53 feat:更新账单搜索模块 2025-11-11 09:35:49 +08:00
e875a8e2d5 Merge branch 'master' of https://cxxgit.iepose.cn/Cxx0822/sweet-hut-ui 2025-11-10 19:51:06 +08:00
7032070d3f feat:增加账单搜索功能 2025-11-10 19:50:22 +08:00
ee20c19b72 feat:更新运动健康模块 2025-11-04 22:35:53 +08:00
5f3a7ca5b4 feat:增加运动健康模块 2025-11-04 20:05:31 +08:00
ed0c8e6664 fix:修复博客接口错误的问题 2025-11-04 09:22:49 +08:00
355a6c256d feat:增加账本统计模块 2025-10-30 23:02:55 +08:00
9e0b8ead79 feat:更新tabbar显示 2025-10-30 16:50:13 +08:00
cc33d3028b feat:增加月经天数统计 2025-10-30 15:00:50 +08:00
200 changed files with 5277 additions and 6088 deletions

1
.env
View File

@@ -1 +0,0 @@
VITE_API_HOST=43.225.157.27

1
.env.development Normal file
View File

@@ -0,0 +1 @@
VITE_API_ALIST_PATH=/local/test

1
.env.production Normal file
View File

@@ -0,0 +1 @@
VITE_API_ALIST_PATH=/local

114
components.d.ts vendored Normal file
View File

@@ -0,0 +1,114 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AnniversaryCard: typeof import('./src/components/Anniversary/AnniversaryCard.vue')['default']
AnniversaryQuery: typeof import('./src/components/Anniversary/AnniversaryQuery.vue')['default']
AppLoading: typeof import('./src/components/Common/AppLoading.vue')['default']
ArriveButton: typeof import('./src/components/Common/ArriveButton.vue')['default']
AssetCard: typeof import('./src/components/Asset/AssetCard.vue')['default']
AssetQuery: typeof import('./src/components/Asset/AssetQuery.vue')['default']
AwardCard: typeof import('./src/components/Award/AwardCard.vue')['default']
AwardQuery: typeof import('./src/components/Award/AwardQuery.vue')['default']
BillCalendar: typeof import('./src/components/Bill/BillCalendar.vue')['default']
BillChart: typeof import('./src/components/Bill/BillChart.vue')['default']
BillDailyItem: typeof import('./src/components/Bill/BillDailyItem.vue')['default']
BillDailyList: typeof import('./src/components/Bill/BillDailyList.vue')['default']
BillDrawer: typeof import('./src/components/Bill/BillDrawer.vue')['default']
BillQuery: typeof import('./src/components/Bill/BillQuery.vue')['default']
BillRank: typeof import('./src/components/Bill/BillRank.vue')['default']
BillSearch: typeof import('./src/components/Bill/BillSearch.vue')['default']
BillStats: typeof import('./src/components/Bill/BillStats.vue')['default']
BlogArchive: typeof import('./src/components/Blog/Archive/BlogArchive.vue')['default']
BlogCategory: typeof import('./src/components/Blog/Category/BlogCategory.vue')['default']
BlogCategoryDetail: typeof import('./src/components/Blog/Category/BlogCategoryDetail.vue')['default']
BlogComment: typeof import('./src/components/Blog/Comment/BlogComment.vue')['default']
BlogDetail: typeof import('./src/components/Blog/Content/BlogDetail.vue')['default']
BlogLabel: typeof import('./src/components/Blog/Content/BlogLabel.vue')['default']
BlogOverview: typeof import('./src/components/Blog/Content/BlogOverview.vue')['default']
CarCard: typeof import('./src/components/Car/CarCard.vue')['default']
CarQuery: typeof import('./src/components/Car/CarQuery.vue')['default']
CategoryCard: typeof import('./src/components/Anniversary/CategoryCard.vue')['default']
CommonLogin: typeof import('./src/components/Login/CommonLogin.vue')['default']
DailyCard: typeof import('./src/components/DailyCard.vue')['default']
DailySummary: typeof import('./src/components/DailySummary.vue')['default']
EmailLogin: typeof import('./src/components/Login/EmailLogin.vue')['default']
FoodBasicData: typeof import('./src/components/Food/Data/FoodBasicData.vue')['default']
FoodCalendar: typeof import('./src/components/Food/FoodCalendar.vue')['default']
FoodCard: typeof import('./src/components/Food/FoodCard.vue')['default']
FoodDailyItem: typeof import('./src/components/Food/FoodDailyItem.vue')['default']
FoodMaterialData: typeof import('./src/components/Food/Data/FoodMaterialData.vue')['default']
FoodMaterialDialog: typeof import('./src/components/Food/Form/FoodMaterialDialog.vue')['default']
FoodMaterialForm: typeof import('./src/components/Food/Form/FoodMaterialForm.vue')['default']
FoodOthersData: typeof import('./src/components/Food/Data/FoodOthersData.vue')['default']
FoodQuery: typeof import('./src/components/Food/FoodQuery.vue')['default']
FoodRecordData: typeof import('./src/components/Food/Data/FoodRecordData.vue')['default']
FoodStats: typeof import('./src/components/Food/FoodStats.vue')['default']
FoodStepData: typeof import('./src/components/Food/Data/FoodStepData.vue')['default']
FoodStepDialog: typeof import('./src/components/Food/Form/FoodStepDialog.vue')['default']
FoodStepForm: typeof import('./src/components/Food/Form/FoodStepForm.vue')['default']
HealthChart: typeof import('./src/components/Health/HealthChart.vue')['default']
HealthForm: typeof import('./src/components/Health/HealthForm.vue')['default']
HealthStats: typeof import('./src/components/Health/HealthStats.vue')['default']
HomeQuery: typeof import('./src/components/HomeQuery.vue')['default']
JournalCalendar: typeof import('./src/components/Journal/JournalCalendar.vue')['default']
JournalCard: typeof import('./src/components/Journal/JournalCard.vue')['default']
JournalDrawer: typeof import('./src/components/Journal/JournalDrawer.vue')['default']
KpiCalendar: typeof import('./src/components/kpi/KpiCalendar.vue')['default']
KpiCard: typeof import('./src/components/kpi/KpiCard.vue')['default']
LedgerCard: typeof import('./src/components/Ledger/LedgerCard.vue')['default']
LedgerQuery: typeof import('./src/components/Ledger/LedgerQuery.vue')['default']
LedgerRank: typeof import('./src/components/Ledger/LedgerRank.vue')['default']
LoginAuth: typeof import('./src/components/Login/LoginAuth.vue')['default']
Milestone: typeof import('./src/components/Milestone.vue')['default']
MobileSetting: typeof import('./src/components/Common/MobileSetting.vue')['default']
ModuleCard: typeof import('./src/components/ModuleCard.vue')['default']
NumberInput: typeof import('./src/components/NumberInput.vue')['default']
PasswordCard: typeof import('./src/components/Password/PasswordCard.vue')['default']
PasswordQuery: typeof import('./src/components/Password/PasswordQuery.vue')['default']
PeriodForm: typeof import('./src/components/Period/PeriodForm.vue')['default']
PeriodLegend: typeof import('./src/components/Period/PeriodLegend.vue')['default']
PhoneLogin: typeof import('./src/components/Login/PhoneLogin.vue')['default']
PlanCalendar: typeof import('./src/components/Plan/PlanCalendar.vue')['default']
PlanCard: typeof import('./src/components/Plan/PlanCard.vue')['default']
PlanQuery: typeof import('./src/components/Plan/PlanQuery.vue')['default']
ProductCard: typeof import('./src/components/Product/ProductCard.vue')['default']
ProductDrawer: typeof import('./src/components/Product/ProductDrawer.vue')['default']
ProductQuery: typeof import('./src/components/Product/ProductQuery.vue')['default']
ProfileAccount: typeof import('./src/components/Profile/ProfileAccount.vue')['default']
ProfileInfo: typeof import('./src/components/Profile/ProfileInfo.vue')['default']
ProfilePassword: typeof import('./src/components/Profile/ProfilePassword.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ServerCard: typeof import('./src/components/Server/ServerCard.vue')['default']
ServerProgress: typeof import('./src/components/Server/ServerProgress.vue')['default']
StatsCard: typeof import('./src/components/StatsCard.vue')['default']
TravelBasicData: typeof import('./src/components/Travel/Data/TravelBasicData.vue')['default']
TravelCalendar: typeof import('./src/components/Travel/TravelCalendar.vue')['default']
TravelCard: typeof import('./src/components/Travel/TravelCard.vue')['default']
TravelConsumeData: typeof import('./src/components/Travel/Data/TravelConsumeData.vue')['default']
TravelDailyItem: typeof import('./src/components/Travel/TravelDailyItem.vue')['default']
TravelDailyList: typeof import('./src/components/Travel/TravelDailyList.vue')['default']
TravelDialog: typeof import('./src/components/Travel/TravelDialog.vue')['default']
TravelGuideData: typeof import('./src/components/Travel/Data/TravelGuideData.vue')['default']
TravelQuery: typeof import('./src/components/Travel/TravelQuery.vue')['default']
TravelRecordData: typeof import('./src/components/Travel/Data/TravelRecordData.vue')['default']
TravelSpotList: typeof import('./src/components/Travel/TravelSpotList.vue')['default']
TravelStats: typeof import('./src/components/Travel/TravelStats.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanFloatingBubble: typeof import('vant/es')['FloatingBubble']
VanSwipeCell: typeof import('vant/es')['SwipeCell']
WeatherIcon: typeof import('./src/components/Common/WeatherIcon.vue')['default']
WeatherTimeCard: typeof import('./src/components/Common/WeatherTimeCard.vue')['default']
}
}

View File

@@ -8,6 +8,8 @@ server {
server_name _;
client_max_body_size 500M;
location /sweet-hut/ {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /sweet-hut/index.html;
@@ -17,10 +19,22 @@ server {
proxy_pass http://host.docker.internal:8081/;
}
location /alist-api/ {
proxy_pass http://host.docker.internal:5244/;
}
location /blog-api/ {
proxy_pass http://host.docker.internal:8082/;
}
location /image-api/ {
proxy_pass http://host.docker.internal:3000/;
}
location /agent-api/ {
proxy_pass http://host.docker.internal:8000/;
}
location / {
try_files $uri $uri/ =404;
}

View File

@@ -2,6 +2,7 @@
"name": "sweet-hut-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
@@ -23,7 +24,7 @@
"dayjs": "^1.11.13",
"echarts-liquidfill": "^3.1.0",
"element-china-area-data": "^6.1.0",
"element-plus": "~2.9.7",
"element-plus": "^2.9.7",
"file-icons-vue": "^1.3.4",
"font-awesome": "^4.7.0",
"lunar-javascript": "^1.7.2",
@@ -31,6 +32,7 @@
"pinia": "^2.3.1",
"ua-parser-js": "^2.0.3",
"v-calendar": "^3.1.2",
"vant": "^4.9.22",
"vue": "3.5.12",
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common",
"vue3-emoji-picker": "^1.1.8",
@@ -38,22 +40,16 @@
},
"devDependencies": {
"@types/node": "^18.19.86",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-vue": "5.2.1",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.21",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-vue": "^8.7.1",
"postcss": "^8.5.3",
"sass": "^1.86.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"unplugin-vue-components": "^32.1.0",
"vfonts": "^0.0.3",
"vite": "5.2.0",
"vite": "^5.2.0",
"vite-plugin-mock": "^3.0.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.27"

350
pnpm-lock.yaml generated
View File

@@ -54,7 +54,7 @@ importers:
specifier: ^6.1.0
version: 6.1.0
element-plus:
specifier: ~2.9.7
specifier: ^2.9.7
version: 2.9.7(vue@3.5.12(typescript@5.8.3))
file-icons-vue:
specifier: ^1.3.4
@@ -71,9 +71,15 @@ importers:
pinia:
specifier: ^2.3.1
version: 2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3))
ua-parser-js:
specifier: ^2.0.3
version: 2.0.6
v-calendar:
specifier: ^3.1.2
version: 3.1.2(@popperjs/core@2.11.8)(vue@3.5.12(typescript@5.8.3))
vant:
specifier: ^4.9.22
version: 4.9.22(vue@3.5.12(typescript@5.8.3))
vue:
specifier: 3.5.12
version: 3.5.12(typescript@5.8.3)
@@ -90,36 +96,15 @@ importers:
'@types/node':
specifier: ^18.19.86
version: 18.19.86
'@typescript-eslint/eslint-plugin':
specifier: ^5.62.0
version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3)
'@typescript-eslint/parser':
specifier: ^5.62.0
version: 5.62.0(eslint@7.32.0)(typescript@5.8.3)
'@vant/auto-import-resolver':
specifier: ^1.3.0
version: 1.3.0
'@vitejs/plugin-vue':
specifier: 5.2.1
specifier: ^5.2.1
version: 5.2.1(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.3)
eslint:
specifier: ^7.32.0
version: 7.32.0
eslint-config-standard:
specifier: ^16.0.3
version: 16.0.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0)
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)
eslint-plugin-node:
specifier: ^11.1.0
version: 11.1.0(eslint@7.32.0)
eslint-plugin-promise:
specifier: ^5.2.0
version: 5.2.0(eslint@7.32.0)
eslint-plugin-vue:
specifier: ^8.7.1
version: 8.7.1(eslint@7.32.0)
postcss:
specifier: ^8.5.3
version: 8.5.3
@@ -132,11 +117,14 @@ importers:
typescript:
specifier: ^5.8.3
version: 5.8.3
unplugin-vue-components:
specifier: ^32.1.0
version: 32.1.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))
vfonts:
specifier: ^0.0.3
version: 0.0.3
vite:
specifier: 5.2.0
specifier: ^5.2.0
version: 5.2.0(@types/node@18.19.86)(sass@1.86.3)
vite-plugin-mock:
specifier: ^3.0.2
@@ -489,6 +477,9 @@ packages:
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -500,6 +491,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
@@ -996,6 +990,17 @@ packages:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@vant/auto-import-resolver@1.3.0':
resolution: {integrity: sha512-lJyWtCyFizR4bHZvMiNMF3w+WTFTUWAvka1eqTnPK9ticUcKTCOx6qEmHcm8JPb3g1t3GaD2W3MnHkBp/nHamw==}
'@vant/popperjs@1.3.0':
resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
'@vant/use@1.6.0':
resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
peerDependencies:
vue: ^3.0.0
'@vavt/util@2.1.0':
resolution: {integrity: sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==}
@@ -1064,6 +1069,9 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vue/shared@3.5.28':
resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==}
'@vueuse/core@13.1.0':
resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==}
peerDependencies:
@@ -1096,8 +1104,13 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.18.0:
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -1337,6 +1350,10 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
class-utils@0.3.6:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
engines: {node: '>=0.10.0'}
@@ -1396,6 +1413,12 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
confbox@0.2.4:
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
connect@3.7.0:
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
engines: {node: '>= 0.10.0'}
@@ -1530,6 +1553,9 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-europe-js@0.1.2:
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
@@ -1829,6 +1855,9 @@ packages:
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
engines: {node: '>=0.10.0'}
exsolve@1.1.1:
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -1860,6 +1889,15 @@ packages:
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2228,6 +2266,9 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
is-standalone-pwa@0.1.1:
resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==}
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -2354,6 +2395,10 @@ packages:
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
engines: {node: '>=4.0.0'}
local-pkg@1.2.1:
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
engines: {node: '>=14'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@@ -2385,6 +2430,9 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
map-cache@0.2.2:
resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
engines: {node: '>=0.10.0'}
@@ -2472,6 +2520,9 @@ packages:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
mockjs@1.1.0:
resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==}
hasBin: true
@@ -2567,6 +2618,10 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
obug@2.1.4:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'}
on-finished@2.3.0:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'}
@@ -2625,6 +2680,9 @@ packages:
pathe@0.2.0:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2632,6 +2690,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
@@ -2649,6 +2711,12 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pkg-types@2.3.1:
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
posix-character-classes@0.1.1:
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
engines: {node: '>=0.10.0'}
@@ -2750,6 +2818,9 @@ packages:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-string@4.3.4:
resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==}
engines: {node: '>=0.10.0'}
@@ -2772,6 +2843,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -3074,6 +3149,10 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'}
@@ -3148,9 +3227,19 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ua-is-frozen@0.1.2:
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
ua-parser-js@2.0.6:
resolution: {integrity: sha512-EmaxXfltJaDW75SokrY4/lXMrVyXomE/0FpIIqP2Ctic93gK7rlme55Cwkz8l3YZ6gqf94fCU7AnIkidd/KXPg==}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.4:
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -3170,6 +3259,53 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
unplugin-utils@0.3.2:
resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
engines: {node: '>=20.19.0'}
unplugin-vue-components@32.1.0:
resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@nuxt/kit': ^3.2.2 || ^4.0.0
vue: ^3.0.0
peerDependenciesMeta:
'@nuxt/kit':
optional: true
unplugin@3.3.0:
resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@farmfe/core': '*'
'@rspack/core': '*'
bun-types-no-globals: '*'
esbuild: '*'
rolldown: '*'
rollup: '*'
unloader: '*'
vite: '*'
webpack: '*'
peerDependenciesMeta:
'@farmfe/core':
optional: true
'@rspack/core':
optional: true
bun-types-no-globals:
optional: true
esbuild:
optional: true
rolldown:
optional: true
rollup:
optional: true
unloader:
optional: true
vite:
optional: true
webpack:
optional: true
unset-value@1.0.0:
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
engines: {node: '>=0.10.0'}
@@ -3207,6 +3343,11 @@ packages:
v8-compile-cache@2.4.0:
resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
vant@4.9.22:
resolution: {integrity: sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==}
peerDependencies:
vue: ^3.0.0
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -3339,6 +3480,9 @@ packages:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
@@ -3822,12 +3966,19 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
@@ -4376,6 +4527,14 @@ snapshots:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
'@vant/auto-import-resolver@1.3.0': {}
'@vant/popperjs@1.3.0': {}
'@vant/use@1.6.0(vue@3.5.12(typescript@5.8.3))':
dependencies:
vue: 3.5.12(typescript@5.8.3)
'@vavt/util@2.1.0': {}
'@vitejs/plugin-vue@5.2.1(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))':
@@ -4481,6 +4640,8 @@ snapshots:
'@vue/shared@3.5.13': {}
'@vue/shared@3.5.28': {}
'@vueuse/core@13.1.0(vue@3.5.12(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
@@ -4517,13 +4678,15 @@ snapshots:
dependencies:
acorn: 7.4.1
acorn-jsx@5.3.2(acorn@8.14.1):
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.14.1
acorn: 8.15.0
acorn@7.4.1: {}
acorn@8.14.1: {}
acorn@8.15.0: {}
acorn@8.18.0: {}
ajv@6.12.6:
dependencies:
@@ -4802,6 +4965,10 @@ snapshots:
dependencies:
readdirp: 4.1.2
chokidar@5.0.0:
dependencies:
readdirp: 5.0.0
class-utils@0.3.6:
dependencies:
arr-union: 3.1.0
@@ -4861,6 +5028,10 @@ snapshots:
concat-map@0.0.1: {}
confbox@0.1.8: {}
confbox@0.2.4: {}
connect@3.7.0:
dependencies:
debug: 2.6.9
@@ -4989,6 +5160,8 @@ snapshots:
delayed-stream@1.0.0: {}
detect-europe-js@0.1.2: {}
detect-libc@1.0.3:
optional: true
@@ -5384,8 +5557,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.3
esprima@4.0.1: {}
@@ -5420,6 +5593,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
exsolve@1.1.1: {}
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
@@ -5462,6 +5637,10 @@ snapshots:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -5847,6 +6026,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-standalone-pwa@0.1.1: {}
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -5961,6 +6142,12 @@ snapshots:
emojis-list: 3.0.0
json5: 1.0.2
local-pkg@1.2.1:
dependencies:
mlly: 1.8.2
pkg-types: 2.3.1
quansync: 0.2.11
lodash-es@4.17.21: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
@@ -5985,6 +6172,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
map-cache@0.2.2: {}
map-visit@1.0.0:
@@ -6087,6 +6278,13 @@ snapshots:
for-in: 1.0.2
is-extendable: 1.0.1
mlly@1.8.2:
dependencies:
acorn: 8.18.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.4
mockjs@1.1.0:
dependencies:
commander: 13.1.0
@@ -6191,6 +6389,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
obug@2.1.4: {}
on-finished@2.3.0:
dependencies:
ee-first: 1.1.1
@@ -6243,10 +6443,14 @@ snapshots:
pathe@0.2.0: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.5: {}
pify@2.3.0: {}
pinia@2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3)):
@@ -6261,6 +6465,18 @@ snapshots:
pirates@4.0.7: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.2
pathe: 2.0.3
pkg-types@2.3.1:
dependencies:
confbox: 0.2.4
exsolve: 1.1.1
pathe: 2.0.3
posix-character-classes@0.1.1: {}
possible-typed-array-names@1.1.0: {}
@@ -6352,6 +6568,8 @@ snapshots:
dependencies:
side-channel: 1.1.0
quansync@0.2.11: {}
query-string@4.3.4:
dependencies:
object-assign: 4.1.1
@@ -6375,6 +6593,8 @@ snapshots:
readdirp@4.1.2: {}
readdirp@5.0.0: {}
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -6780,6 +7000,11 @@ snapshots:
dependencies:
any-promise: 1.3.0
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
to-object-path@0.3.0:
dependencies:
kind-of: 3.2.2
@@ -6878,8 +7103,18 @@ snapshots:
typescript@5.8.3: {}
ua-is-frozen@0.1.2: {}
ua-parser-js@2.0.6:
dependencies:
detect-europe-js: 0.1.2
is-standalone-pwa: 0.1.1
ua-is-frozen: 0.1.2
uc.micro@2.1.0: {}
ufo@1.6.4: {}
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -6900,6 +7135,44 @@ snapshots:
unpipe@1.0.0: {}
unplugin-utils@0.3.2:
dependencies:
pathe: 2.0.3
picomatch: 4.0.5
unplugin-vue-components@32.1.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3)):
dependencies:
chokidar: 5.0.0
local-pkg: 1.2.1
magic-string: 0.30.21
mlly: 1.8.2
obug: 2.1.4
picomatch: 4.0.5
tinyglobby: 0.2.17
unplugin: 3.3.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))
unplugin-utils: 0.3.2
vue: 3.5.12(typescript@5.8.3)
transitivePeerDependencies:
- '@farmfe/core'
- '@rspack/core'
- bun-types-no-globals
- esbuild
- rolldown
- rollup
- unloader
- vite
- webpack
unplugin@3.3.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3)):
dependencies:
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.5
webpack-virtual-modules: 0.6.2
optionalDependencies:
esbuild: 0.20.2
rollup: 4.40.0
vite: 5.2.0(@types/node@18.19.86)(sass@1.86.3)
unset-value@1.0.0:
dependencies:
has-value: 0.3.1
@@ -6936,6 +7209,13 @@ snapshots:
v8-compile-cache@2.4.0: {}
vant@4.9.22(vue@3.5.12(typescript@5.8.3)):
dependencies:
'@vant/popperjs': 1.3.0
'@vant/use': 1.6.0(vue@3.5.12(typescript@5.8.3))
'@vue/shared': 3.5.28
vue: 3.5.12(typescript@5.8.3)
vary@1.1.2: {}
vfonts@0.0.3: {}
@@ -7072,6 +7352,8 @@ snapshots:
webidl-conversions@7.0.0: {}
webpack-virtual-modules@0.6.2: {}
whatwg-mimetype@3.0.0: {}
which-boxed-primitive@1.1.1:

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

35
src/apis/award.ts Normal file
View File

@@ -0,0 +1,35 @@
import { cloudService } from './index'
import { IAward, IAwardQuery } from '@/types/award.ts'
export const addAwardApi = (award: IAward): Promise<boolean> =>
cloudService({
url: '/home-api/award',
method: 'post',
data: award
})
export const updateAwardApi = (id: number, award: IAward): Promise<boolean> =>
cloudService({
url: `/home-api/award/${id}`,
method: 'put',
data: award
})
export const deleteAwardApi = (id: number): Promise<boolean> =>
cloudService({
url: `/home-api/award/${id}`,
method: 'delete'
})
export const queryAwardApi = (id: number): Promise<IAward> =>
cloudService({
url: `/home-api/award/${id}`,
method: 'get'
})
export const queryAwardListApi = (query: IAwardQuery): Promise<IAward[]> =>
cloudService({
url: '/home-api/award',
method: 'get',
params: query
})

View File

@@ -4,7 +4,7 @@ import {
IBillCategory,
IBillPay,
IBillQuery,
IBillRecord
IBillRecord, IBillSearch
} from '@/types/bill'
import { IStatsChart } from 'vue3-common/dist/types'
@@ -41,6 +41,13 @@ export const queryBillSummaryApi = (query: IBillQuery): Promise<IBillRecord[]> =
params: query
})
export const searchBillSummaryApi = (search: IBillSearch): Promise<IBillRecord[]> =>
cloudService({
url: '/home-api/bill/search',
method: 'get',
params: search
})
export const queryBillStatsApi = (type: string, query: IBillQuery): Promise<IStatsChart[]> =>
cloudService({
url: `/home-api/bill/stats/${type}`,

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IBlog, IBlogCategory, IBlogComment, IBlogStats, INewBlog, IQueryBlog } from '@/types/blog'
import { IBlog, IBlogCategory, IBlogComment, IBlogStats, IQueryBlog } from '@/types/blog'
import qs from 'qs'
import { IPageRecord } from '@/types'
@@ -23,7 +23,7 @@ export const queryBlogByConditionApi = (query: IQueryBlog): Promise<IBlog[]> =>
export const queryBlogByIdApi = (id: number): Promise<IBlog> =>
cloudService({
url: `/blog-api/blog/content/${id}`,
url: `/blog-api/blog/${id}/content`,
method: 'get'
})
@@ -47,33 +47,6 @@ export const queryLatestBlogApi = (): Promise<IBlog[]> =>
export const queryBlogCommentApi = (blogId: number): Promise<IBlogComment[]> =>
cloudService({
url: `/blog-api/blog/comment/${blogId}`,
url: `/blog-api/blog/${blogId}/comment`,
method: 'get'
})
export const addBlogCommentApi = (blogComment: IBlogComment): Promise<boolean> =>
cloudService({
url: '/blog-api/blog/comment',
method: 'put',
data: blogComment
})
export const addBlogApi = (blog: INewBlog): Promise<boolean> =>
cloudService({
url: '/admin-api/blog-manage',
method: 'post',
data: blog
})
export const updateBlogApi = (id: number, blog: INewBlog): Promise<boolean> =>
cloudService({
url: `/admin-api/blog-manage/${id}`,
method: 'put',
data: blog
})
export const deleteBlogApi = (id: number): Promise<boolean> =>
cloudService({
url: `/admin-api/blog-manage/${id}`,
method: 'DELETE'
})

View File

@@ -1,49 +0,0 @@
import { cloudService } from './index'
import { IBudgetInfo } from '@/types/budget'
export const addBudgetApi = (budget: IBudgetInfo): Promise<any> =>
cloudService({
url: '/home-api/budget',
method: 'post',
data: budget
})
export const updateBudgetApi = (id: number, budget: IBudgetInfo): Promise<any> =>
cloudService({
url: `/home-api/budget/${id}`,
method: 'put',
data: budget
})
export const deleteBudgetApi = (id: number): Promise<any> =>
cloudService({
url: `/home-api/budget/${id}`,
method: 'DELETE'
})
export const queryBudgetApi = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget',
method: 'get',
params: { category }
})
export const queryBudgetCategory = (): Promise<any> =>
cloudService({
url: '/home-api/budget/category',
method: 'get'
})
export const queryBudgetRankStats = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget/stats/rank',
method: 'get',
params: { category }
})
export const queryBudgetProjectStats = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget/stats/project',
method: 'get',
params: { category }
})

29
src/apis/car.ts Normal file
View File

@@ -0,0 +1,29 @@
import { cloudService } from './index'
import { ICarEvent, ICarEventQuery } from '@/types/car.ts'
export const addCarEventApi = (carEvent: ICarEvent): Promise<boolean> =>
cloudService({
url: '/home-api/car',
method: 'post',
data: carEvent
})
export const updateCarEventApi = (id: number, carEvent: ICarEvent): Promise<boolean> =>
cloudService({
url: `/home-api/car/${id}`,
method: 'put',
data: carEvent
})
export const deleteCarEventApi = (id: number): Promise<boolean> =>
cloudService({
url: `/home-api/car/${id}`,
method: 'delete'
})
export const queryCarEventListApi = (query: ICarEventQuery): Promise<ICarEvent[]> =>
cloudService({
url: '/home-api/car',
method: 'get',
params: query
})

View File

@@ -1,5 +1,6 @@
import { cloudService } from './index'
import { IFileInfo } from '@/types/file'
import type { AxiosProgressEvent } from 'axios'
export const uploadFileApi = (form: FormData, path: string, md5: string): Promise<any> =>
cloudService({
@@ -12,6 +13,19 @@ export const uploadFileApi = (form: FormData, path: string, md5: string): Promis
data: form
})
export const uploadAListFileApi = (form: FormData,
progress: (event: AxiosProgressEvent) => void): Promise<string> =>
cloudService({
url: '/home-api/file/alist',
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: progress,
timeout: 0,
method: 'post',
data: form
})
export const getFolderInfoApi = (folderName: string): Promise<IFileInfo[]> =>
cloudService({
url: '/home-api/file/catalog',

View File

@@ -1,5 +1,6 @@
import { cloudService } from './index'
import { IFoodQuery, IFoodRecipe, IFoodRecord, IFoodStats } from '@/types/food'
import { IPageRecord } from '@/types'
export const addFoodRecipeApi = (foodRecipe: IFoodRecipe): Promise<boolean> =>
cloudService({
@@ -21,20 +22,27 @@ export const deleteFoodRecipeApi = (id: number): Promise<boolean> =>
method: 'DELETE'
})
export const queryFoodRecipeByIdApi = (id: number): Promise<any> =>
export const queryFoodRecipeByIdApi = (id: number): Promise<IFoodRecipe> =>
cloudService({
url: `/home-api/food/recipe/${id}`,
method: 'get'
})
export const queryFoodRecipeApi = (currentPage: number, pageSize: number,
query: IFoodQuery): Promise<any> =>
query: IFoodQuery): Promise<IPageRecord<IFoodRecipe>> =>
cloudService({
url: '/home-api/food/recipe/page',
method: 'get',
params: { currentPage, pageSize, ...query }
})
export const searchFoodRecipeApi = (name: string): Promise<IFoodRecipe> =>
cloudService({
url: '/home-api/food/recipe/search',
method: 'get',
params: { name }
})
export const queryFoodNameListApi = (): Promise<string[]> =>
cloudService({
url: '/home-api/food/recipe/name',

16
src/apis/health.ts Normal file
View File

@@ -0,0 +1,16 @@
import { cloudService } from './index'
import { IHealth, IHealthQuery } from '@/types/health'
export const updateHealthApi = (id: number, health: IHealth): Promise<boolean> =>
cloudService({
url: `/home-api/health/${id}`,
method: 'put',
data: health
})
export const queryHealthApi = (query: IHealthQuery): Promise<IHealth[]> =>
cloudService({
url: '/home-api/health',
method: 'get',
params: query
})

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IPassword, IQueryPassword } from '@/types/password'
import { IPassword, IPasswordQuery } from '@/types/password'
export const addPasswordApi = (password: IPassword): Promise<boolean> =>
cloudService({
@@ -21,7 +21,7 @@ export const deletePasswordApi = (id: number): Promise<boolean> =>
method: 'delete'
})
export const queryPasswordApi = (query: IQueryPassword): Promise<IPassword[]> =>
export const queryPasswordApi = (query: IPasswordQuery): Promise<IPassword[]> =>
cloudService({
url: '/home-api/password',
method: 'get',

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IPeriodDay, IPeriodSetting } from '@/types/period.ts'
import { IMenstrualPeriod, IPeriodDay, IPeriodSetting } from '@/types/period.ts'
export const updatePeriodDayApi = (id: number, periodDay: IPeriodDay): Promise<boolean> =>
@@ -16,6 +16,12 @@ export const queryPeriodDayApi = (month: string): Promise<IPeriodDay[]> =>
params: { month }
})
export const getMenstrualDaysApi = (): Promise<IMenstrualPeriod[]> =>
cloudService({
url: '/home-api/period//menstruate/days',
method: 'get'
})
export const getMenstruateFirstDateApi = (month: string): Promise<string> =>
cloudService({
url: '/home-api/period/menstruate',

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IProduct, IQueryProduct } from '@/types/product.ts'
import { IProduct, IProductQuery } from '@/types/product.ts'
export const addProductApi = (product: IProduct): Promise<boolean> =>
cloudService({
@@ -27,7 +27,7 @@ export const queryProductApi = (id: number): Promise<IProduct> =>
method: 'get'
})
export const queryProductListApi = (query: IQueryProduct): Promise<IProduct[]> =>
export const queryProductListApi = (query: IProductQuery): Promise<IProduct[]> =>
cloudService({
url: '/home-api/product',
method: 'get',

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="isMobile()" class="mobile-common-query card-item">
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="anniversaryStore.queryInfo.category"
@@ -7,27 +7,11 @@
@change="anniversaryStore.queryAnniversaryList()"/>
</div>
</div>
<div v-else class="common-query">
<el-select v-model="anniversaryStore.queryInfo.category"
placeholder="请选择纪念日类别" clearable
@change="anniversaryStore.queryAnniversaryList()"
style="width: 180px">
<el-option
v-for="(item, index) in queryCategoryList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
<el-button :icon="Refresh" circle @click="refreshClick"/>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAnniversaryStore } from '@/store/anniversary.ts'
import { Refresh } from '@element-plus/icons-vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ICategory } from '@/types'
const anniversaryStore = useAnniversaryStore()
@@ -39,11 +23,6 @@ onMounted(async () => {
initCategoryList()
})
const refreshClick = async () => {
anniversaryStore.queryInfo.category = ''
await anniversaryStore.queryAnniversaryList()
}
const initCategoryList = () => {
queryCategoryList.value = []
queryCategoryList.value.push({

View File

@@ -12,6 +12,7 @@ import { defineProps, PropType } from 'vue'
import { IAnniversaryCategory } from '@/types/anniversary.ts'
import { useAnniversaryStore } from '@/store/anniversary.ts'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const props = defineProps({
categoryItem: {
@@ -28,8 +29,12 @@ const router = useRouter()
const anniversaryStore = useAnniversaryStore()
const selectCategoryClick = () => {
if (props.count) {
anniversaryStore.queryInfo.category = props.categoryItem?.name as string
router.push({ name: 'AnniversaryList' })
} else {
ElMessage.info('暂无纪念日')
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="isMobile()" class="mobile-common-query card-item">
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="assetStore.queryInfo.category"
@@ -7,27 +7,11 @@
@change="assetStore.refreshInfo()"/>
</div>
</div>
<div v-else class="common-query">
<el-select v-model="assetStore.queryInfo.category"
placeholder="请选择纪念日类别" clearable
@change="assetStore.refreshInfo()"
style="width: 180px">
<el-option
v-for="(item, index) in queryCategoryList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
<el-button :icon="Refresh" circle @click="refreshClick"/>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAssetStore } from '@/store/asset.ts'
import { Refresh } from '@element-plus/icons-vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ICategory } from '@/types'
const assetStore = useAssetStore()
@@ -39,11 +23,6 @@ onMounted(async () => {
initCategoryList()
})
const refreshClick = async () => {
assetStore.queryInfo.category = ''
await assetStore.queryAssetList()
}
const initCategoryList = () => {
queryCategoryList.value = []
queryCategoryList.value.push({

View File

@@ -0,0 +1,97 @@
<template>
<div class="life-record-card card-item">
<el-image v-if="props.item?.imageList.length > 0"
:src="`${props.item?.imageList[0]}${ThumbImageQuery}`"
fit="contain" alt="暂无图片" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
</div>
</template>
</el-image>
<el-empty v-else description="暂无图片" />
<van-swipe-cell>
<div class="record-info" @click="editClick()">
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.item.name }}</span>
</div>
</div>
</div>
<div class="bottom-container">
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-location-dot" style="color: #6A5ACD"></i>
{{ props.item.location }}
</div>
</div>
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-flag"></i>
{{ props.item.owner }}
</div>
<div class="info-item">
<i class="fa-regular fa-calendar-check"></i>
{{ props.item.date }}
</div>
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { useAwardStore } from '@/store/award.ts'
import { defineProps, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
import { showImagePreview } from 'vant'
import { IAward } from '@/types/award.ts'
const awardStore = useAwardStore()
const router = useRouter()
const props = defineProps({
item: {
required: true,
type: Object as PropType<IAward>
}
})
const viewImageClick = () => {
const imageList = props.item.imageList.map((item) => `${item}${RawImageQuery}`)
showImagePreview({ images: imageList, closeable: true })
}
const editClick = async () => {
const id = props.item?.id
awardStore.awardApiType = 'UPDATE'
awardStore.currentAward = props.item as IAward
await router.push({ name: 'AwardDetailId', params: { id } })
}
const deleteClick = () => {
awardStore.awardApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该记录: ${props.item?.name}?`).then(() => {
awardStore.handleAwardApi(props.item as IAward)
})
}
</script>
<style lang="scss">
@use "@/styles/home/life.record.module";
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>类型</span>
<el-segmented v-model="awardStore.queryInfo.type"
:options="queryTypeList"
@change="awardStore.queryAwardList()"/>
</div>
</div>
</template>
<script setup lang="ts">
import { useAwardStore } from '@/store/award.ts'
const awardStore = useAwardStore()
const queryTypeList = [
{
label: '全部',
value: ''
},
{
label: '获奖',
value: '获奖'
},
{
label: '技能',
value: '技能'
}, {
label: '身份',
value: '身份'
}, {
label: '财产',
value: '财产'
}
]
</script>

View File

@@ -1,166 +0,0 @@
<template>
<div class="bill-setting">
<div class="button-container">
<el-button type="primary" @click="addNewBookClick">新增账本</el-button>
</div>
<el-table
:data="billStore.billBookList"
border stripe
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
style="width: 100%"
class="card-item">
<el-table-column type="index" align="center" width="50"/>
<el-table-column prop="name" label="名称" align="center" width="100"/>
<el-table-column label="图标" align="center" width="80">
<template #default="scope">
<i :class="scope.row.icon" :style="{color: scope.row.color, fontSize: '24px'}"></i>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="editBookClick(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteBookClick(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="billInfo.billBookDialog.visible"
:title="billInfo.billBookDialog.title"
center width="80%" @open="openDialogEvent">
<el-form ref="formRef" :model="billInfo.billBookDialog.data"
:rules="rules" label-position="top" label-width="100px">
<el-form-item label="账本名称" prop="name">
<el-input v-model="billInfo.billBookDialog.data.name" autocomplete="off"
placeholder="请输入类型名称"
:maxlength="10" show-word-limit/>
</el-form-item>
<el-form-item label="类型图标" prop="icon">
<el-input v-model="billInfo.billBookDialog.data.icon" autocomplete="off"
placeholder="请输入类型图标"
:maxlength="40" show-word-limit/>
</el-form-item>
<el-form-item label="图标颜色" prop="color">
<el-color-picker v-model="billInfo.billBookDialog.data.color" />
</el-form-item>
<el-form-item label="图标预览">
<i :class="billInfo.billBookDialog.data.icon"
:style="{color: billInfo.billBookDialog.data.color, fontSize: '24px'}"></i>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确认</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { useBillStore } from '@/store/bill.ts'
import { ElMessage, FormRules } from 'element-plus'
import { IBillBook } from '@/types/bill.ts'
import { billBookRules } from '@/utils/element/elRules.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import type { IDialog, IHandleApi } from 'vue3-common/types'
import { addBillBookApi, updateBillBookApi } from '@/apis/bill.ts'
import { tableCellStyle, tableHeaderStyle, tableRowStyle } from 'vue3-common/utils/elUtil'
const formRef = ref()
const rules = reactive<FormRules>(billBookRules)
const billStore = useBillStore()
const billInfo = reactive({
setBillBookType: 'ADD' as IHandleApi,
billBookDialog: {
title: '',
visible: false,
data: {}
} as IDialog<IBillBook>
})
onMounted(async () => {
await billStore.queryBillBook()
})
/**
* 点击新增账本按钮
*/
const addNewBookClick = () => {
billInfo.setBillBookType = 'ADD'
billInfo.billBookDialog = {
title: '新增账本',
visible: true,
data: {
name: '',
icon: '',
color: ''
}
}
}
/**
* 点击编辑账本按钮
* @param book 账本
*/
const editBookClick = async (book: IBillBook) => {
billInfo.setBillBookType = 'UPDATE'
billInfo.billBookDialog = {
title: '编辑账本',
visible: true,
data: deepCopyObject(book)
}
}
/**
* 点击删除账本按钮
* @param book 账本
*/
const deleteBookClick = (book: IBillBook) => {
billInfo.setBillBookType = 'DELETE'
ElMessage.error('暂不支持该功能')
}
const openDialogEvent = () => {
nextTick(() => {
formRef.value.clearValidate()
})
}
const setBillBookApi = async (id?: number) => {
switch (billInfo.setBillBookType) {
case 'ADD':
await addBillBookApi(billInfo.billBookDialog.data)
ElMessage.success('新增账本成功')
break
case 'UPDATE':
await updateBillBookApi(id as number, billInfo.billBookDialog.data)
ElMessage.success('更新账本成功')
break
case 'DELETE':
ElMessage.success('删除账本成功')
break
default:
break
}
await billStore.queryBillBook()
billInfo.billBookDialog.visible = false
}
const cancelClick = () => {
billInfo.billBookDialog.visible = false
}
const confirmClick = async () => {
await formRef.value.validate(async (valid) => {
if (valid) {
await setBillBookApi(billInfo.billBookDialog.data.id)
}
})
}
</script>

View File

@@ -67,7 +67,7 @@ const getCalendarAttrDates = () => {
})
dailyInfo.attrs.push({
dot: 'red',
dates: getCalendarDateList(billStore.billRecordList, 'expensive')
dates: getCalendarDateList(billStore.billRecordList, 'expense')
})
dailyInfo.attrs.push({
dot: 'green',

View File

@@ -1,222 +0,0 @@
<template>
<div class="bill-setting">
<div class="button-container">
<el-select
v-model="billInfo.bookName"
placeholder="请选择账本名称"
@focus="focusBookNameEvent"
@change="selectBookNameEvent"
style="width: 150px;">
<el-option
v-for="(item, index) in billInfo.billBookList"
:key="index" :label="item.name" :value="item.name"
/>
</el-select>
<el-button type="primary" @click="addNewCategoryClick">新增类别</el-button>
</div>
<el-table
:data="billInfo.billCategoryList"
border stripe
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
class="card-item">
<el-table-column type="index" align="center" width="50"/>
<el-table-column prop="name" label="名称" align="center" width="80"/>
<el-table-column label="类型" align="center" width="80">
<template #default="scope">
<el-tag v-if="scope.row.type === 'expensive'" effect="dark" color="#CA6C65">支出</el-tag>
<el-tag v-else effect="dark" color="#6FBB69">收入</el-tag>
</template>
</el-table-column>
<el-table-column label="图标" align="center" width="80">
<template #default="scope">
<i :class="scope.row.icon" :style="{color: scope.row.color, fontSize: '24px'}"></i>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" size="small" @click="editCategoryClick(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteCategoryClick(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="billInfo.billCategoryDialog.visible"
:title="billInfo.billCategoryDialog.title"
center width="80%" @open="openDialogEvent">
<el-form ref="formRef" :model="billInfo.billCategoryDialog.data"
:rules="rules" label-position="top" label-width="100px">
<el-form-item label="账本名称" prop="bookName">
<el-select
v-model="billInfo.billCategoryDialog.data.bookName"
placeholder="请选择账本名称"
@focus="focusBookNameEvent">
<el-option
v-for="(item, index) in billInfo.billBookList"
:key="index" :label="item.name" :value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="类别名称" prop="name">
<el-input v-model="billInfo.billCategoryDialog.data.name" autocomplete="off"
placeholder="请输入类型名称"
:maxlength="10" show-word-limit/>
</el-form-item>
<el-form-item label="类型名称" prop="type">
<el-radio-group v-model="billInfo.billCategoryDialog.data.type">
<el-radio value="expensive">支出</el-radio>
<el-radio value="income">收入</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="类别图标" prop="icon">
<el-input v-model="billInfo.billCategoryDialog.data.icon" autocomplete="off"
placeholder="请输入类型图标"
:maxlength="40" show-word-limit/>
</el-form-item>
<el-form-item label="图标颜色" prop="color">
<el-color-picker v-model="billInfo.billCategoryDialog.data.color" />
</el-form-item>
<el-form-item label="图标预览">
<i :class="billInfo.billCategoryDialog.data.icon"
:style="{color: billInfo.billCategoryDialog.data.color, fontSize: '24px'}"></i>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确认</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { useBillStore } from '@/store/bill.ts'
import { IBillBook, IBillCategory } from '@/types/bill.ts'
import { ElMessage, FormRules } from 'element-plus'
import { billCategoryRules } from '@/utils/element/elRules.ts'
import { addBillCategoryApi, updateBillCategoryApi } from '@/apis/bill.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { tableHeaderStyle, tableRowStyle, tableCellStyle } from 'vue3-common/utils/elUtil'
import type { IDialog, IHandleApi } from 'vue3-common/types'
const formRef = ref()
const rules = reactive<FormRules>(billCategoryRules)
const billStore = useBillStore()
const billInfo = reactive({
bookName: '日常账本',
setBillCategoryType: 'ADD' as IHandleApi,
billCategoryDialog: {
title: '',
visible: false,
data: {}
} as IDialog<IBillCategory>,
billBookList: [] as IBillBook[],
billCategoryList: [] as IBillCategory[]
})
onMounted(async () => {
await billStore.queryBillCategory()
queryBookCategoryByBookName(billInfo.bookName)
})
const selectBookNameEvent = (value: string) => {
queryBookCategoryByBookName(value)
}
const queryBookCategoryByBookName = (bookName: string) => {
billInfo.billCategoryList = billStore.billCategoryList.filter((item) => {
return item.bookName === bookName
})
}
/**
* 点击新增类别按钮
*/
const addNewCategoryClick = () => {
billInfo.setBillCategoryType = 'ADD'
billInfo.billCategoryDialog = {
title: '新增账单类别',
visible: true,
data: {
bookName: '',
name: '',
type: '',
icon: '',
color: ''
}
}
}
/**
* 点击编辑类别按钮
* @param category 类别
*/
const editCategoryClick = async (category: IBillCategory) => {
billInfo.setBillCategoryType = 'UPDATE'
billInfo.billCategoryDialog = {
title: '编辑账单类别',
visible: true,
data: deepCopyObject(category)
}
}
/**
* 点击删除类别按钮
* @param category 类别
*/
const deleteCategoryClick = (category: IBillCategory) => {
billInfo.setBillCategoryType = 'DELETE'
ElMessage.error('暂不支持该功能')
}
const openDialogEvent = () => {
nextTick(() => {
formRef.value.clearValidate()
})
}
const focusBookNameEvent = async () => {
await billStore.queryBillBook()
billInfo.billBookList = billStore.billBookList
}
const setBillCategoryApi = async (id?: number) => {
switch (billInfo.setBillCategoryType) {
case 'ADD':
await addBillCategoryApi(billInfo.billCategoryDialog.data)
ElMessage.success('新增账单类别成功')
break
case 'UPDATE':
await updateBillCategoryApi(id as number, billInfo.billCategoryDialog.data)
ElMessage.success('更新账单类别成功')
break
case 'DELETE':
ElMessage.success('删除账单类别成功')
break
default:
break
}
await billStore.queryBillCategory()
billInfo.billCategoryDialog.visible = false
}
const cancelClick = () => {
billInfo.billCategoryDialog.visible = false
}
const confirmClick = async () => {
await formRef.value.validate(async (valid) => {
if (valid) {
await setBillCategoryApi(billInfo.billCategoryDialog.data.id)
queryBookCategoryByBookName(billInfo.bookName)
}
})
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="bill-chart"
<div class="bill-chart common-chart"
v-loading="billStore.isLoading"
element-loading-text="正在加载中...">
<div class="chart-container card-item">
@@ -8,19 +8,22 @@
</div>
<div class="chart" ref="billDailyChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊类别统计</span>
</div>
<div class="chart" ref="billCategoryChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊账本统计</span>
</div>
<div class="chart" ref="billBookChartRef"/>
</div>
<div v-if="isMobile()" class="chart-container card-item">
<div class="chart-container card-item">
<div class="content">
<span class="title">📊排行榜</span>
</div>
@@ -28,12 +31,6 @@
<bill-rank />
</div>
</div>
<div v-else class="chart-container card-item">
<div class="content">
<span class="title">📊排行榜</span>
</div>
<div class="chart" ref="billRankChartRef"/>
</div>
</div>
</template>
@@ -44,8 +41,7 @@ import * as echarts from 'echarts'
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
import { useBillStore } from '@/store/bill.ts'
import { useAppStore } from '@/store/app.ts'
import type { IStatsChart } from 'vue3-common/types'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { formatDate } from 'vue3-common/utils/dateUtil'
const billStore = useBillStore()
const appStore = useAppStore()
@@ -53,12 +49,10 @@ const appStore = useAppStore()
const billDailyChartRef = ref<HTMLElement>()
const billBookChartRef = ref<HTMLElement>()
const billCategoryChartRef = ref<HTMLElement>()
const billRankChartRef = ref<HTMLElement>()
let billDailyChart: echarts.ECharts
let billBookChart: echarts.ECharts
let billCategoryChart: echarts.ECharts
let billRankChart: echarts.ECharts
watch(() => billStore.isRefresh, () => {
updateChart()
@@ -68,24 +62,21 @@ onMounted(() => {
billDailyChart = echarts.init(billDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
billBookChart = echarts.init(billBookChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
billCategoryChart = echarts.init(billCategoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
if (!isMobile()) {
billRankChart = echarts.init(billRankChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
}
})
const updateChart = () => {
const MAX_CATEGORY_COUNT = 5
const MAX_RANK_COUNT = 10
// 收支统计图
const dailyXAxis = billStore.billDateStatsList.map((item) => item.name)
const dailyXAxis = billStore.billDateStatsList.map((item) => {
return formatDate(item.name, 'MM-DD')
})
const dailyYAxis = billStore.billDateStatsList.map((item) => item.value)
billDailyChart.setOption(lineChartOptions('日期', dailyXAxis, '金额(元)', dailyYAxis))
billDailyChart.setOption({
series: [{
itemStyle: {
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69'
color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
},
markPoint: {
label: {
@@ -105,7 +96,7 @@ const updateChart = () => {
billBookChart.setOption({
series: [{
itemStyle: {
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69'
color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
},
markPoint: {
label: {
@@ -119,7 +110,6 @@ const updateChart = () => {
const billCategoryList = billStore.billCategoryStatsList.slice(0, MAX_CATEGORY_COUNT)
billCategoryChart.setOption(pieChartOptions('rank', '元', billCategoryList))
if (isMobile()) {
billCategoryChart.setOption({
series: [{
radius: ['20%', '50%'],
@@ -129,19 +119,4 @@ const updateChart = () => {
}]
})
}
// 排行榜统计图
const billRankList: IStatsChart[] = []
const resultList = billStore.billRecordList.filter((item) => item.type === billStore.billType)
resultList.sort((a, b) => b.amount - a.amount).forEach((item) => {
billRankList.push({
name: item.content,
value: item.amount
})
})
if (!isMobile()) {
billRankChart.setOption(pieChartOptions('rank', '元', billRankList.slice(0, MAX_RANK_COUNT)))
}
}
</script>

View File

@@ -5,7 +5,7 @@
<daily-summary :date="dailyItemInfo.date">
<template #extra>
<span>收入{{ formatAmount(dailyItemInfo.totalIncome, true) }}</span>
<span>支出{{ formatAmount(dailyItemInfo.totalExpensive, true) }}</span>
<span>支出{{ formatAmount(dailyItemInfo.totalExpense, true) }}</span>
</template>
</daily-summary>
@@ -40,16 +40,12 @@ import DailySummary from '@/components/DailySummary.vue'
import DailyCard from '@/components/DailyCard.vue'
import { defineProps, onMounted, reactive, watch } from 'vue'
import { useBillStore } from '@/store/bill.ts'
import { useRouter } from 'vue-router'
import { formatChineseDate } from 'vue3-common/utils/dateUtil'
import { IBillRecord } from '@/types/bill.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { formatAmount } from 'vue3-common/utils/numberUtil'
import { getCalendarDailyCount, getCategoryByName } from '@/utils/home/billUtil.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const billStore = useBillStore()
const router = useRouter()
const props = defineProps({
date: {
@@ -72,7 +68,7 @@ onMounted(() => {
const dailyItemInfo = reactive({
date: '',
totalExpensive: 0,
totalExpense: 0,
totalIncome: 0,
itemList: [] as IBillRecord[]
})
@@ -88,7 +84,7 @@ const getBillDailyByDate = (date: string) => {
const calendarDaily = getCalendarDailyCount(billStore.billRecordList, date)
dailyItemInfo.itemList = calendarDaily.dailyRecordList
dailyItemInfo.totalIncome = calendarDaily.totalIncome
dailyItemInfo.totalExpensive = calendarDaily.totalExpensive
dailyItemInfo.totalExpense = calendarDaily.totalExpense
} else {
dailyItemInfo.itemList = []
}
@@ -101,19 +97,7 @@ const getBillDailyByDate = (date: string) => {
const selectBillRecordClick = async (billRecord: IBillRecord) => {
await billStore.queryBillRecord(billRecord.id as number)
billStore.billApiType = 'UPDATE'
if (isMobile()) {
await router.push({
name: 'BillDetailId',
params: { id: billStore.currentBillRecord.id }
})
} else {
billStore.billDialog = {
title: '编辑账单',
visible: true,
data: deepCopyObject(billStore.currentBillRecord)
}
}
billStore.billDrawer = true
}
</script>

View File

@@ -1,235 +0,0 @@
<template>
<el-dialog v-model="billStore.billDialog.visible"
title="账单内容" center class="bill-dialog"
@open="openDialogEvent">
<el-form ref="formRef" :model="billStore.billDialog.data"
:rules="rules" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="账本类型" prop="bookName">
<el-radio-group v-model="billStore.billDialog.data.bookName"
@change="changeBillBookNameEvent">
<el-radio v-for="(item, index) in billStore.billBookList"
:value="item.name" :key="index">
<template #default>
<i :class="item.icon" :style="{color: item.color}"/>
<span style="margin-left: 5px;">{{ item.name }}</span>
</template>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="账单类别" prop="type">
<el-radio-group v-model="billStore.billDialog.data.type"
@change="changeBillCategoryEvent">
<el-radio v-for="(item, index) in billInfo.billTypeList"
:value="item" :key="index">
{{ getBillTypeName(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="账单分类" prop="category">
<el-radio-group v-model="billStore.billDialog.data.category">
<el-radio v-for="(item, index) in billInfo.billCategoryList"
:value="item.name" :key="index">
<template #default>
<i :class="item.icon" :style="{color: item.color}"/>
<span style="margin-left: 5px;">{{ item.name }}</span>
</template>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="账单时间" prop="date">
<el-date-picker
v-model="billStore.billDialog.data.date"
type="date" :editable="false" placeholder="请选择账单时间"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="账单地点" prop="location">
<el-input v-model="billStore.billDialog.data.location" autocomplete="off"
placeholder="请输入账单地点" clearable
show-word-limit maxlength="10">
<template #prefix>
<el-icon class="el-input__icon"><location/></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="账单账户" prop="payAccount">
<el-radio-group v-model="billStore.billDialog.data.payAccount">
<el-radio v-for="(item, index) in billStore.billPayList"
:value="item.name" :key="index">
<template #default>
<i :class="item.icon" :style="{color: item.color}"/>
<span style="margin-left: 5px;">{{ item.name }}</span>
</template>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="账单金额" prop="amount">
<amount-input v-model="billStore.billDialog.data.amount"/>
</el-form-item>
<el-form-item label="账单内容" prop="content">
<el-input v-model="billStore.billDialog.data.content" autocomplete="off"
placeholder="请输入账单内容" clearable
show-word-limit maxlength="10">
<template #prefix>
<el-icon class="el-input__icon"><document/></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="账单备注">
<el-input v-model="billStore.billDialog.data.remark" type="textarea"
placeholder="请输入账单备注" clearable
:rows="5" show-word-limit maxlength="200">
</el-input>
</el-form-item>
<el-form-item label="账单图片">
<upload-image v-model="billStore.billDialog.data.imageList"
:service-url="fileServiceUrl"
:service-file-root-path="serviceFileRootPath"
:image-path="billStore.billImagePath"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">
{{ billStore.billApiType === 'UPDATE' ? '编辑' : '新增' }}
</el-button>
<el-button v-if="billStore.billApiType === 'UPDATE'"
type="danger" @click="deleteClick">
删除
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { UploadImage, AmountInput } from 'vue3-common'
import { useBillStore } from '@/store/bill.ts'
import { Location, Document } from '@element-plus/icons-vue'
import { FormRules } from 'element-plus'
import { billRecordRules } from '@/utils/element/elRules.ts'
import { nextTick, reactive, ref } from 'vue'
import { IBillCategory } from '@/types/bill.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
const formRef = ref()
const rules = reactive<FormRules>(billRecordRules)
const billStore = useBillStore()
const billInfo = reactive({
billTypeList: [] as string[],
billCategoryList: [] as IBillCategory[]
})
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
// 如果是新增账单 默认选择第一个账本
if (billStore.billApiType !== 'UPDATE') {
billStore.billDialog.data.bookName = billStore.billBookList[0].name
}
getBillTypeByBookName(billStore.billDialog.data.bookName)
// 如果是新增账单 默认选择一个类别
if (billStore.billApiType !== 'UPDATE') {
billStore.billDialog.data.type = billInfo.billTypeList[0]
}
getBillCategory()
// 如果是新增账单 默认选择一个分类
if (billStore.billApiType !== 'UPDATE') {
billStore.billDialog.data.category = billInfo.billCategoryList[0].name
billStore.billDialog.data.payAccount = billStore.billPayList[0].name
}
await nextTick(() => {
formRef.value.clearValidate()
})
}
/**
* 获取账单类型名称
* @param type 账单类型
*/
const getBillTypeName = (type: string) => {
return type === 'expensive' ? '支出' : '收入'
}
/**
* 选择账本事件
* @param value 账本名称
*/
const changeBillBookNameEvent = (value: string) => {
getBillTypeByBookName(value)
billStore.billDialog.data.type = billInfo.billTypeList[0]
getBillCategory()
billStore.billDialog.data.category = billInfo.billCategoryList[0].name
}
/**
* 选择账单类别事件
*/
const changeBillCategoryEvent = () => {
getBillCategory()
billStore.billDialog.data.category = billInfo.billCategoryList[0].name
}
/**
* 通过账本获取账单类型
* @param bookName 账本
*/
const getBillTypeByBookName = (bookName: string) => {
billInfo.billTypeList = []
billStore.billCategoryList.forEach((item) => {
if (item.bookName === bookName && !billInfo.billTypeList.includes(item.type)) {
billInfo.billTypeList.push(item.type)
}
})
}
/**
* 获取账单类别
*/
const getBillCategory = () => {
billInfo.billCategoryList = billStore.billCategoryList.filter((value) => {
return value.bookName === billStore.billDialog.data.bookName
&& value.type === billStore.billDialog.data.type
})
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(() => {
billStore.handleBillApi(billStore.billDialog.data)
})
}
const cancelClick = () => {
billStore.billDialog.visible = false
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该账单内容?').then(() => {
billStore.billApiType = 'DELETE'
billStore.handleBillApi(billStore.billDialog.data)
})
}
</script>

View File

@@ -0,0 +1,263 @@
<template>
<el-drawer v-model="billStore.billDrawer" destroy-on-close
:with-header="false" direction="btt" size="95%">
<div class="common-mobile-view bill-detail">
<el-form ref="formRef" :model="billStore.currentBillRecord"
:rules="rules" label-position="top" label-width="80px"
class="form-item bill-form">
<div class="item-list">
<el-select v-model="billStore.currentBillRecord.bookName"
@change="changeBillBookNameEvent" style="width: 100px">
<el-option
v-for="item in billStore.billBookList"
:key="item.id" :label="item.name" :value="item.name"
/>
</el-select>
<el-select v-model="billStore.currentBillRecord.type"
@change="changeBillCategoryEvent" style="width: 100px">
<el-option
v-for="(item, index) in billInfo.billTypeList"
:key="index" :label="getBillTypeName(item)" :value="item"
/>
</el-select>
<el-select v-model="billStore.currentBillRecord.payAccount"
@change="changeBillCategoryEvent" style="width: 100px">
<el-option
v-for="item in billStore.billPayList"
:key="item.id" :label="item.name" :value="item.name"
/>
</el-select>
</div>
<el-radio-group v-model="billStore.currentBillRecord.category" class="bill-category-group">
<el-radio-button v-for="(item, index) in billInfo.billCategoryList"
:value="item.name" :key="index">
<template #default>
<i :class="item.icon"/>
<span style="margin-left: 5px;">{{ item.name }}</span>
</template>
</el-radio-button>
</el-radio-group>
<div class="bill-gap"></div>
<div class="item-list">
<el-date-picker
v-model="billStore.currentBillRecord.date"
type="date" :editable="false" placeholder="请选择账单时间"
value-format="YYYY-MM-DD"/>
<el-input v-model="billStore.currentBillRecord.location" autocomplete="off"
placeholder="请输入账单地点" clearable
show-word-limit maxlength="10">
<template #prefix>
<el-icon class="el-input__icon"><location/></el-icon>
</template>
</el-input>
</div>
<el-input v-model="billStore.currentBillRecord.content" autocomplete="off"
placeholder="请输入账单内容" clearable
show-word-limit maxlength="10">
<template #prefix>
<el-icon class="el-input__icon"><document/></el-icon>
</template>
</el-input>
<el-input v-model="billStore.currentBillRecord.remark" type="textarea"
placeholder="请输入账单备注" clearable
:rows="3" show-word-limit maxlength="50">
</el-input>
<number-input v-model:amount="billInfo.amountStr" class="amount-input" />
</el-form>
</div>
<template #footer>
<div style="flex: auto">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="billStore.billApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { useBillStore } from '@/store/bill.ts'
import { Location, Document } from '@element-plus/icons-vue'
import { ElMessage, FormRules, UploadUserFile } from 'element-plus'
import { billRecordRules } from '@/utils/element/elRules.ts'
import { nextTick, reactive, ref, watch } from 'vue'
import { IBillCategory } from '@/types/bill.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import NumberInput from '@/components/NumberInput.vue'
const formRef = ref()
const rules = reactive<FormRules>(billRecordRules)
const billStore = useBillStore()
const imageFileList = ref<UploadUserFile[]>([])
const billInfo = reactive({
billTypeList: [] as string[],
billCategoryList: [] as IBillCategory[],
amountStr: ''
})
watch(() => billStore.billDrawer, async (value) => {
if (value) {
await initBillRecord()
initImageFileList()
billInfo.amountStr = billStore.currentBillRecord.amount.toString()
}
})
const initBillRecord = async () => {
// 如果是新增账单 默认选择第一个账本
if (billStore.billApiType !== 'UPDATE') {
billStore.currentBillRecord.bookName = billStore.billBookList[0].name
}
getBillTypeByBookName(billStore.currentBillRecord.bookName)
// 如果是新增账单 默认选择一个类别
if (billStore.billApiType !== 'UPDATE') {
billStore.currentBillRecord.type = billInfo.billTypeList[0]
}
getBillCategory()
// 如果是新增账单 默认选择一个分类
if (billStore.billApiType !== 'UPDATE') {
billStore.currentBillRecord.category = billInfo.billCategoryList[0].name
billStore.currentBillRecord.payAccount = billStore.billPayList[0].name
}
await nextTick(() => {
formRef.value.clearValidate()
})
}
const initImageFileList = () => {
imageFileList.value = []
billStore.currentBillRecord.imageList.forEach((image) => {
imageFileList.value.push({
name: 'image',
url: image
})
})
}
/**
* 获取账单类型名称
* @param type 账单类型
*/
const getBillTypeName = (type: string) => {
return type === 'expense' ? '支出' : '收入'
}
/**
* 选择账本事件
* @param value 账本名称
*/
const changeBillBookNameEvent = (value: string) => {
getBillTypeByBookName(value)
billStore.currentBillRecord.type = billInfo.billTypeList[0]
getBillCategory()
billStore.currentBillRecord.category = billInfo.billCategoryList[0].name
}
/**
* 选择账单类别事件
*/
const changeBillCategoryEvent = () => {
getBillCategory()
billStore.currentBillRecord.category = billInfo.billCategoryList[0].name
}
/**
* 通过账本获取账单类型
* @param bookName 账本
*/
const getBillTypeByBookName = (bookName: string) => {
billInfo.billTypeList = []
billStore.billCategoryList.forEach((item) => {
if (item.bookName === bookName && !billInfo.billTypeList.includes(item.type)) {
billInfo.billTypeList.push(item.type)
}
})
}
/**
* 获取账单类别
*/
const getBillCategory = () => {
billInfo.billCategoryList = billStore.billCategoryList.filter((value) => {
return value.bookName === billStore.currentBillRecord.bookName
&& value.type === billStore.currentBillRecord.type
})
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
if (billStore.currentBillRecord.content.trim()) {
formRef.value.validate().then(async () => {
billStore.currentBillRecord.amount = Number(billInfo.amountStr)
await billStore.handleBillApi(billStore.currentBillRecord)
billStore.billDrawer = false
})
} else {
ElMessage.error('请输入账单内容')
}
}
const cancelClick = () => {
billStore.billDrawer = false
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该账单内容?').then(async () => {
billStore.billApiType = 'DELETE'
await billStore.handleBillApi(billStore.currentBillRecord)
billStore.billDrawer = false
})
}
</script>
<style lang="scss">
.bill-detail {
.bill-form {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 10px;
.item-list {
display: flex;
justify-content: space-between;
gap: 5px;
}
.bill-category-group {
display: grid;
grid-template-columns: repeat(4, 1fr);
.el-radio-button {
.el-radio-button__inner {
padding: 8px 10px;
border: none;
}
}
}
.bill-gap {
flex-grow: 1;
}
}
}
</style>

View File

@@ -1,166 +0,0 @@
<template>
<div class="bill-setting">
<div class="button-container">
<el-button type="primary" @click="addNewPayClick">新增账户</el-button>
</div>
<el-table
:data="billStore.billPayList"
border stripe
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
style="width: 100%"
class="card-item">
<el-table-column type="index" align="center" width="50" />
<el-table-column prop="name" label="名称" align="center" width="100"/>
<el-table-column label="图标" align="center" width="80">
<template #default="scope">
<i :class="scope.row.icon" :style="{color: scope.row.color, fontSize: '24px'}"></i>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="editPayClick(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="deletePayClick(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="billInfo.billPayDialog.visible"
:title="billInfo.billPayDialog.title"
center width="80%" @open="openDialogEvent">
<el-form ref="formRef" :model="billInfo.billPayDialog.data"
:rules="rules" label-position="top" label-width="100px">
<el-form-item label="账户名称" prop="name">
<el-input v-model="billInfo.billPayDialog.data.name" autocomplete="off"
placeholder="请输入类型名称"
:maxlength="10" show-word-limit/>
</el-form-item>
<el-form-item label="账户图标" prop="icon">
<el-input v-model="billInfo.billPayDialog.data.icon" autocomplete="off"
placeholder="请输入类型图标"
:maxlength="40" show-word-limit/>
</el-form-item>
<el-form-item label="图标颜色" prop="color">
<el-color-picker v-model="billInfo.billPayDialog.data.color" />
</el-form-item>
<el-form-item label="图标预览">
<i :class="billInfo.billPayDialog.data.icon"
:style="{color: billInfo.billPayDialog.data.color, fontSize: '24px'}"></i>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确认</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { useBillStore } from '@/store/bill.ts'
import { ElMessage, FormRules } from 'element-plus'
import { IBillPay } from '@/types/bill.ts'
import { billPayRules } from '@/utils/element/elRules.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { addBillPayApi, updateBillPayApi } from '@/apis/bill.ts'
import { tableCellStyle, tableHeaderStyle, tableRowStyle } from 'vue3-common/utils/elUtil'
import type { IDialog, IHandleApi } from 'vue3-common/types'
const formRef = ref()
const rules = reactive<FormRules>(billPayRules)
const billStore = useBillStore()
const billInfo = reactive({
setBillPayType: 'ADD' as IHandleApi,
billPayDialog: {
title: '',
visible: false,
data: {}
} as IDialog<IBillPay>
})
onMounted(async () => {
await billStore.queryBillPay()
})
/**
* 点击新增账户按钮
*/
const addNewPayClick = () => {
billInfo.setBillPayType = 'ADD'
billInfo.billPayDialog = {
title: '新增账户',
visible: true,
data: {
name: '',
icon: '',
color: ''
}
}
}
/**
* 点击编辑账户按钮
* @param pay 账户
*/
const editPayClick = async (pay: IBillPay) => {
billInfo.setBillPayType = 'UPDATE'
billInfo.billPayDialog = {
title: '编辑账户',
visible: true,
data: deepCopyObject(pay)
}
}
/**
* 点击删除账户按钮
* @param pay 账户
*/
const deletePayClick = (pay: IBillPay) => {
billInfo.setBillPayType = 'DELETE'
ElMessage.error('暂不支持该功能')
}
const openDialogEvent = () => {
nextTick(() => {
formRef.value.clearValidate()
})
}
const setBillBookApi = async (id?: number) => {
switch (billInfo.setBillPayType) {
case 'ADD':
await addBillPayApi(billInfo.billPayDialog.data)
ElMessage.success('新增账户成功')
break
case 'UPDATE':
await updateBillPayApi(id as number, billInfo.billPayDialog.data)
ElMessage.success('更新账户成功')
break
case 'DELETE':
ElMessage.success('删除账户成功')
break
default:
break
}
await billStore.queryBillPay()
billInfo.billPayDialog.visible = false
}
const cancelClick = () => {
billInfo.billPayDialog.visible = false
}
const confirmClick = async () => {
await formRef.value.validate((valid) => {
if (valid) {
setBillBookApi(billInfo.billPayDialog.data.id)
}
})
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="common-query">
<el-select v-model="billStore.billQueryForm.billBook"
@change="billStore.refreshInfo()" style="width: 120px">
@change="billStore.refreshInfo(isStats)">
<el-option
v-for="(item, index) in billQuery.billBookList"
:key="index" :label="item.label" :value="item.name"
@@ -9,34 +9,32 @@
</el-select>
<el-select v-model="billStore.billDateType"
@change="billStore.refreshInfo()"
style="width: 120px;align-self: center;">
@change="billStore.refreshInfo(isStats)">
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
</el-select>
<el-switch
v-model="billStore.billType"
inline-prompt
active-value="income" inactive-value="expensive"
active-text="收入账单" inactive-text="支出账单"
@change="billStore.refreshInfo()"
style="--el-switch-on-color: #6FBB69; --el-switch-off-color: #CA6C65;"
/>
<el-select v-if="isStats" v-model="billStore.billType"
@change="billStore.refreshInfo(isStats)">
<el-option v-for="(item, index) in billTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
</el-select>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import { computed, onMounted, reactive } from 'vue'
import { useBillStore } from '@/store/bill.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { IBillBook } from '@/types/bill.ts'
import { useRoute } from 'vue-router'
const billStore = useBillStore()
const route = useRoute()
const isStats = computed(() => route.path.includes('chart'))
onMounted(async () => {
await billStore.initBillInfo()
billQuery.billBookList = deepCopyObject(billStore.billBookList)
billQuery.billBookList.push({
color: '',
@@ -56,6 +54,15 @@ const dateTypeOptions = [{
value: 'year'
}]
const billTypeOptions = [{
label: '收入',
value: 'income'
},
{
label: '支出',
value: 'expense'
}]
const billQuery = reactive({
query: '',
billBookList: [] as IBillBook[]

View File

@@ -3,7 +3,7 @@
:key="index" class="rank-item">
<span class="rank-index">{{ index+1 }}.</span>
<span class="rank-content">{{ item.content }}</span>
<span class="rank-amount">{{ item.amount }}</span>
<span class="rank-amount">{{ formatAmount(item.amount) }}</span>
</div>
</template>
@@ -11,6 +11,7 @@
import { useBillStore } from '@/store/bill.ts'
import { getBillSortByAmount } from '@/utils/home/billUtil.ts'
import { computed } from 'vue'
import { formatAmount } from 'vue3-common/utils/numberUtil'
const billStore = useBillStore()

View File

@@ -0,0 +1,34 @@
<template>
<div class="common-query">
<el-form :model="billStore.billSearchForm">
<el-form-item label="账单内容">
<el-input v-model="billStore.billSearchForm.content"
@input="billStore.searchInfo()"
placeholder="请输入账单内容" clearable/>
</el-form-item>
<el-form-item label="账单类别">
<el-input v-model="billStore.billSearchForm.category"
@input="billStore.searchInfo()"
placeholder="请输入账单类别" clearable/>
</el-form-item>
<el-form-item label="账单地址">
<el-input v-model="billStore.billSearchForm.location"
@input="billStore.searchInfo()"
placeholder="请输入账单地址" clearable/>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { useBillStore } from '@/store/bill.ts'
const billStore = useBillStore()
onMounted(async () => {
await billStore.searchInfo()
})
</script>

View File

@@ -2,7 +2,7 @@
<div class="common-statistics">
<stats-card
title="当前支出" color="#CA6C65" unit="元" icon="fa-solid fa-sack-xmark"
:value="formatAmount(billStats.expensive)" />
:value="formatAmount(billStats.expense)" />
<stats-card
title="当前收入" color="#6FBB69" unit="元" icon="fa-solid fa-sack-dollar"
@@ -34,7 +34,7 @@ watch(() => billStore.isRefresh, () => {
const billStats = ref<IBillSummaryStats>({
balance: 0,
expensive: 0,
expense: 0,
income: 0,
rate: 0
})

View File

@@ -1,121 +0,0 @@
<template>
<div class="blog-comment-edit card-item">
<div class="comment-title">
<el-input v-model="blogComment.name"
:maxlength="10" show-word-limit
placeholder="请输入昵称"/>
<el-input v-model="blogComment.website"
:maxlength="50" show-word-limit
placeholder="请输入个人博客网站"/>
</div>
<el-input v-model="blogComment.content"
placeholder="有什么想和我说的呢" :maxlength="500"
:rows="7" show-word-limit type="textarea"
/>
<div class="comment-button">
<el-popover ref="emojiPopoverRef" placement="top" :width="300" trigger="hover">
<template #default>
<div class="emoji-panel">
<EmojiPicker :native="true" @select="onSelectEmoji" />
</div>
</template>
<template #reference>
<el-button circle>
<i class="fa-regular fa-face-smile"></i>
</el-button>
</template>
</el-popover>
<el-button type="primary"
:disabled="blogComment.content.length === 0"
@click="sendCommentClick"
class="send-button">发送</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import EmojiPicker from 'vue3-emoji-picker'
import 'vue3-emoji-picker/css'
import { defineProps, ref, onMounted } from 'vue'
import { useBlogStore } from '@/store/blog.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { IBlogComment } from '@/types/blog.ts'
const blogStore = useBlogStore()
const emojiPopoverRef = ref()
const props = defineProps({
parentId: {
required: true,
type: Number,
default: 0
}
})
const blogComment = ref<IBlogComment>({
id: 0,
blogId: 0,
name: '',
website: '',
content: '',
ipAddress: '',
userAgent: '',
parentId: 0,
isApproved: true,
createTime: ''
})
onMounted(() => {
blogComment.value = deepCopyObject(blogStore.getEmptyBlogComment())
})
const onSelectEmoji = (emoji) => {
if (blogComment.value.content.length <= 498) {
blogComment.value.content += emoji.i
}
emojiPopoverRef.value.hide()
}
const sendCommentClick = async () => {
blogComment.value.parentId = props.parentId
blogComment.value.blogId = blogStore.currentBlogId
if (!blogComment.value.name) {
blogComment.value.name = 'Anonymous'
}
await blogStore.addBlogComment(blogComment.value)
blogComment.value = deepCopyObject(blogStore.getEmptyBlogComment())
blogStore.isShowBlogCommentEdit = false
}
</script>
<style lang="scss">
.blog-comment-edit {
width: 100%;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.comment-title {
display: flex;
justify-content: space-between;
gap: 10px;
}
.comment-button {
width: 100%;
display: flex;
.send-button {
margin-left: auto;
}
}
}
</style>

View File

@@ -1,127 +0,0 @@
<template>
<div class="blog-comment-card">
<el-avatar :src="`https://api.dicebear.com/6.x/adventurer/svg?seed=${props.comment.name}`"
:size="50" alt="未加载"/>
<div class="blog-comment-content">
<div class="basic-info">
<span>{{ props.comment.name }}</span>
<span class="content">{{ getBrowser(props.comment.userAgent) }}</span>
<span class="content">{{ getOsInfo(props.comment.userAgent) }}</span>
</div>
<div class="sub-info">
<span class="content">{{ props.comment.createTime }}</span>
<el-button v-if="!blogComment.isShowEdit"
type="primary" text @click="replyClick">回复</el-button>
<el-button v-else type="primary"
text @click="cancelClick">取消</el-button>
</div>
<p>{{ props.comment.content }}</p>
<blog-comment-edit v-if="blogComment.isShowEdit"
:parent-id="props.comment.id"/>
<div class="reply-list">
<div class="blog-comment-card" v-for="(item, index) in props.replyList" :key="index">
<el-avatar :src="`https://api.dicebear.com/6.x/adventurer/svg?seed=${item.name}`"
:size="50" alt="未加载"/>
<div class="blog-comment-content">
<div class="basic-info">
<span>{{ item.name }}</span>
<span class="content">{{ getBrowser(props.comment.userAgent) }}</span>
<span class="content">{{ getOsInfo(props.comment.userAgent) }}</span>
</div>
<div class="sub-info">
<span class="content">{{ item.createTime }}</span>
</div>
<p>{{ item.content }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import BlogCommentEdit from '@/components/Comment/BlogCommentEdit.vue'
import 'vue3-emoji-picker/css'
import { defineProps, PropType, reactive } from 'vue'
import { IBlogComment } from '@/types/blog.ts'
import { useBlogStore } from '@/store/blog.ts'
import { UAParser } from 'ua-parser-js'
const blogStore = useBlogStore()
const props = defineProps({
comment: {
required: true,
type: Object as PropType<IBlogComment>
},
replyList: {
required: true,
type: Object as PropType<IBlogComment[]>
}
})
const blogComment = reactive({
isShowEdit: false
})
const replyClick = () => {
blogStore.currentBlogComment = blogStore.getEmptyBlogComment()
blogComment.isShowEdit = true
}
const cancelClick = () => {
blogComment.isShowEdit = false
}
const getOsInfo = (userAgent: string) => {
const uaParse = new UAParser(userAgent)
return `${uaParse.getOS().name} ${uaParse.getOS().version}`
}
const getBrowser = (userAgent: string) => {
const uaParse = new UAParser(userAgent)
return `${uaParse.getBrowser().name} ${uaParse.getBrowser().version}`
}
</script>
<style lang="scss">
.blog-comment-card {
width: 100%;
padding: 10px;
display: grid;
grid-template-columns: 60px 1fr;
gap: 5px;
.blog-comment-content {
display: flex;
flex-direction: column;
gap: 5px;
.basic-info {
display: flex;
align-items: center;
gap: 10px;
font-size: small;
}
.sub-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.content {
font-size: small;
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="life-record-card card-item">
<el-image :src="`${props.carItem?.imageUrl}${ThumbImageQuery}`"
fit="contain" alt="暂无图片" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
</div>
</template>
</el-image>
<van-swipe-cell>
<div class="record-info" @click="editClick()">
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.carItem.name }}</span>
</div>
<div class="info-item">
<i class="fa-solid fa-file-lines" style="color: #8e44ad"></i>
{{ props.carItem.description }}
</div>
</div>
<el-tag type="primary" effect="dark">
{{ props.carItem.type }}
</el-tag>
</div>
<div class="bottom-container">
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-gauge" style="color: #3498db"></i>
{{ props.carItem.mileage }} M
</div>
<div class="info-item">
<i class="fa-solid fa-coins" style="color: #27ae60"></i>
{{ props.carItem.cost }}
</div>
<div class="info-item">
<i class="fa-solid fa-location-dot" style="color: #e67e22"></i>
{{ props.carItem.location }}
</div>
</div>
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-calendar" style="color: #26a69a"></i>
{{ props.carItem.date }}
</div>
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { useCarStore } from '@/store/car.ts'
import { defineProps, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
import { ThumbImageQuery } from '@/utils'
import { showImagePreview } from 'vant'
import { ICarEvent } from '@/types/car.ts'
const carStore = useCarStore()
const router = useRouter()
const props = defineProps({
carItem: {
required: true,
type: Object as PropType<ICarEvent>
}
})
const viewImageClick = () => {
showImagePreview({ images: [props.carItem?.imageUrl], closeable: true })
}
const editClick = async () => {
const id = props.carItem?.id
carStore.carEventApiType = 'UPDATE'
carStore.currentCarEvent = props.carItem as ICarEvent
await router.push({ name: 'CarDetailId', params: { id } })
}
const deleteClick = () => {
carStore.carEventApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该事件: ${props.carItem?.description}?`).then(() => {
carStore.handleCarEventApi(props.carItem as ICarEvent)
})
}
</script>
<style lang="scss">
@use "@/styles/home/life.record.module";
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>类别</span>
<el-segmented v-model="carStore.queryInfo.type"
:options="queryTypeList"
@change="carStore.queryCarEventList()"/>
</div>
</div>
</template>
<script setup lang="ts">
import { useCarStore } from '@/store/car.ts'
const carStore = useCarStore()
const queryTypeList = [
{
label: '全部',
value: ''
},
{
label: '保养',
value: '保养'
}, {
label: '事故',
value: '事故'
}, {
label: '违章',
value: '违章'
}
]
</script>

View File

@@ -1,101 +0,0 @@
<template>
<el-button ref="buttonRef" type="primary" :icon="Plus"
circle size="large" :style="style" style="font-size: 1.8rem;"
@mousedown="startDrag" @touchstart="startDrag"/>
</template>
<script setup lang="ts">
import { computed, defineEmits, onMounted, reactive, ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const buttonRef = ref()
const buttonPos = reactive({
x: 0,
y: 0,
width: 0,
height: 0
})
const startPos = reactive({
x: 0,
y: 0
})
const offsetPos = reactive({
x: 0,
y: 0
})
/**
* 最小拖到阈值
*/
const dragThreshold = 5
const emit = defineEmits(['click'])
/**
* 实时更新位置
*/
const style = computed(() => ({
position: 'absolute',
left: `${buttonPos.x}px`,
top: `${buttonPos.y}px`,
touchAction: 'none'
}))
onMounted(() => {
initButtonPosition()
})
const initButtonPosition = () => {
buttonPos.x = window.innerWidth - 50
buttonPos.y = window.innerHeight - 120
buttonPos.width = buttonRef.value.$el.offsetWidth
buttonPos.height = buttonRef.value.$el.offsetHeight
}
const startDrag = (event) => {
event.preventDefault()
const touch = event.touches ? event.touches[0] : event
startPos.x = touch.clientX
startPos.y = touch.clientY
offsetPos.x = touch.clientX - buttonPos.x
offsetPos.y = touch.clientY - buttonPos.y
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
document.addEventListener('touchmove', onDrag, { passive: false })
document.addEventListener('touchend', stopDrag)
}
const onDrag = (event) => {
const touch = event.touches ? event.touches[0] : event
// 计算新位置,并限制在边界内
const newX = touch.clientX - offsetPos.x
const newY = touch.clientY - offsetPos.y
buttonPos.x = Math.max(0, Math.min(newX, window.innerWidth - buttonPos.width))
buttonPos.y = Math.max(40, Math.min(newY, window.innerHeight - buttonPos.height - 70))
}
const stopDrag = (event) => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
const touch = event.changedTouches ? event.changedTouches[0] : event
const deltaX = Math.abs(touch.clientX - startPos.x)
const deltaY = Math.abs(touch.clientY - startPos.y)
// 此时为点击
if (deltaX <= dragThreshold && deltaY <= dragThreshold) {
emit('click', event)
}
}
</script>

View File

@@ -12,13 +12,22 @@
{{ appStore.isDark ? '🌙' : '☀️' }}
</el-button>
</div>
<div class="setting-item">
<span>我的</span>
<el-button circle :icon="UserFilled" type="primary" @click="viewProfileClick"/>
</div>
</div>
</template>
<script setup lang="ts">
import { UserFilled } from '@element-plus/icons-vue'
import { useAppStore } from '@/store/app.ts'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const appStore = useAppStore()
const router = useRouter()
onMounted(() => {
appStore.themeColor = appStore.getThemeColor()
@@ -27,6 +36,11 @@ onMounted(() => {
const changeThemeColorEvent = (value: string) => {
appStore.setThemeColor(value)
}
const viewProfileClick = () => {
router.push('/profile/index')
appStore.isShowSetting = false
}
</script>
<style scoped lang="scss">

View File

@@ -12,9 +12,8 @@
<el-card class="record-card">
<span class="record-content">完成人{{ item.person }}</span>
<el-image v-if="item.imageUrl"
:src="serviceFileRootPath + item.imageUrl"
:preview-src-list="[serviceFileRootPath + item.imageUrl]"
fit="contain" alt="暂无图片" class="record-image"/>
:src="`${item.imageUrl}${ThumbImageQuery}`" fit="cover" alt="暂无图片"
@click="viewImageClick(item)" class="record-image"/>
</el-card>
</el-timeline-item>
</el-timeline>
@@ -25,8 +24,14 @@
<script setup lang="ts">
import { useFoodStore } from '@/store/food.ts'
import { serviceFileRootPath } from '@/utils'
import { ThumbImageQuery } from '@/utils'
import { SvgIcon } from 'vue3-common'
import { IFoodRecord } from '@/types/food.ts'
import { showImagePreview } from 'vant'
const foodStore = useFoodStore()
const viewImageClick = (item: IFoodRecord) => {
showImagePreview({ images: [item.imageUrl], closeable: true })
}
</script>

View File

@@ -12,7 +12,7 @@
<div class="step-body">
<span class="step-content">{{ item.content }}</span>
<img v-if="item.imageUrl"
:src="serviceFileRootPath + item.imageUrl" alt="暂无图片" class="step-image"/>
:src="item.imageUrl" alt="暂无图片" class="step-image"/>
</div>
</div>
</div>
@@ -21,7 +21,6 @@
<script setup lang="ts">
import { useFoodStore } from '@/store/food.ts'
import { serviceFileRootPath } from '@/utils'
import { SvgIcon } from 'vue3-common'
const foodStore = useFoodStore()

View File

@@ -1,39 +1,25 @@
<template>
<div class="life-record-card card-item">
<el-carousel v-if="props.foodItem?.recordList.length > 0"
arrow="always" @change="changeFoodEvent">
<el-carousel-item v-for="(item, index) in getFoodRecord(props.foodItem?.recordList)"
:key="index">
<el-image :src="serviceFileRootPath + item.imageUrl"
fit="contain" alt="暂无成果">
<el-image v-if="props.foodItem?.recordList.length > 0"
:src="`${props.foodItem?.recordList[0].imageUrl}${ThumbImageQuery}`"
fit="contain" alt="暂无成果" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
</div>
</template>
</el-image>
</el-carousel-item>
</el-carousel>
<el-empty v-else description="暂无成果" />
<div class="record-info">
<van-swipe-cell>
<div class="record-info" @click="viewClick()">
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.foodItem.name }}</span>
</div>
</div>
<div class="button-info">
<el-button v-if="!isMobile()"
type="primary" :icon="Edit" circle :size="'small'"
@click="editFoodRecipeClick"/>
<el-button type="primary" :icon="MoreFilled" circle :size="'small'"
@click="viewFoodRecipeClick()" />
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
@click="deleteFoodRecipeClick()" />
</div>
</div>
<div class="bottom-container">
@@ -60,20 +46,26 @@
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
import { useFoodStore } from '@/store/food.ts'
import { defineProps, onMounted, PropType, reactive } from 'vue'
import { IFoodRecipe, IFoodRecord } from '@/types/food.ts'
import { IFoodRecipe } from '@/types/food.ts'
import { useRouter } from 'vue-router'
import { formatDate } from 'vue3-common/utils/dateUtil'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { serviceFileRootPath } from '@/utils'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { showImagePreview } from 'vant'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
const router = useRouter()
const foodStore = useFoodStore()
@@ -94,35 +86,28 @@ onMounted(() => {
foodCardInfo.recommendRate = props.foodItem?.recommendRate as number
})
const getFoodRecord = (recordList: IFoodRecord[]) => {
return recordList.filter((item) => item.imageUrl)
const viewImageClick = () => {
const imageList = props.foodItem?.recordList.map((item) => `${item.imageUrl}${RawImageQuery}`)
showImagePreview({ images: imageList, closeable: true })
}
const editFoodRecipeClick = async () => {
const editClick = async () => {
const id = props.foodItem?.id
foodStore.foodRecipeApiType = 'UPDATE'
await foodStore.queryFoodRecipe(id as number)
foodStore.foodRecipeDialog = {
title: '编辑菜谱',
visible: true,
data: deepCopyObject(foodStore.currentFoodRecipe)
}
await router.push({ name: 'FoodRecipeForm' })
}
const viewFoodRecipeClick = async () => {
const viewClick = async () => {
await router.push({ name: 'FoodDetailId', params: { id: props.foodItem?.id } })
}
const deleteFoodRecipeClick = () => {
const deleteClick = () => {
foodStore.foodRecipeApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => {
foodStore.handleFoodRecipeApi(props.foodItem?.id as number)
foodStore.handleFoodRecipeApi(props.foodItem)
})
}
const changeFoodEvent = (current: number) => {
foodCardInfo.currentFoodIndex = current
}
</script>
<style lang="scss">

View File

@@ -30,8 +30,6 @@ import { useRouter } from 'vue-router'
import { useAppStore } from '@/store/app.ts'
import { formatChineseDate } from 'vue3-common/utils/dateUtil'
import { IFoodRecord } from '@/types/food.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const foodStore = useFoodStore()
const appStore = useAppStore()
@@ -85,18 +83,10 @@ const getFoodDailyByDate = (date: string) => {
*/
const selectFoodRecordClick = async (foodRecord: IFoodRecord) => {
foodStore.foodRecordApiType = 'UPDATE'
if (isMobile()) {
appStore.isShowMobileBack = true
foodStore.currentFoodRecord = foodRecord
appStore.isShowMobileBack = true
await router.push({ name: 'FoodRecordForm' })
} else {
foodStore.foodRecordDialog = {
title: '编辑记录',
visible: true,
data: deepCopyObject(foodRecord)
}
}
}
</script>

View File

@@ -1,57 +0,0 @@
<template>
<div class="food-daily-list">
<el-date-picker v-model="foodStore.queryRecord.year"
type="year" :editable="false" style="width: 120px;"
@change="foodStore.refreshInfo()"/>
<el-empty v-if="foodDailyInfo.dateList.length === 0"/>
<el-scrollbar>
<food-daily-item v-for="(item, index) in foodDailyInfo.dateList"
:key="index" :date="item"/>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import FoodDailyItem from '@/components/Food/FoodDailyItem.vue'
import { reactive, watch } from 'vue'
import { useFoodStore } from '@/store/food.ts'
const foodStore = useFoodStore()
watch(() => foodStore.isRefresh, () => {
updateInfo()
})
const foodDailyInfo = reactive({
dateList: [] as string[]
})
const updateInfo = () => {
const dateList = foodStore.foodRecordList.map((item) => item.date)
foodDailyInfo.dateList = [...new Set(dateList)] as string[]
}
</script>
<style lang="scss">
.food-daily-list {
height: calc(100vh - 120px);
overflow: auto;
display: flex;
flex-direction: column;
gap: 1vh;
.el-date-editor {
align-self: center;
}
.el-scrollbar {
.el-scrollbar__view {
display: flex;
flex-direction: column;
gap: 2vh;
}
}
}
</style>

View File

@@ -1,116 +0,0 @@
<template>
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
center @open="openDialogEvent" class="food-dialog">
<el-form ref="formRef" :model="dialogData.data"
:rules=rules label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="美食名称" prop="name">
<el-input v-model="dialogData.data.name" autocomplete="off"
placeholder="请输入美食名称" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="美食菜系" prop="category">
<el-select v-model="dialogData.data.category">
<el-option v-for="(item, index) in foodCategoryList"
:key="index" :label="item" :value="item"/>
</el-select>
</el-form-item>
<el-form-item label="推荐指数" prop="recommendRate">
<el-rate v-model="dialogData.data.recommendRate"
:colors="foodStore.rateColorList"/>
</el-form-item>
<el-form-item label="食材">
<food-material-form />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="步骤">
<food-step-form />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="dialogData.data.remark" autocomplete="off"
placeholder="请输入备注" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import FoodMaterialForm from '@/components/Food/Form/FoodMaterialForm.vue'
import FoodStepForm from '@/components/Food/Form/FoodStepForm.vue'
import { useFoodStore } from '@/store/food.ts'
import { FormInstance, FormRules } from 'element-plus'
import { computed, nextTick, ref, reactive } from 'vue'
import { foodRules } from '@/utils/element/elRules.ts'
const rules = reactive<FormRules>(foodRules)
const formRef = ref<FormInstance>()
const foodStore = useFoodStore()
const foodCategoryList = ['家常菜', '淮扬菜', '鲁菜', '川菜', '粤菜', '闽菜', '浙菜', '湘菜', '徽菜', '其他']
const dialogData = computed(() => foodStore.foodRecipeDialog)
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value?.validate().then(() => {
foodStore.handleFoodRecipeApi()
})
}
const cancelClick = () => {
dialogData.value.visible = false
}
</script>
<style lang="scss">
.food-dialog {
width: 75%;
.el-dialog__body {
height: 65vh;
.el-button.is-text {
padding: 0;
}
.material-table {
height: 45vh;
}
.food-step-table {
height: 55vh;
}
}
}
</style>

View File

@@ -1,68 +1,39 @@
<template>
<div v-if="isMobile()" class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="foodStore.queryRecipe.category"
:options="queryCategoryList"
@change="queryFoodSegmented"/>
</div>
</div>
<div v-else class="common-query">
<el-select v-model="foodStore.queryDateType"
@change="foodStore.refreshInfo()"
style="width: 120px;align-self: center;">
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
<div class="common-query">
<el-select
v-model="foodStore.queryRecipe.name"
filterable clearable placeholder="请选择菜谱名称"
style="width: 150px;"
@change="changeFoodNameEvent">
<el-option
v-for="(item, index) in foodStore.foodNameList"
:key="index" :label="item" :value="item"
/>
</el-select>
</div>
</template>
<script setup lang="ts">
import { useFoodStore } from '@/store/food.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { onMounted, ref } from 'vue'
import { ICategory } from '@/types'
import { onMounted } from 'vue'
import { searchFoodRecipeApi } from '@/apis/food.ts'
const foodStore = useFoodStore()
const queryCategoryList = ref([] as ICategory[])
onMounted(async () => {
await foodStore.queryFoodCategory()
initCategoryList()
await foodStore.queryFoodNameList()
foodStore.queryRecipe.name = ''
})
const dateTypeOptions = [{
label: '按月选择',
value: 'month'
},
{
label: '按年选择',
value: 'year'
}]
const initCategoryList = () => {
queryCategoryList.value = []
queryCategoryList.value.push({
label: '全部',
value: ''
})
foodStore.foodCategoryList.forEach((item) => {
queryCategoryList.value.push({
label: item,
value: item
})
})
}
const queryFoodSegmented = async () => {
foodStore.pageInfo = {
currentPage: 1,
pageSize: 3,
totalSize: 1
}
const changeFoodNameEvent = async (value: string) => {
if (value) {
const result = await searchFoodRecipeApi(value)
foodStore.foodRecipeList = [result]
} else {
foodStore.pageInfo.currentPage = 1
foodStore.pageInfo.pageSize = 3
foodStore.foodRecipeList = []
await foodStore.queryMobileFoodRecipeList()
}
}
</script>

View File

@@ -1,45 +0,0 @@
<template>
<div class="food-list card-item">
<el-empty v-if="foodStore.foodRecipeList.length === 0" description="暂无美食记录"/>
<div v-else class="list">
<food-card v-for="(item, index) in foodStore.foodRecipeList"
:key="index" :food-item="item"/>
</div>
<el-pagination
v-model:current-page="foodStore.pageInfo.currentPage"
:page-size="foodStore.pageInfo.pageSize"
layout="total, prev, pager, next"
:total="foodStore.pageInfo.totalSize"
@change="changePageEvent"
/>
<el-button v-if="isLargeScreen()"
type="primary" :icon="Plus" circle
@click="addFoodRecipeClick" class="add-new" />
</div>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import FoodCard from '@/components/Food/FoodCard.vue'
import { useFoodStore } from '@/store/food.ts'
import { isLargeScreen } from '@/utils'
const foodStore = useFoodStore()
const addFoodRecipeClick = () => {
foodStore.foodRecipeApiType = 'ADD'
foodStore.foodRecipeDialog = {
title: '新增菜谱',
visible: true,
data: foodStore.getEmptyFoodRecipe()
}
}
const changePageEvent = (value: number) => {
foodStore.pageInfo.currentPage = value
foodStore.queryFoodRecipeList()
}
</script>

View File

@@ -47,7 +47,7 @@ const getAverageCount = () => {
}
})
return foodStore.foodRecordList.length / Object.keys(result).length
return (foodStore.foodRecordList.length / Object.keys(result).length).toFixed(2)
}
}
</script>

View File

@@ -1,13 +1,13 @@
<template>
<el-dialog v-model="foodStore.foodMaterialDialog.visible"
:title="foodStore.foodMaterialDialog.title"
<el-dialog v-model="foodStore.foodMaterialDialogVisible"
:title="foodStore.foodMaterialApiType === 'ADD' ? '新增食材' : '编辑食材'"
center @open="openDialogEvent"
class="food-material-dialog">
<el-form ref="formRef" :model="foodStore.foodMaterialDialog.data"
:rules="rules">
<el-form ref="formRef" :model="foodStore.currentFoodMaterial"
:rules="rules" label-width="auto">
<el-form-item label="类型" prop="type">
<el-select
v-model="foodStore.foodMaterialDialog.data.type"
v-model="foodStore.currentFoodMaterial.type"
placeholder="请选择类型">
<el-option
v-for="(item, index) in foodMaterialTypeList"
@@ -17,14 +17,14 @@
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="foodStore.foodMaterialDialog.data.name" autocomplete="off"
<el-input v-model="foodStore.currentFoodMaterial.name" autocomplete="off"
placeholder="请输入名称" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="分量" prop="amount">
<el-input v-model="foodStore.foodMaterialDialog.data.amount" autocomplete="off"
<el-input v-model="foodStore.currentFoodMaterial.amount" autocomplete="off"
placeholder="请输入分量1斤/2勺/适量)" clearable
show-word-limit maxlength="20">
</el-input>
@@ -68,22 +68,22 @@ const confirmClick = () => {
formRef.value.validate().then(async () => {
if (foodStore.foodMaterialApiType === 'UPDATE') {
const { editFoodMaterialIndex: index } = foodStore
foodStore.foodRecipeDialog.data.materialList[index] = foodStore.foodMaterialDialog.data
foodStore.currentFoodRecipe.materialList[index] = foodStore.currentFoodMaterial
} else {
foodStore.foodRecipeDialog.data.materialList.push(foodStore.foodMaterialDialog.data)
foodStore.currentFoodRecipe.materialList.push(foodStore.currentFoodMaterial)
}
foodStore.foodMaterialDialog.visible = false
foodStore.foodMaterialDialogVisible = false
})
}
const cancelClick = () => {
foodStore.foodMaterialDialog.visible = false
foodStore.foodMaterialDialogVisible = false
}
</script>
<style lang="scss">
.food-material-dialog {
width: 30%;
width: 80%;
.el-dialog__body {
// height: 50vh;

View File

@@ -1,23 +1,24 @@
<template>
<el-table :data="foodStore.foodRecipeDialog.data.materialList"
border stripe class="material-table">
<el-table-column type="index" align="center" width="90">
<template #header>
<div class="material-form">
<div style="align-self: center;">
<el-button type="primary" @click="handleAddClick">新增</el-button>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" align="center" min-width="10%"/>
</div>
<el-table :data="foodStore.currentFoodRecipe.materialList"
border stripe class="material-table">
<el-table-column prop="type" label="类型" align="center" min-width="15%"/>
<el-table-column prop="name" label="名称" align="center" min-width="20%"/>
<el-table-column prop="amount" label="分量" align="center" min-width="20%"/>
<el-table-column label="操作" align="center" width="100">
<template #default="scope">
<el-button type="primary" text
<el-button type="primary" text style="margin: 0 10px 0 0;padding: 0;"
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
<el-button type="danger" text
<el-button type="danger" text style="margin: 0;padding: 0;"
@click="handleDeleteClick(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
@@ -30,27 +31,21 @@ const foodStore = useFoodStore()
const handleAddClick = () => {
foodStore.foodMaterialApiType = 'ADD'
foodStore.foodMaterialDialog = {
title: '新增食材',
visible: true,
data: foodStore.getEmptyFoodMaterial()
}
foodStore.currentFoodMaterial = foodStore.getEmptyFoodMaterial()
foodStore.foodMaterialDialogVisible = true
}
const handleEditClick = (index: number, material: IFoodMaterial) => {
foodStore.editFoodMaterialIndex = index
foodStore.foodMaterialApiType = 'UPDATE'
foodStore.foodMaterialDialog = {
title: '编辑食材',
visible: true,
data: deepCopyObject(material)
}
foodStore.currentFoodMaterial = deepCopyObject(material)
foodStore.foodMaterialDialogVisible = true
}
const handleDeleteClick = (index: number, material: IFoodMaterial) => {
foodStore.foodMaterialApiType = 'DELETE'
commonElMessageBox(`确认删除该食材: ${material.name}?`).then(() => {
foodStore.foodRecipeDialog.data.materialList.splice(index, 1)
foodStore.currentFoodRecipe.materialList.splice(index, 1)
})
}
</script>

View File

@@ -1,107 +0,0 @@
<template>
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
center @open="openDialogEvent" class="food-record-dialog">
<el-form ref="formRef" :model="dialogData.data"
:rules="rules" label-width="80">
<el-form-item label="菜谱名称" prop="name">
<el-select
v-model="dialogData.data.name"
@focus="foodStore.queryFoodNameList()"
:disabled="foodStore.foodRecordApiType === 'UPDATE'"
placeholder="请选择菜谱名称">
<el-option
v-for="(item, index) in foodStore.foodNameList"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="完成时间" prop="date">
<el-date-picker
v-model="dialogData.data.date"
type="date" :editable="false" placeholder="请选择完成时间"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="完成人" prop="person">
<el-select
v-model="dialogData.data.person"
placeholder="请选择完成人">
<el-option
v-for="(item, index) in ['哥哥', '宝宝']"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="图片">
<upload-image v-model="dialogData.data.imageUrl"
:service-url="fileServiceUrl"
:image-limit="1"
:service-file-root-path="serviceFileRootPath"
:image-path="foodStore.foodImagePath"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">保存</el-button>
<el-button v-if="foodStore.foodRecordApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { UploadImage } from 'vue3-common'
import { useFoodStore } from '@/store/food.ts'
import { computed, ref, reactive, nextTick } from 'vue'
import { FormInstance, FormRules } from 'element-plus'
import { foodRecordRules } from '@/utils/element/elRules.ts'
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
const rules = reactive<FormRules>(foodRecordRules)
const formRef = ref<FormInstance>()
const foodStore = useFoodStore()
const dialogData = computed(() => foodStore.foodRecordDialog)
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value?.validate().then(() => {
foodStore.handleFoodRecordApi(foodStore.foodRecordDialog.data)
})
}
const cancelClick = () => {
dialogData.value.visible = false
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该记录?').then(() => {
foodStore.foodRecordApiType = 'DELETE'
foodStore.handleFoodRecordApi(dialogData.value.data)
})
}
</script>
<style lang="scss">
.food-record-dialog {
max-width: 600px;
}
</style>

View File

@@ -1,23 +1,37 @@
<template>
<el-dialog v-model="foodStore.foodStepDialog.visible"
:title="foodStore.foodStepDialog.title"
<el-dialog v-model="foodStore.foodStepDialogVisible"
:title="foodStore.foodStepApiType === 'ADD' ? '新增步骤' : '编辑步骤'"
center @open="openDialogEvent"
class="food-step-dialog">
<el-form ref="formRef" :model="foodStore.foodStepDialog.data"
:rules="rules">
<el-form ref="formRef" :model="foodStore.currentFoodStep"
:rules="rules" label-width="auto">
<el-form-item label="内容" prop="content">
<el-input v-model="foodStore.foodStepDialog.data.content" autocomplete="off"
<el-input v-model="foodStore.currentFoodStep.content" autocomplete="off"
placeholder="请输入内容" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="图片">
<upload-image v-model="foodStore.foodStepDialog.data.imageUrl"
:service-url="fileServiceUrl"
:image-limit="1"
:service-file-root-path="serviceFileRootPath"
:image-path="foodStore.foodImagePath"/>
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:accept="acceptImageTypes"
:limit="1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-success="handleSuccess"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
@@ -31,17 +45,58 @@
</template>
<script lang="ts" setup>
import { UploadImage } from 'vue3-common'
import { useFoodStore } from '@/store/food.ts'
import { ref, reactive, nextTick } from 'vue'
import { FormRules } from 'element-plus'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { foodStepRules } from '@/utils/element/elRules.ts'
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
import { acceptImageTypes } from '@/utils/file.ts'
import { Plus } from '@element-plus/icons-vue'
import { beforeUploadImage, httpRequestFile } from '@/utils/element/elUpload.ts'
const rules = reactive<FormRules>(foodStepRules)
const formRef = ref()
const foodStore = useFoodStore()
const imageFileList = ref<UploadUserFile[]>([])
onMounted(async () => {
initImageFileList()
})
const initImageFileList = () => {
imageFileList.value = []
const { imageUrl } = foodStore.currentFoodStep
if (imageUrl) {
imageFileList.value.push({
name: 'image',
url: imageUrl
})
}
}
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
return beforeUploadImage(rawFile)
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
return await httpRequestFile(foodStore.foodImagePath, options)
}
const handleSuccess = (response: string) => {
foodStore.currentFoodStep.imageUrl = response
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1张图片')
}
const handleRemove: UploadProps['onRemove'] = () => {
foodStore.currentFoodStep.imageUrl = ''
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
/**
* 打开对话框事件
@@ -60,22 +115,22 @@ const confirmClick = () => {
.then(async () => {
if (foodStore.foodStepApiType === 'UPDATE') {
const { editFoodStepIndex: index } = foodStore
foodStore.foodRecipeDialog.data.stepList[index] = foodStore.foodStepDialog.data
foodStore.currentFoodRecipe.stepList[index] = foodStore.currentFoodStep
} else {
foodStore.foodRecipeDialog.data.stepList.push(foodStore.foodStepDialog.data)
foodStore.currentFoodRecipe.stepList.push(foodStore.currentFoodStep)
}
foodStore.foodStepDialog.visible = false
foodStore.foodStepDialogVisible = false
})
}
const cancelClick = () => {
foodStore.foodStepDialog.visible = false
foodStore.foodStepDialogVisible = false
}
</script>
<style lang="scss">
.food-step-dialog {
width: 30%;
width: 80%;
.el-dialog__body {
// height: 50vh;

View File

@@ -1,27 +1,28 @@
<template>
<el-table :data="foodStore.foodRecipeDialog.data.stepList"
border class="food-step-table">
<el-table-column type="index" align="center" width="90">
<template #header>
<div class="step-form">
<div style="align-self: center;">
<el-button type="primary" @click="handleAddClick">新增</el-button>
</template>
</el-table-column>
</div>
<el-table :data="foodStore.currentFoodRecipe.stepList"
border class="food-step-table">
<el-table-column prop="content" label="内容" align="center" min-width="20%" />
<el-table-column prop="imageUrl" label="图片" align="center" min-width="20%">
<template #default="scope">
<el-image v-if="scope.row.imageUrl" :src="serviceFileRootPath + scope.row.imageUrl" />
<el-image v-if="scope.row.imageUrl" :src="scope.row.imageUrl" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="scope">
<el-button type="primary" text
<el-button type="primary" text style="margin: 0 10px 0 0;padding: 0;"
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
<el-button type="danger" text
<el-button type="danger" text style="margin: 0;padding: 0;"
@click="handleDeleteClick(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
@@ -29,35 +30,27 @@ import { useFoodStore } from '@/store/food.ts'
import { IFoodStep } from '@/types/food.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { serviceFileRootPath } from '@/utils'
const foodStore = useFoodStore()
const handleAddClick = () => {
foodStore.foodStepApiType = 'ADD'
foodStore.foodStepImageList = []
foodStore.foodStepDialog = {
title: '新增步骤',
visible: true,
data: foodStore.getEmptyFoodStep()
}
foodStore.currentFoodStep = foodStore.getEmptyFoodStep()
foodStore.foodStepDialogVisible = true
}
const handleEditClick = (index: number, step: IFoodStep) => {
foodStore.editFoodStepIndex = index
foodStore.foodStepApiType = 'UPDATE'
foodStore.foodStepDialog = {
title: '编辑步骤',
visible: true,
data: deepCopyObject(step)
}
foodStore.currentFoodStep = deepCopyObject(step)
foodStore.foodStepDialogVisible = true
}
const handleDeleteClick = (index: number) => {
commonElMessageBox('确认删除该步骤?').then(() => {
foodStore.foodStepApiType = 'DELETE'
foodStore.foodRecipeDialog.data.stepList.splice(index, 1)
foodStore.foodRecipeDialog.data.stepList.forEach((item) => {
foodStore.currentFoodRecipe.stepList.splice(index, 1)
foodStore.currentFoodRecipe.stepList.forEach((item) => {
if (item.sort > index) {
item.sort -= 1
}

View File

@@ -0,0 +1,129 @@
<template>
<div class="bill-chart common-chart"
v-loading="healthStore.isLoading"
element-loading-text="正在加载中...">
<div class="chart-container card-item">
<div class="content">
<span class="title">📊体重趋势</span>
</div>
<div class="chart" ref="weightDailyChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊睡眠时长统计图</span>
</div>
<div class="chart" ref="sleepDailyChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊运动时长统计</span>
</div>
<div class="chart" ref="healthDurationChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊运动类别统计</span>
</div>
<div class="chart" ref="healthCategoryChartRef"/>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
import { useHealthStore } from '@/store/health'
import { useAppStore } from '@/store/app.ts'
import { formatDate } from 'vue3-common/utils/dateUtil'
const healthStore = useHealthStore()
const appStore = useAppStore()
const weightDailyChartRef = ref<HTMLElement>()
const sleepDailyChartRef = ref<HTMLElement>()
const healthDurationChartRef = ref<HTMLElement>()
const healthCategoryChartRef = ref<HTMLElement>()
let weightDailyChart: echarts.ECharts
let sleepDailyChart: echarts.ECharts
let healthDurationChart: echarts.ECharts
let healthCategoryChart: echarts.ECharts
watch(() => healthStore.isRefresh, () => {
updateChart()
})
onMounted(() => {
weightDailyChart = echarts.init(weightDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
sleepDailyChart = echarts.init(sleepDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
healthDurationChart = echarts.init(healthDurationChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
healthCategoryChart = echarts.init(healthCategoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
})
const updateChart = () => {
const MAX_CATEGORY_COUNT = 5
// 体重趋势统计图
const weightRecordList = healthStore.healthRecordList
.filter((item) => item.weight !== 0)
const weightXAxis = weightRecordList.map((item) => {
return formatDate(item.date, 'MM-DD')
}).reverse()
const weightYAxis = weightRecordList.map((item) => item.weight).reverse()
const sortedRecordList = weightRecordList.sort((a, b) => a.weight - b.weight)
weightDailyChart.setOption(lineChartOptions('日期', weightXAxis, '体重(千克)', weightYAxis))
if (sortedRecordList.length > 0) {
weightDailyChart.setOption({
yAxis: {
min: Math.floor(sortedRecordList[0].weight),
max: Math.ceil(sortedRecordList[sortedRecordList.length - 1].weight)
}
})
}
// 睡眠时长统计图
const sleepRecordList = healthStore.healthRecordList
.filter((item) => item.sleepDuration !== null)
.sort((a, b) => (a.date > b.date ? -1 : 1))
const sleepXAxis = sleepRecordList.map((item) => {
return formatDate(item.date, 'MM-DD')
}).reverse()
const sleepYAxis = sleepRecordList.map((item) => item.sleepDuration).reverse() as number[]
sleepDailyChart.setOption(barChartOptions('日期', sleepXAxis, '时间(小时)', sleepYAxis))
// 运动量统计图
const sportRecordList = healthStore.healthRecordList
.filter((item) => item.sportProject !== '')
.sort((a, b) => (a.date > b.date ? -1 : 1))
const durationXAxis = sportRecordList.map((item) => {
return formatDate(item.date, 'MM-DD')
}).reverse()
const durationYAxis = sportRecordList.map((item) => item.sportDuration).reverse()
healthDurationChart.setOption(barChartOptions('日期', durationXAxis, '时间(分钟)', durationYAxis))
// 类别统计图
const healthCategoryList = healthStore.healthRecordList.reduce((acc, item) => {
if (item.sportProject && item.sportDuration > 0) {
const existing = acc.find((project) => project.name === item.sportProject)
if (existing) {
existing.value += item.sportDuration
} else {
acc.push({ name: item.sportProject, value: item.sportDuration })
}
}
return acc
}, [])
healthCategoryChart.setOption(pieChartOptions('名称', '分钟', healthCategoryList.slice(0, MAX_CATEGORY_COUNT)))
}
</script>

View File

@@ -0,0 +1,179 @@
<template>
<el-form :model="healthStore.currentHealth" class="health-form card-item">
<el-form-item>
<template #label>
<i class="fa-regular fa-calendar-check"></i>
<span>日期</span>
</template>
<el-date-picker v-model="healthStore.currentHealth.date"
type="date" :disabled="true"
style="width: 150px;"/>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-person-running"></i>
<span>运动项目</span>
</template>
<el-select v-model="healthStore.currentHealth.sportProject"
placeholder="请选择运行项目" clearable style="width: 150px;">
<el-option v-for="(item, index) in projectList" :key="index"
:label="item" :value="item"/>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-clock"></i>
<span>运动时长</span>
</template>
<el-input-number v-model="healthStore.currentHealth.sportDuration"
:min="0" :max="300" :precision="0"
style="width: 120px;"></el-input-number>
<span style="margin-left: 5px;">分钟</span>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-weight-scale"></i>
<span>体重</span>
</template>
<el-input-number v-model="healthStore.currentHealth.weight"
:min="0" :max="200"
style="width: 120px;"></el-input-number>
<span style="margin-left: 5px;">千克</span>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-moon"></i>
<span>睡眠开始时间</span>
</template>
<el-time-picker
v-model="healthStore.currentHealth.sleepStartTime"
clearable :editable="false"
placeholder="请选择睡眠开始时间"
style="width: 180px"
format="HH:mm"
value-format="HH:mm"
@clear="healthStore.currentHealth.sleepStartTime = ''"
/>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-sun"></i>
<span>睡眠结束时间</span>
</template>
<el-time-picker
v-model="healthStore.currentHealth.sleepEndTime"
clearable :editable="false"
placeholder="请选择睡眠结束时间"
style="width: 180px"
format="HH:mm"
value-format="HH:mm"
@clear="healthStore.currentHealth.sleepEndTime = ''"
/>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-hourglass-start"></i>
<span>睡眠时长</span>
</template>
<span>{{ healthStore.currentHealth.sleepDuration }}</span>
<span style="margin-left: 5px;">小时</span>
</el-form-item>
<el-form-item style="align-self: center;">
<el-button type="primary" @click="onSubmit"
:disabled="isBeforeDate(formatDate(new Date()), healthStore.selectDate)">
确认
</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { useHealthStore } from '@/store/health'
import { formatDate, isBeforeDate } from 'vue3-common/utils/dateUtil'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
const healthStore = useHealthStore()
const projectList = ['羽毛球', '乒乓球', '跑步', '其他']
const onSubmit = async () => {
const { sportProject, sportDuration, weight,
sleepStartTime, sleepEndTime } = healthStore.currentHealth
if (sportProject === '' && sportDuration === 0 && weight === 0
&& sleepStartTime === '' && sleepEndTime === '') {
return
}
if (sportDuration) {
if (!sportProject) {
ElMessage.error('请选择运动项目')
return
}
}
if (sleepStartTime && sleepEndTime) {
healthStore.currentHealth.sleepDuration = calculateSleepDuration(sleepStartTime, sleepEndTime)
}
await healthStore.updateHealthDay()
healthStore.currentHealth = healthStore.getHealthDay(healthStore.selectDate)
}
const calculateSleepDuration = (startTime: string, endTime: string) => {
const start = dayjs(startTime, 'HH:mm')
let end = dayjs(endTime, 'HH:mm')
// 处理跨天
if (end.isSameOrBefore(start)) {
end = end.add(1, 'day')
}
// 计算时间差(小时)
return end.diff(start, 'hour', true)
}
</script>
<style lang="scss">
.health-form {
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
.el-form-item {
justify-content: space-between;
.el-form-item__label {
display: grid;
grid-template-columns: 20px 1fr;
align-items: center;
gap: 5px;
i {
font-size: 1.2rem;
color: #DE3163;
}
}
.el-form-item__content {
flex: none;
}
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="common-statistics">
<stats-card
title="平均体重" color="#CA6C65" unit="千克" icon="fa-solid fa-weight-scale"
:value="formatAmount(healthStats.averageWeight)" />
<stats-card
title="运动时长" color="#6FBB69" unit="分钟" icon="fa-solid fa-person-walking"
:value="healthStats.totalDuration" />
</div>
</template>
<script setup lang="ts">
import StatsCard from '@/components/StatsCard.vue'
import { useHealthStore } from '@/store/health'
import { ref, watch } from 'vue'
import { formatAmount } from 'vue3-common/utils/numberUtil'
import record from '@/views/travel/record.vue'
import { max } from '@popperjs/core/lib/utils/math'
const healthStore = useHealthStore()
watch(() => healthStore.isRefresh, () => {
updateStats()
})
const healthStats = ref({
averageWeight: 0,
totalDuration: 0
})
const updateStats = () => {
const recordList = healthStore.healthRecordList
const totalWeight = recordList.reduce((sum, item) => sum + item.weight, 0)
if (recordList.length > 0) {
healthStats.value.averageWeight = totalWeight / recordList.length
healthStats.value.totalDuration = recordList.reduce((sum, item) => sum + item.sportDuration, 0)
}
}
</script>

View File

@@ -5,8 +5,11 @@
:is-dark="appStore.isDark"
class="journal-calendar card-item"/>
<journal-card v-if="journalStore.currentJournal.id !== 0"
:item="journalStore.currentJournal"/>
<div v-if="journalStore.dateJournalList.length > 0"
class="journal-list">
<journal-card v-for="(item, index) in journalStore.dateJournalList"
:key="index" :item="item"/>
</div>
<el-empty v-else description="今日暂无日记" />
</div>
@@ -35,6 +38,7 @@ const dailyInfo = reactive({
})
const updateInfo = () => {
journalStore.dateJournalList = []
getCalendarMoveDate()
getCalendarAttrDates()
}
@@ -42,7 +46,7 @@ const updateInfo = () => {
const getCalendarMoveDate = () => {
const recordLength = journalStore.journalList.length
if (recordLength > 0) {
journalStore.currentJournal = getJournalDay(formatDate(new Date()))
journalStore.dateJournalList = getJournalDay(formatDate(new Date()))
}
}
@@ -54,7 +58,7 @@ const getCalendarAttrDates = () => {
dates: new Date()
})
const dateList = journalStore.journalList.map((item) => item.date)
const dateList = journalStore.journalList.map((item) => formatDate(item.date))
dailyInfo.attrs.push({
dot: 'red',
dates: [...new Set(dateList)]
@@ -66,12 +70,11 @@ const getCalendarAttrDates = () => {
* @param day 日期
*/
const dateClick = (day: CalendarDay) => {
journalStore.currentJournal = getJournalDay(formatDate(day.date))
journalStore.dateJournalList = getJournalDay(formatDate(day.date))
}
const getJournalDay = (date: string): IJournal => {
return journalStore.journalList.find((item) => formatDate(item.date) === date)
|| journalStore.getEmptyJournal()
const getJournalDay = (date: string): IJournal[] => {
return journalStore.journalList.filter((item) => formatDate(item.date) === date) || []
}
/**
@@ -96,5 +99,12 @@ const changeMonthClick = async (value) => {
.journal-calendar {
width: 100%;
}
.journal-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 1vh;
}
}
</style>

View File

@@ -37,16 +37,15 @@
<!-- 单媒体内容图片优先于视频 -->
<el-image
v-if="props.item?.imageUrl"
:src="serviceFileRootPath + props.item?.imageUrl"
:preview-src-list="[serviceFileRootPath + props.item?.imageUrl]"
fit="contain"
:src="`${props.item?.imageUrl}${ThumbImageQuery}`"
fit="contain" @click="viewImageClick"
class="media-content"/>
<video
v-if="props.item?.videoUrl"
controls
class="media-content">
<source :src="serviceFileRootPath + props.item?.videoUrl" type="video/mp4">
<source :src="props.item?.videoUrl" type="video/mp4">
</video>
<!-- 展开/收起按钮 -->
@@ -71,12 +70,10 @@ import { SvgIcon } from 'vue3-common'
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import { IJournal } from '@/types/journal.ts'
import { formatDate } from 'vue3-common/utils/dateUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { useJournalStore } from '@/store/journal.ts'
import { useAppStore } from '@/store/app.ts'
import { useRouter } from 'vue-router'
import { serviceFileRootPath } from '@/utils'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
import { showImagePreview } from 'vant'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
const props = defineProps({
item: {
@@ -86,8 +83,6 @@ const props = defineProps({
})
const journalStore = useJournalStore()
const appStore = useAppStore()
const router = useRouter()
onMounted(() => {
if (overflowLines.value <= 0) expanded.value = true
@@ -106,23 +101,16 @@ const overflowLines = computed(() => {
const showToggle = computed(() => overflowLines.value > 0)
const toggleExpand = () => expanded.value = !expanded.value
const toggleExpand = () => { expanded.value = !expanded.value }
const viewImageClick = () => {
showImagePreview({ images: [props.item?.imageUrl + RawImageQuery], closeable: true })
}
const editClick = async () => {
const id = props.item?.id
journalStore.journalApiType = 'UPDATE'
journalStore.currentJournal = props.item
if (isMobile()) {
appStore.isShowMobileBack = true
await router.push({ name: 'JournalDetailId', params: { id } })
} else {
journalStore.journalDialog = {
title: '编辑日记',
visible: true,
data: deepCopyObject(journalStore.currentJournal)
}
}
journalStore.currentJournal = deepCopyObject(props.item)
journalStore.journalDrawer = true
}
</script>

View File

@@ -0,0 +1,248 @@
<template>
<el-drawer v-model="journalStore.journalDrawer" destroy-on-close
:with-header="false" direction="btt" size="95%">
<div class="mobile-journal-detail common-mobile-view">
<el-form ref="formRef" :model="journalStore.currentJournal"
:rules="rules" label-position="top" label-width="60px"
class="form-item">
<el-form-item label="日期" prop="date">
<el-date-picker
v-model="journalStore.currentJournal.date"
type="datetime" :editable="false" placeholder="请选择日期"
value-format="YYYY-MM-DD HH:mm:ss"/>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="journalStore.currentJournal.content"
:rows="5" type="textarea"
show-word-limit :maxlength="200"
placeholder="请输入日记内容"
/>
</el-form-item>
<el-form-item label="地址">
<el-input
v-model="journalStore.currentJournal.location"
placeholder="请输入日记地址"
show-word-limit :maxlength="10"
>
<template #prefix>
<el-icon class="el-input__icon">
<location/>
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="分类">
<el-radio-group v-model="journalStore.currentJournal.category"
placeholder="请选择分类" clearable>
<el-radio
v-for="(item, index) in journalCategoryTypeList"
:key="index" :label="item" :value="item"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="天气">
<el-radio-group v-model="journalStore.currentJournal.weather"
placeholder="请选择天气" clearable>
<el-radio
v-for="(item, index) in journalWeatherTypeList"
:key="index" :value="item.value"
>
<template #default>
<svg-icon :name="`weather-${item.value}`"/>
</template>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="心情">
<el-radio-group v-model="journalStore.currentJournal.mood"
placeholder="请选择心情" clearable>
<el-radio
v-for="(item, index) in journalMoodTypeList"
:key="index" :value="item" :label="item"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="图片">
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:accept="acceptImageTypes"
:limit="1"
:before-upload="beforeImageUpload"
:http-request="httpImageRequest"
:on-success="handleImageSuccess"
:on-exceed="handleImageExceed"
:before-remove="beforeRemove"
:on-remove="handleImageRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="视频">
<el-upload
v-model:file-list="videoFileList"
:accept="acceptVideoTypes"
:limit="1"
:before-upload="beforeVideoUpload"
:http-request="httpVideoRequest"
:on-success="handleVideoSuccess"
:on-exceed="handleVideoExceed"
:before-remove="beforeRemove"
:on-remove="handleVideoRemove"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
请上传小于200M的视频
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</div>
<template #footer>
<div style="flex: auto">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="journalStore.journalApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除
</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { Location, Plus } from '@element-plus/icons-vue'
import { useJournalStore } from '@/store/journal.ts'
import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { journalRules } from '@/utils/element/elRules.ts'
import { reactive, ref, watch } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { SvgIcon } from 'vue3-common'
import { acceptImageTypes, acceptVideoTypes } from '@/utils/file.ts'
import { beforeUploadImage, beforeUploadVideo, httpRequestFile } from '@/utils/element/elUpload.ts'
import { journalCategoryTypeList, journalMoodTypeList, journalWeatherTypeList } from '@/utils/home/journalUtil.ts'
const formRef = ref()
const rules = reactive<FormRules>(journalRules)
const journalStore = useJournalStore()
const imageFileList = ref<UploadUserFile[]>([])
const videoFileList = ref<UploadUserFile[]>([])
watch(() => journalStore.journalDrawer, (value) => {
initFileList()
})
const initFileList = () => {
imageFileList.value = []
videoFileList.value = []
const { imageUrl, videoUrl } = journalStore.currentJournal
if (imageUrl) {
imageFileList.value.push({
name: 'image',
url: imageUrl
})
}
if (videoUrl) {
videoFileList.value.push({
name: 'video',
url: videoUrl
})
}
}
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
return beforeUploadImage(rawFile)
}
const httpImageRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
return await httpRequestFile(journalStore.journalImagePath, options)
}
const handleImageSuccess = (response: string) => {
journalStore.currentJournal.imageUrl = response
}
const handleImageExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1张图片')
}
const handleImageRemove: UploadProps['onRemove'] = () => {
journalStore.currentJournal.imageUrl = ''
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
return beforeUploadVideo(rawFile)
}
const httpVideoRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
return await httpRequestFile(journalStore.journalImagePath, options, false)
}
const handleVideoSuccess = (response: string) => {
journalStore.currentJournal.videoUrl = `p${response}`
}
const handleVideoExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1个视频')
}
const handleVideoRemove: UploadProps['onRemove'] = () => {
journalStore.currentJournal.videoUrl = ''
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(async () => {
await journalStore.handleJournalApi(journalStore.currentJournal)
journalStore.journalDrawer = false
})
}
const cancelClick = () => {
journalStore.journalDrawer = false
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该日记内容?').then(async () => {
journalStore.journalApiType = 'DELETE'
await journalStore.handleJournalApi(journalStore.currentJournal)
journalStore.journalDrawer = false
})
}
</script>
<style lang="scss">
.mobile-journal-detail {
.el-radio {
.el-radio__label {
display: flex;
align-items: center;
}
}
}
</style>

View File

@@ -38,8 +38,6 @@
<script setup lang="ts">
import { defineProps, PropType } from 'vue'
import { formatAmount } from 'vue3-common/utils/numberUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { useLedgerStore } from '@/store/ledger.ts'
import { useRouter } from 'vue-router'
import { ILedger, ILegerItem } from '@/types/ledger.ts'

View File

@@ -27,10 +27,6 @@ onMounted(async () => {
const initCategoryList = () => {
queryCategoryList.value = []
if (ledgerStore.ledgerBookList.length > 0) {
ledgerStore.queryInfo.book = ledgerStore.ledgerBookList[0]
}
ledgerStore.ledgerBookList.forEach((item) => {
queryCategoryList.value.push({
label: item,

View File

@@ -0,0 +1,23 @@
<template>
<div v-for="(item, index) in ledgerRankList.slice(0, MAX_RANK_COUNT)"
:key="index" class="rank-item">
<span class="rank-index">{{ index+1 }}.</span>
<span class="rank-content">{{ item.content }}</span>
<span class="rank-amount">{{ formatAmount(item.totalPrice) }}</span>
</div>
</template>
<script setup lang="ts">
import { useLedgerStore } from '@/store/ledger.ts'
import { getLedgerSortByPrice } from '@/utils/home/ledgerUtil.ts'
import { computed } from 'vue'
import { formatAmount } from 'vue3-common/utils/numberUtil'
const ledgerStore = useLedgerStore()
const MAX_RANK_COUNT = 10
const ledgerRankList = computed(() => {
return getLedgerSortByPrice(ledgerStore.ledgerRecordList)
})
</script>

View File

@@ -79,7 +79,6 @@ import { onMounted, reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules.ts'
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { ILoginType } from '@/types/auth.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore()
@@ -98,16 +97,6 @@ onMounted(() => {
const handleLogin = async (type: ILoginType) => {
loginFormRef.value.validate(async (valid: any) => {
if (valid) {
if (type === 'common' && !isMobile()) {
ElMessage.error('目前只支持移动端')
return
}
if (type === 'admin' && isMobile()) {
ElMessage.error('目前只支持PC端')
return
}
if (type === 'common') {
authStore.loginLoading = true
} else {

View File

@@ -0,0 +1,114 @@
<template>
<div class="keyboard">
<div class="display">
{{ amount || '0' }}
</div>
<div class="keys">
<el-button v-for="key in keys" :key="String(key)" class="key" @click="press(key)">
{{ key }}
</el-button>
<el-button class="key delete" @click="removeLast">
<i class="fa-solid fa-delete-left"></i>
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineModel } from 'vue'
const amount = defineModel('amount', { type: String, default: '0' })
const keys = [1, 2, 3, 4, 5, 6, 7, 8, 9, '.', 0] as const
const press = (key: number | '.') => {
const pressValue = amount.value
// 小数点
if (key === '.') {
if (pressValue.includes('.')) return
if (pressValue === '') {
amount.value = '0.'
} else {
amount.value += '.'
}
return
}
const next = pressValue + key
// 限制小数点后两位
if (next.includes('.')) {
const [, decimal] = next.split('.')
if (decimal.length > 2) return
}
if (next.length > 1 && next.startsWith('0') && !next.startsWith('0.')) {
amount.value = next.slice(1)
} else {
amount.value = next
}
}
const removeLast = () => {
amount.value = amount.value.slice(0, -1)
}
</script>
<style lang="scss" scoped>
.keyboard {
min-height: 280px;
width: 100%;
background: #f7f8fa;
border-radius: 16px;
padding: 16px;
box-sizing: border-box;
user-select: none;
display: flex;
flex-direction: column;
justify-content: space-between;
.display {
height: 40px;
line-height: 40px;
padding: 0 14px;
margin-bottom: 12px;
border-radius: 12px;
background: #fff;
font-size: 22px;
text-align: right;
color: #333;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
}
.keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
.key {
margin-left: 0;
height: 40px;
font-size: 20px;
border-radius: 12px;
border: none;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.08),
inset 0 -1px 0 rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: all 0.15s ease;
}
.delete {
color: #ff3b30;
.fa-delete-left {
font-size: 28px;
}
}
}
}
</style>

View File

@@ -1,25 +1,26 @@
<template>
<div class="password-card card-item">
<van-swipe-cell>
<div class="password-card card-item" @click="editClick">
<el-avatar class="avatar-info">{{ passwordItem.title.charAt(0) }}</el-avatar>
<div class="title-info">
<span>{{ passwordItem.title }}</span>
<span>{{ passwordItem.username }}</span>
</div>
</div>
<div class="button-info">
<el-button type="primary" :icon="Edit" circle
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<el-button type="danger" :icon="DeleteFilled" circle
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</div>
</div>
</template>
</van-swipe-cell>
</template>
<script setup lang="ts">
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { usePasswordStore } from '@/store/password.ts'
import { defineProps, onMounted, PropType } from 'vue'
import { defineProps, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
import { IPassword } from '@/types/password.ts'
@@ -34,10 +35,6 @@ const props = defineProps({
}
})
onMounted(() => {
})
const editClick = async () => {
const id = props.passwordItem?.id
passwordStore.passwordApiType = 'UPDATE'
@@ -59,7 +56,7 @@ const deleteClick = () => {
padding: 10px;
display: grid;
grid-template-columns: 40px 1fr 80px;
grid-template-columns: 40px 1fr;
align-items: center;
gap: 10px;

View File

@@ -5,9 +5,8 @@
<script lang="ts" setup>
import { onMounted, watch } from 'vue'
import { Calendar, DateSelectArg, EventClickArg } from '@fullcalendar/core'
import { initCommonCalendar, initMobileCalendar } from '@/utils/home/planUtil.ts'
import { initMobileCalendar } from '@/utils/home/planUtil.ts'
import { usePlanStore } from '@/store/plan.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
let calendar: Calendar | undefined
@@ -27,11 +26,7 @@ watch(() => planStore.isRefresh, () => {
const initCalendar = async () => {
const calendarEl = document.getElementById('planCalendar') as HTMLElement
if (isMobile()) {
calendar = initMobileCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
} else {
calendar = initCommonCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
}
calendar.render()
}
@@ -83,7 +78,7 @@ const eventClickEvent = (eventClick: EventClickArg) => {
</script>
<style lang="scss">
@import "@/styles/home.variable.module";
@use "@/styles/home.variable.module" as variable;
$cell-height: 3.8vh;
$time-slot-width: 70px;
@@ -109,7 +104,7 @@ $month-cell-height: 120px;
}
.fc-button-active {
background-color: $theme-color;
background-color: variable.$theme-color;
color: #FFFFFF;
}
}
@@ -142,7 +137,7 @@ $month-cell-height: 120px;
// 表头区域
.fc-col-header {
color: $theme-color;
color: variable.$theme-color;
height: 50px;
font-size: 1.4rem;
background-color: var(--color-record-card-bg);

View File

@@ -67,11 +67,9 @@ import { defineProps, PropType } from 'vue'
import { AlarmClock, Clock } from '@element-plus/icons-vue'
import { IPlanInfo } from '@/types/plan.ts'
import { getRemainDays, isBeforeToday } from '@/utils/home/planUtil.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { usePlanStore } from '@/store/plan.ts'
import { useRouter } from 'vue-router'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const planStore = usePlanStore()
const router = useRouter()
@@ -85,17 +83,8 @@ const props = defineProps({
const editPlanClick = async () => {
planStore.planApiType = 'UPDATE'
if (isMobile()) {
planStore.currentPlan = props.plan as IPlanInfo
await router.push({ name: 'MobileHomePlanDetailId', params: { id: planStore.currentPlan.id } })
} else {
planStore.planDialog = {
title: '编辑计划',
visible: true,
data: deepCopyObject(props.plan)
}
}
await router.push({ name: 'PlanDetailId', params: { id: planStore.currentPlan.id } })
}
const changeCompleteEvent = (value: boolean) => {

View File

@@ -1,211 +0,0 @@
<template>
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
center @open="openDialogEvent"
class="plan-dialog">
<el-form ref="formRef" :model="dialogData.data"
:rules="rules" label-width="80px" class="plan-form">
<el-form-item label="计划标题" prop="title">
<el-input v-model="dialogData.data.title" autocomplete="off"
placeholder="请输入标题" clearable
show-word-limit maxlength="10">
</el-input>
</el-form-item>
<el-form-item label="计划内容">
<el-input v-model="dialogData.data.content" autocomplete="off"
placeholder="请输入标题" :rows="2" type="textarea" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<div style="display: flex;justify-content: space-between;">
<el-form-item label="开始日期">
<el-date-picker v-model="dialogData.data.date"
type="date" placeholder="请选择日期" :editable="false"
value-format="YYYY-MM-DD" style="width: 150px;"
:disabled-date="disabledDate" @change="changeDateEvent"/>
</el-form-item>
<el-form-item label="开始时间">
<el-time-picker v-model="dialogData.data.startTime"
placeholder="请选择开始时间" :editable="false"
format="HH:mm" value-format="HH:mm" style="width: 150px;"
:disabled="dialogData.data.date === null"
@change="changeStartTimeEvent"/>
</el-form-item>
<el-form-item label="结束时间">
<el-time-picker v-model="dialogData.data.endTime"
placeholder="请选择结束时间" :editable="false"
:disabled="dialogData.data.startTime === null"
format="HH:mm" value-format="HH:mm" style="width: 150px;"/>
</el-form-item>
</div>
<div style="display: flex;gap: 40px;">
<el-form-item label="提醒时间">
<el-date-picker v-model="dialogData.data.alterTime"
type="datetime" placeholder="请选择提醒时间" :editable="false"
format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss"
@change="changeAlterTimeEvent"
:disabled-date="disabledDate" style="width: 200px;"/>
</el-form-item>
<el-form-item label="邮箱地址" prop="email">
<el-input v-model="dialogData.data.email"
placeholder="请输入邮箱地址" clearable
:disabled="dialogData.data.alterTime === null"
style="width: 200px;">
<template #prefix>
<i class="fa-solid fa-envelope"></i>
</template>
</el-input>
</el-form-item>
</div>
<div style="display: flex;justify-content: space-between;">
<el-form-item label="类别">
<el-input v-model="dialogData.data.tag" autocomplete="off"
placeholder="请输入类别" clearable
show-word-limit maxlength="10" style="width: 150px;">
</el-input>
</el-form-item>
<el-form-item label="置顶">
<el-switch v-model="dialogData.data.isTop"
:active-value="1" :inactive-value="0"
@change="changeTopEvent"
/>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="dialogData.data.priority"
placeholder="请选择优先级" style="width: 150px;"
:disabled="!!dialogData.data.isTop">
<el-option v-for="(item, index) in priorityOptionList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="planStore.planApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { planRules } from '@/utils/element/elRules.ts'
import { computed, nextTick, reactive, ref } from 'vue'
import { isBeforeTime } from '@/utils/home/planUtil.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { usePlanStore } from '@/store/plan.ts'
import { useUserStore } from '@/store/user.ts'
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>(planRules)
const planStore = usePlanStore()
const userStore = useUserStore()
const dialogData = computed(() => planStore.planDialog)
const priorityOptionList = [{
value: 0,
label: '高'
},
{
value: 1,
label: '中'
},
{
value: 2,
label: '低'
}]
const disabledDate = (time: Date) => {
return time.getTime() < Date.now() - 8.64e7
}
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
const changeDateEvent = (value: string) => {
if (!value) {
dialogData.value.data.startTime = null
dialogData.value.data.endTime = null
}
}
const changeStartTimeEvent = (value: string) => {
if (!value) {
dialogData.value.data.endTime = null
}
}
const changeAlterTimeEvent = (value: string) => {
if (value) {
dialogData.value.data.email = userStore.userInfo.email
}
}
const changeTopEvent = (value: number) => {
if (value) {
dialogData.value.data.priority = 0
}
}
const validatePlan = (): boolean => {
const { startTime, endTime } = dialogData.value.data
if (startTime && endTime) {
if (!isBeforeTime(startTime, endTime)) {
ElMessage.error('请确认结束时间大于开始时间')
return false
}
}
return true
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value?.validate().then(async () => {
if (validatePlan()) {
await planStore.handlePlanApi(dialogData.value.data)
}
})
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该计划内容?').then(() => {
planStore.planApiType = 'DELETE'
planStore.handlePlanApi(dialogData.value.data)
})
}
const cancelClick = () => {
planStore.planDialog.visible = false
}
</script>
<style lang="scss">
.plan-dialog {
width:40%;
min-width: 800px;
}
</style>

View File

@@ -1,18 +0,0 @@
<template>
<div class="plan-list">
<el-empty v-if="planStore.planList.length === 0" />
<el-scrollbar v-else>
<plan-card v-for="(item, index) in planStore.planList"
:key="index" :plan="item">
</plan-card>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import PlanCard from '@/components/Plan/PlanCard.vue'
import { usePlanStore } from '@/store/plan.ts'
const planStore = usePlanStore()
</script>

View File

@@ -1,18 +1,18 @@
<template>
<div class="common-query">
<el-select v-model="planStore.queryInfo.planType"
@change="planStore.refreshInfo()"
style="width: 120px;align-self: center;">
<el-option v-for="(item, index) in planTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
</el-select>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="planStore.queryInfo.planType"
:options="planTypeOptions"
@change="planStore.refreshInfo()"/>
</div>
<el-select v-model="planStore.queryInfo.orderType"
@change="planStore.refreshInfo()"
style="width: 120px;align-self: center;">
<el-option v-for="(item, index) in orderTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
</el-select>
<div class="query-item">
<span>排序</span>
<el-segmented v-model="planStore.queryInfo.orderType"
:options="orderTypeOptions"
@change="planStore.refreshInfo()"/>
</div>
</div>
</template>
@@ -26,20 +26,20 @@ const planTypeOptions = [{
value: 'all'
},
{
label: '已完成计划',
label: '已完成',
value: 'completed'
},
{
label: '未完成计划',
label: '未完成',
value: 'notCompleted'
}]
const orderTypeOptions = [{
label: '时间排序',
label: '时间',
value: 'date'
},
{
label: '重要性排序',
label: '重要性',
value: 'priority'
}]
</script>

View File

@@ -1,26 +0,0 @@
<template>
<div class="common-statistics">
<stats-card
title="计划总数" color="#CA6C65" unit="个" icon="iconfont icon-richeng"
:value="planStore.planStats.totalCount" />
<stats-card
title="完成总数" color="#6FBB69" unit="个" icon="iconfont icon-wancheng"
:value="planStore.planStats.completedCount" />
<stats-card
title="当前计划" color="#E4BF62" unit="个" icon="iconfont icon-dangqianjihua"
:value="planStore.planStats.currentCount" />
<stats-card
title="过期计划" color="#5470C6" unit="个" icon="iconfont icon-yuqi"
:value="planStore.planStats.remainCount" />
</div>
</template>
<script setup lang="ts">
import StatsCard from '@/components/StatsCard.vue'
import { usePlanStore } from '@/store/plan.ts'
const planStore = usePlanStore()
</script>

View File

@@ -1,8 +1,7 @@
<template>
<div class="life-record-card card-item">
<el-image :src="serviceFileRootPath + props.productItem?.imageUrl"
:preview-src-list="[serviceFileRootPath + props.productItem?.imageUrl]"
fit="contain" alt="暂无成果">
<el-image :src="`${props.productItem?.imageUrl}${ThumbImageQuery}`"
fit="contain" alt="暂无图片" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
@@ -10,7 +9,8 @@
</template>
</el-image>
<div class="record-info">
<van-swipe-cell>
<div class="record-info" @click="editClick()">
<div class="top-container">
<div class="info-list">
<div class="info-item">
@@ -23,29 +23,21 @@
</div>
</div>
<div class="button-info">
<el-button type="primary" :icon="Edit" circle :size="'small'"
@click="editClick"/>
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
@click="deleteClick()" />
</div>
<el-tag v-if="props.productItem.expiryDate" type="danger" effect="dark">
{{ props.productItem.expiryDate }}
</el-tag>
</div>
<div class="bottom-container">
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-clock" style="color: #6A5ACD"></i>
{{ props.productItem.totalDay }}
{{ getTotalDay(props.productItem) }}
</div>
<div class="info-item">
<i class="fa-solid fa-chart-line" style="color: #FFA500"></i>
{{ props.productItem.averagePrice }}/
</div>
<div class="info-item">
<i class="fa-solid fa-location-dot"></i>
{{ props.productItem.channel }}
{{ (props.productItem.price / getTotalDay(props.productItem)).toFixed(2) }}/
</div>
</div>
<div class="info-list">
@@ -60,20 +52,28 @@
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { useProductStore } from '@/store/product.ts'
import { defineProps, onMounted, PropType } from 'vue'
import { defineProps, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { IProduct } from '@/types/product.ts'
import { useRouter } from 'vue-router'
import { serviceFileRootPath } from '@/utils'
import { ThumbImageQuery } from '@/utils'
import { showImagePreview } from 'vant'
import { formatDate, getDateDifference } from 'vue3-common/utils/dateUtil'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
const productStore = useProductStore()
const router = useRouter()
const props = defineProps({
productItem: {
@@ -82,16 +82,14 @@ const props = defineProps({
}
})
onMounted(() => {
})
const viewImageClick = () => {
showImagePreview({ images: [props.productItem?.imageUrl], closeable: true })
}
const editClick = async () => {
const id = props.productItem?.id
productStore.productApiType = 'UPDATE'
productStore.currentProduct = props.productItem as IProduct
await router.push({ name: 'ProductDetailId', params: { id } })
productStore.currentProduct = deepCopyObject(props.productItem) as IProduct
productStore.productDrawer = true
}
const deleteClick = () => {
@@ -100,6 +98,14 @@ const deleteClick = () => {
productStore.handleProductApi(props.productItem as IProduct)
})
}
const getTotalDay = (product: IProduct) => {
if (product.expiryDate) {
return getDateDifference(product.purchaseDate, product.expiryDate, 'days')
} else {
return getDateDifference(product.purchaseDate, formatDate(new Date()), 'days')
}
}
</script>
<style lang="scss">

View File

@@ -0,0 +1,168 @@
<template>
<el-drawer v-model="productStore.productDrawer" destroy-on-close
:with-header="false" direction="btt" size="95%">
<div class="common-mobile-view">
<el-form ref="formRef" :model="productStore.currentProduct"
:rules="rules" label-position="top" label-width="80px"
class="form-item">
<el-form-item label="物品名称" prop="name">
<el-input v-model="productStore.currentProduct.name" autocomplete="off"
placeholder="请输入物品名称" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="物品价格" prop="price">
<el-input
v-model="productStore.currentProduct.price"
placeholder="请输入物品价格" clearable
:formatter="(value: string) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="(value: string) => value.replace(/\$\s?|(,*)/g, '')"
/>
</el-form-item>
<el-form-item label="购买日期" prop="purchaseDate">
<el-date-picker
v-model="productStore.currentProduct.purchaseDate"
type="date" :editable="false" placeholder="请选择购买日期"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="购买人" prop="purchaser">
<el-select v-model="productStore.currentProduct.purchaser"
placeholder="请选择购买人" clearable>
<el-option
v-for="(item, index) in ['哥哥', '宝宝']"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="过期日期" prop="expiryDate">
<el-date-picker
v-model="productStore.currentProduct.expiryDate"
type="date" :editable="false" placeholder="请选择过期日期"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="物品分类" prop="category">
<el-select v-model="productStore.currentProduct.category"
placeholder="请选择物品分类" clearable
:allow-create="true" :filterable="true">
<el-option
v-for="(item, index) in productStore.categoryList"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="购买渠道" prop="channel">
<el-input v-model="productStore.currentProduct.channel" autocomplete="off"
placeholder="请输入购买渠道" clearable
show-word-limit maxlength="10">
</el-input>
</el-form-item>
<el-form-item label="物品图片">
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:accept="acceptImageTypes"
:limit="1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-success="handleSuccess"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</div>
<template #footer>
<div style="flex: auto">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { useProductStore } from '@/store/product.ts'
import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { productRules } from '@/utils/element/elRules.ts'
import { reactive, ref, watch } from 'vue'
import { acceptImageTypes } from '@/utils/file.ts'
import { Plus } from '@element-plus/icons-vue'
import { beforeUploadImage, httpRequestFile } from '@/utils/element/elUpload.ts'
const formRef = ref()
const rules = reactive<FormRules>(productRules)
const productStore = useProductStore()
const imageFileList = ref<UploadUserFile[]>([])
watch(() => productStore.productDrawer, (value) => {
if (value) {
initImageFileList()
}
})
const initImageFileList = () => {
imageFileList.value = []
const { imageUrl } = productStore.currentProduct
if (imageUrl) {
imageFileList.value.push({
name: 'image',
url: imageUrl
})
}
}
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
return beforeUploadImage(rawFile)
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
return await httpRequestFile(productStore.productImagePath, options)
}
const handleSuccess = (response: string) => {
productStore.currentProduct.imageUrl = response
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1张图片')
}
const handleRemove: UploadProps['onRemove'] = () => {
productStore.currentProduct.imageUrl = ''
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(async () => {
await productStore.handleProductApi(productStore.currentProduct)
productStore.productDrawer = false
})
}
const cancelClick = () => {
productStore.productDrawer = false
}
</script>

View File

@@ -35,7 +35,7 @@ const queryOrderList = [
label: '购买日期',
value: 'date'
}, {
label: '日均价格',
label: '价格',
value: 'price'
}
]

View File

@@ -12,24 +12,15 @@
<el-card class="record-card">
<span class="record-content">旅行人{{ item.persons.join('-') }}</span>
<el-image v-for="(image, index) in item.imageList" :key="index"
:src="serviceFileRootPath + image"
:preview-src-list="[serviceFileRootPath + image]"
fit="contain" alt="暂无图片" class="record-image"/>
:src="`${image}${ThumbImageQuery}`"
fit="cover" alt="暂无图片" class="record-image"
@click="viewImageClick(item, index)"/>
<video v-for="(video, index) in item.videoList"
:key="index" controls style="width: 60%;min-height: 200px;">
<source :src="serviceFileRootPath + video" type="video/mp4">
:key="index" controls style="width: 100%;">
<source :src="video" type="video/mp4">
您的浏览器不支持 HTML5 视频
</video>
<!-- <el-carousel v-if="item.imageList.length > 0"-->
<!-- arrow="always">-->
<!-- <el-carousel-item v-for="(item, index) in item.imageList" :key="index">-->
<!-- -->
<!-- </el-carousel-item>-->
<!-- </el-carousel>-->
<!-- <el-empty v-else description="暂无记录"/>-->
</el-card>
</el-timeline-item>
</el-timeline>
@@ -41,7 +32,13 @@
<script setup lang="ts">
import { SvgIcon } from 'vue3-common'
import { useTravelStore } from '@/store/travel.ts'
import { serviceFileRootPath } from '@/utils'
import { showImagePreview } from 'vant'
import { ITravelRecord } from '@/types/travel.ts'
import { ThumbImageQuery } from '@/utils'
const travelStore = useTravelStore()
const viewImageClick = (record: ITravelRecord, index: number) => {
showImagePreview({ images: record.imageList, closeable: true, startPosition: index })
}
</script>

View File

@@ -1,106 +0,0 @@
<template>
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
center @open="openDialogEvent" class="travel-record-dialog">
<el-form ref="formRef" :model="dialogData.data"
:rules="rules" label-width="80">
<el-form-item label="景点名称" prop="name">
<el-select
v-model="travelStore.travelRecordDialog.data.name"
@focus="travelStore.queryTravelSpotNameList()"
:disabled="travelStore.travelRecordApiType === 'UPDATE'"
placeholder="请选择景点名称">
<el-option
v-for="(item, index) in travelStore.travelSpotNameList"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="旅行日期" prop="date">
<el-date-picker
v-model="dialogData.data.date"
type="date" placeholder="请选择日期"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="旅行人员" prop="persons">
<multi-input :data="dialogData.data.persons"
:size="5"/>
</el-form-item>
<el-form-item label="图片">
<upload-image v-model="dialogData.data.imageList"
:service-url="fileServiceUrl"
:image-limit="3"
:service-file-root-path="serviceFileRootPath"
:image-path="travelStore.travelImagePath"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">保存</el-button>
<el-button v-if="travelStore.travelRecordApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { UploadImage } from 'vue3-common'
import { useTravelStore } from '@/store/travel.ts'
import { ref, reactive, computed, nextTick } from 'vue'
import { FormRules } from 'element-plus'
import { travelRecordRules } from '@/utils/element/elRules.ts'
import { MultiInput } from 'vue3-common'
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
const rules = reactive<FormRules>(travelRecordRules)
const formRef = ref()
const travelStore = useTravelStore()
const dialogData = computed(() => travelStore.travelRecordDialog)
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(() => {
travelStore.handleTravelRecordApi(dialogData.value.data)
})
}
const cancelClick = () => {
dialogData.value.visible = false
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该记录?').then(() => {
travelStore.travelRecordApiType = 'DELETE'
travelStore.handleTravelRecordApi(dialogData.value.data)
})
}
</script>
<style lang="scss">
.travel-record-dialog {
width: 40%;
.el-dialog__body {
height: 45vh;
}
}
</style>

View File

@@ -1,37 +1,25 @@
<template>
<div class="life-record-card card-item">
<el-carousel v-if="props.travelItem?.recordList.length > 0"
arrow="always" @change="changeTravelEvent">
<el-carousel-item v-for="(item, index) in props.travelItem?.recordList" :key="index">
<el-image :src="serviceFileRootPath + item.imageList[0]"
fit="contain" alt="暂无图片">
<el-image v-if="props.travelItem?.recordList.length > 0"
:src="`${props.travelItem?.recordList[0].imageList[0]}${ThumbImageQuery}`"
fit="contain" alt="暂无成果" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
</div>
</template>
</el-image>
</el-carousel-item>
</el-carousel>
<el-empty v-else description="暂无记录"/>
<div class="record-info">
<van-swipe-cell>
<div class="record-info" @click="viewClick">
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.travelItem.name }}</span>
</div>
</div>
<div class="button-info">
<el-button type="primary" :icon="Edit" circle :size="'small'"
@click="editClick"/>
<el-button type="primary" :icon="MoreFilled" circle :size="'small'"
@click="viewClick()"/>
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
@click="deleteClick()"/>
</div>
</div>
<div class="bottom-container">
@@ -58,11 +46,18 @@
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
import { useTravelStore } from '@/store/travel.ts'
import { useAppStore } from '@/store/app.ts'
import { defineProps, PropType, reactive } from 'vue'
@@ -70,8 +65,9 @@ import { useRouter } from 'vue-router'
import { formatDate } from 'vue3-common/utils/dateUtil'
import { ITravelSpot } from '@/types/travel.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { serviceFileRootPath } from '@/utils'
import { parsePerson } from '@/utils/home/travelUtil.ts'
import { showImagePreview } from 'vant'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
const router = useRouter()
const travelStore = useTravelStore()
@@ -88,6 +84,13 @@ const props = defineProps({
}
})
const viewImageClick = () => {
const imageList = props.travelItem.recordList.flatMap((record) => {
return record.imageList.map((item) => `${item}${RawImageQuery}`)
})
showImagePreview({ images: imageList, closeable: true })
}
const editClick = async () => {
const id = props.travelItem?.id
travelStore.travelSpotApiType = 'UPDATE'
@@ -108,10 +111,6 @@ const deleteClick = () => {
travelStore.handleTravelSpotApi(props.travelItem)
})
}
const changeTravelEvent = (current: number) => {
cardInfo.currentIndex = current
}
</script>
<style lang="scss">

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="isMobile()" class="mobile-common-query card-item">
<div class="mobile-common-query card-item">
<div class="query-item">
<span>城市</span>
<el-segmented v-model="travelStore.querySpot.category"
@@ -7,20 +7,10 @@
@change="queryTravelSegmented"/>
</div>
</div>
<div v-else class="common-query">
<el-select v-model="travelStore.queryDateType"
@change="travelStore.refreshInfo()"
style="width: 120px;align-self: center;">
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
:label="item.label" :value="item.value"/>
</el-select>
</div>
</template>
<script setup lang="ts">
import { useTravelStore } from '@/store/travel.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ref, onMounted } from 'vue'
import { ICategory } from '@/types'
@@ -28,15 +18,6 @@ const travelStore = useTravelStore()
const queryCategoryList = ref([] as ICategory[])
const dateTypeOptions = [{
label: '按月选择',
value: 'month'
},
{
label: '按年选择',
value: 'year'
}]
onMounted(async () => {
await travelStore.queryTravelCategory()
initCategoryList()

View File

@@ -15,8 +15,6 @@
<script setup lang="ts">
import { PropType, defineProps, computed } from 'vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { useKpiStore } from '@/store/kpi.ts'
import { useAppStore } from '@/store/app.ts'
import { useRouter } from 'vue-router'
@@ -42,16 +40,8 @@ const editClick = async (kpi: IKpi) => {
kpiStore.kpiApiType = 'UPDATE'
kpiStore.currentKpi = kpi
if (isMobile()) {
appStore.isShowMobileBack = true
await router.push({ name: 'KpiDetailId', params: { id } })
} else {
kpiStore.kpiDialog = {
title: '编辑绩效',
visible: true,
data: deepCopyObject(kpiStore.currentKpi)
}
}
}
</script>

View File

@@ -1,10 +1,5 @@
<template>
<div class="tab-bar">
<router-link to="/home" @click="routerClick()" class="item">
<i class="fa-solid fa-house"></i>
<span>主页</span>
</router-link>
<router-link v-if="path.includes('bill')" to="/bill/data"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
@@ -15,10 +10,10 @@
<i class="fa-solid fa-chart-pie"></i>
<span>统计</span>
</router-link>
<router-link v-if="path.includes('bill')" to="/bill/setting"
<router-link v-if="path.includes('bill')" to="/bill/agent"
@click="routerClick()" class="item">
<i class="fa-solid fa-gear"></i>
<span>设置</span>
<i class="fa-solid fa-magnifying-glass"></i>
<span>分析</span>
</router-link>
<router-link v-if="path.includes('food')" to="/food/recipe"
@@ -26,6 +21,11 @@
<i class="fa-solid fa-utensils"></i>
<span>菜谱</span>
</router-link>
<router-link v-if="path.includes('food')" to="/food/timeline"
@click="routerClick()" class="item">
<i class="fa-solid fa-timeline"></i>
<span>时间轴</span>
</router-link>
<router-link v-if="path.includes('food')" to="/food/record"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
@@ -43,6 +43,17 @@
<span>记录</span>
</router-link>
<router-link v-if="path.includes('health')" to="/health/calendar"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
<span>记录</span>
</router-link>
<router-link v-if="path.includes('health')" to="/health/chart"
@click="routerClick()" class="item">
<i class="fa-solid fa-chart-pie"></i>
<span>统计</span>
</router-link>
<router-link v-if="path.includes('plan')" to="/plan/calendar"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
@@ -70,6 +81,11 @@
<i class="fa-solid fa-calendar-days"></i>
<span>记录</span>
</router-link>
<router-link v-if="path.includes('period')" to="/period/status"
@click="routerClick()" class="item">
<i class="fa-solid fa-chart-pie"></i>
<span>统计</span>
</router-link>
<router-link v-if="path.includes('period')" to="/period/setting"
@click="routerClick()" class="item">
<i class="fa-solid fa-gear"></i>
@@ -82,6 +98,12 @@
<span>清单</span>
</router-link>
<router-link v-if="path.includes('car')" to="/car/list"
@click="routerClick()" class="item">
<i class="fa-solid fa-list-ul"></i>
<span>清单</span>
</router-link>
<router-link v-if="path.includes('journal')" to="/journal/record"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
@@ -110,6 +132,11 @@
<i class="fa-solid fa-list-ul"></i>
<span>清单</span>
</router-link>
<router-link v-if="path.includes('ledger')" to="/ledger/stats"
@click="routerClick()" class="item">
<i class="fa-solid fa-chart-pie"></i>
<span>统计</span>
</router-link>
<router-link v-if="path.includes('password')" to="/password/list"
@click="routerClick()" class="item">
@@ -117,6 +144,12 @@
<span>清单</span>
</router-link>
<router-link v-if="path.includes('award')" to="/award/list"
@click="routerClick()" class="item">
<i class="fa-solid fa-list-ul"></i>
<span>清单</span>
</router-link>
<router-link v-if="path.includes('server')" to="/server/monitor"
@click="routerClick()" class="item">
<i class="fa-solid fa-desktop"></i>
@@ -143,12 +176,6 @@
<i class="fa-solid fa-box-archive"></i>
<span>归档</span>
</router-link>
<router-link to="/profile/index"
@click="routerClick()" class="item">
<i class="fa-solid fa-user"></i>
<span>我的</span>
</router-link>
</div>
</template>

View File

@@ -5,7 +5,7 @@
type="info" :icon="Back" size="small"
circle color="#FFFFFF"
@click="goBackClick" />
<div v-else class="logo-container">
<div v-else class="logo-container" @click="goHomeClick">
<img :src="getAssetsImageFile('logo/logo.png')" alt="暂无图片" class="logo"/>
<span>{{ appStore.appName }}</span>
</div>
@@ -44,6 +44,10 @@ const router = useRouter()
const title = computed(() => router.currentRoute.value.meta.title)
const goHomeClick = () => {
router.push('/home')
}
const goBackClick = () => {
appStore.isShowMobileBack = false
history.back()

View File

@@ -1,16 +1,22 @@
<template>
<section class="layout-mobile-home">
<van-config-provider :theme="appStore.isDark ? 'dark' : 'light'">
<section class="layout-mobile-home"
:class="{ 'hide-tab-bar': appStore.isShowMobileBack }">
<title-bar />
<router-view />
<tab-bar />
<tab-bar v-if="!appStore.isShowMobileBack"/>
</section>
</van-config-provider>
</template>
<script lang="ts" setup>
import TitleBar from '@/layout/TitleBar.vue'
import TabBar from '@/layout/TabBar.vue'
import { useAppStore } from '@/store/app.ts'
const appStore = useAppStore()
</script>
<style lang="scss">

View File

@@ -25,6 +25,9 @@ import 'animate.css'
// 注册svg-icon
import 'virtual:svg-icons-register'
// vant图片预览样式
import 'vant/es/image-preview/style'
// 创建Vue3实例
const app = createApp(App)
// 使用路由

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,230 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
const billMock: MockMethod[] = [
{
url: '/home-service/bill/pay',
method: 'get',
response: () => {
return [{
id: 602111394902085,
name: '支付宝',
icon: 'fa-brands fa-alipay',
color: '#2220B2'
},
{
id: 602130528899141,
name: '招商银行',
icon: 'fa-solid fa-money-check',
color: '#3E5BFF'
}]
}
},
{
url: '/home-service/bill/book',
method: 'get',
response: () => {
return [{
id: 601082142339141,
name: '日常账本',
icon: 'fa-solid fa-book',
color: '#1E29FF'
},
{
id: 601083198201925,
name: '汽车账本',
icon: 'fa-solid fa-car',
color: '#2E8B57'
}]
}
},
{
url: '/home-service/bill/category',
method: 'get',
response: () => {
return [{
bookName: '日常账本',
color: '#FF1ED6',
icon: 'fa-brands fa-shopify',
id: 601082333016133,
name: '购物',
type: 'expensive'
},
{
bookName: '日常账本',
color: '#3C1EFF',
icon: 'fa-solid fa-utensils',
id: 601082911907909,
name: '餐饮',
type: 'expensive'
},
{
bookName: '日常账本',
color: '#FF1ED6',
icon: 'fa-brands fa-shopify',
id: 601082333016133,
name: '购物',
type: 'expensive'
},
{
bookName: '日常账本',
color: '#FF3C1E',
icon: 'fa-solid fa-sack-dollar',
id: 601422191939653,
name: '工资',
type: 'income'
},
{
bookName: '汽车账本',
color: '#1ED2FF',
icon: 'fa-solid fa-car',
id: 601083391709253,
name: '汽车保养',
type: 'expensive'
}]
}
},
{
url: '/home-service/bill/summary',
method: 'get',
response: () => {
return [{
amount: 213.32,
bookName: '日常账本',
category: '购物',
content: '买东西',
date: '2024-11-11',
id: 611071966380101,
location: '埃斯顿',
payAccount: '支付宝',
remark: '',
type: 'expensive'
},
{
amount: 32.22,
bookName: '日常账本',
category: '餐饮',
content: '肯德基套餐',
date: '2024-10-17',
id: 601083046211653,
location: '肯德基',
payAccount: '招商银行',
remark: '',
type: 'expensive'
},
{
amount: 213.32,
bookName: '日常账本',
category: '购物',
content: '买了一件衣服',
date: '2024-10-17',
id: 601082570399813,
location: '江宁奥特莱斯',
payAccount: '招商银行',
remark: '',
type: 'expensive'
},
{
amount: 55555,
bookName: '日常账本',
category: '工资',
content: '工资收入',
date: '2024-10-17',
id: 601422418636869,
location: '埃斯顿',
payAccount: '招商银行',
remark: '',
type: 'income'
},
{
amount: 2125.23,
bookName: '日常账本',
category: '餐饮',
content: '聚餐',
date: '2024-10-16',
id: 601884663570501,
location: '江宁',
payAccount: '支付宝',
remark: '',
type: 'expensive'
},
{
amount: 532.22,
bookName: '日常账本',
category: '汽车保养',
content: '汽车保养',
date: '2024-10-16',
id: 601083666997317,
location: '溧水4S店',
payAccount: '支付宝',
remark: '',
type: 'expensive'
},
{
amount: 122.32,
bookName: '日常账本',
category: '餐饮',
content: '老表聚餐',
date: '2024-09-12',
id: 601876599046213,
location: '江宁',
payAccount: '招商银行',
remark: '',
type: 'expensive'
}]
}
},
{
url: '/bill/stats/year',
method: 'get',
response: () => {
return [{
name: '2024-10',
value: 2902.9
},
{
name: '2024-09',
value: 122.32
},
{
name: '2024-11',
value: 213.32
}]
}
},
{
url: '/bill/stats/month',
method: 'get',
response: () => {
return []
}
},
{
url: '/bill/stats/book',
method: 'get',
response: () => {
return [{
name: '日常账本',
value: 3238.54
}]
}
},
{
url: '/bill/stats/category',
method: 'get',
response: () => {
return [{
name: '购物',
value: 426.55
},
{
name: '餐饮',
value: 2279.77
},
{
name: '汽车保养',
value: 532.22
}]
}
}]
export default billMock

View File

@@ -1,159 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
const blogMock: MockMethod[] = [
{
url: '/blog-service/blog/stats',
method: 'get',
response: () => {
return {
blogCount: 10,
categoryCount: 3
}
}
},
{
url: '/blog-service/blog/latest',
method: 'get',
response: () => {
return [{
id: 591259203186757,
title: '一篇标题很长的博客嘿嘿嘿哦哦哦哦哦哦哦'
},
{
id: 591837462601797,
title: '锵锵锵'
},
{
id: 591137032622149,
title: '台风'
},
{
id: 591317392101445,
title: '一篇新的2336'
},
{
id: 591317151170629,
title: '新的一篇文章233500'
}]
}
},
{
url: '/blog-service/blog/category',
method: 'get',
response: () => {
return [{
count: 7,
name: '学习'
},
{
count: 3,
name: '工作'
}]
}
},
{
url: '/blog-service/blog/page',
method: 'get',
response: () => {
return {
current: 1,
pages: 2,
records: [
{
category: '学习',
content: null,
createTime: '2024-09-18 10:52:50',
id: 591837462601797,
isGreat: true,
readDuration: 1.27,
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白月下匆匆的步履停顿凝望清点心中如帛的情丝。\n月透着初秋的韵致想念已悄然坠临无语凝噎的痛楚在心间扩散丰盈的思念在秋夜沉迷抒这夜的情愁',
title: '锵锵锵',
topValue: 3,
updateTime: '2024-09-18 10:57:20',
visitCount: 19,
wordCount: 761
},
{
category: '学习',
content: null,
createTime: '2024-09-16 11:22:47',
id: 591837462601797,
isGreat: true,
readDuration: 1.27,
summary: '# 台风12312321312312',
title: '锵锵锵',
topValue: 3,
updateTime: '2024-09-17 00:16:31',
visitCount: 19,
wordCount: 761
},
{
category: '学习',
content: null,
createTime: '2024-09-18 10:52:50',
id: 591837462601797,
isGreat: true,
readDuration: 1.27,
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白月下匆匆的步履停顿凝望清点心中如帛的情丝。\n月透着初秋的韵致想念已悄然坠临无语凝噎的痛楚在心间扩散丰盈的思念在秋夜沉迷抒这夜的情愁',
title: '锵锵锵',
topValue: 3,
updateTime: '2024-09-18 10:57:20',
visitCount: 19,
wordCount: 761
},
{
category: '学习',
content: null,
createTime: '2024-09-18 10:52:50',
id: 591837462601797,
isGreat: true,
readDuration: 1.27,
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白月下匆匆的步履停顿凝望清点心中如帛的情丝。\n月透着初秋的韵致想念已悄然坠临无语凝噎的痛楚在心间扩散丰盈的思念在秋夜沉迷抒这夜的情愁',
title: '锵锵锵',
topValue: 3,
updateTime: '2024-09-18 10:57:20',
visitCount: 19,
wordCount: 761
},
{
category: '学习',
content: null,
createTime: '2024-09-18 10:52:50',
id: 591837462601797,
isGreat: true,
readDuration: 1.27,
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白月下匆匆的步履停顿凝望清点心中如帛的情丝。\n月透着初秋的韵致想念已悄然坠临无语凝噎的痛楚在心间扩散丰盈的思念在秋夜沉迷抒这夜的情愁',
title: '锵锵锵',
topValue: 3,
updateTime: '2024-09-18 10:57:20',
visitCount: 19,
wordCount: 761
}
],
size: 5,
total: 10
}
}
},
{
url: '/blog-service/blog/content/591837462601797',
method: 'get',
response: () => {
return {
category: '学习',
content: '# 台风12312321312312',
createTime: '2024-09-16 11:22:47',
id: 591137032622149,
isGreat: true,
readDuration: 0.03,
summary: '# 台风12312321312312',
title: '台风',
topValue: 3,
updateTime: '2024-09-17 00:16:31',
visitCount: 1,
wordCount: 17
}
}
}]
export default blogMock

View File

@@ -1,117 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
const budgetMock: MockMethod[] = [
{
url: '/home-service/budget/category',
method: 'get',
response: () => {
return ['结婚预算']
}
},
{
url: '/home-service/budget',
method: 'get',
response: () => {
return [{
actual: 26000,
budget: 23123,
category: '结婚预算',
content: '10.2午宴',
id: 611115630714949,
payTime: null,
project: '婚宴',
remake: '5桌',
vendor: '世纪缘酒店'
},
{
actual: 680,
budget: 700,
category: '结婚预算',
content: '看结婚日子',
id: 611116171505733,
payTime: null,
project: '吉日',
remake: '',
vendor: '算命先生'
},
{
actual: 0,
budget: 0,
category: '结婚预算',
content: '化妆师',
id: 611404639944773,
payTime: null,
project: '婚庆',
remake: '',
vendor: ''
},
{
actual: 0,
budget: 0,
category: '结婚预算',
content: '主持人',
id: 611404716798021,
payTime: null,
project: '婚庆',
remake: '',
vendor: ''
},
{
actual: 10000,
budget: 10000,
category: '结婚预算',
content: '婚车',
id: 611404772474949,
payTime: null,
project: '婚庆',
remake: '',
vendor: ''
}]
}
},
{
url: '/home-service/budget/stats/rank',
method: 'get',
response: () => {
return [{
name: '10.2午宴',
value: 26000
},
{
name: '婚车',
value: 10000
},
{
name: '看结婚日子',
value: 680
},
{
name: '化妆师',
value: 0
},
{
name: '主持人',
value: 0
}]
}
},
{
url: '/home-service/budget/stats/project',
method: 'get',
response: () => {
return [{
name: '婚宴',
value: 26000
},
{
name: '吉日',
value: 680
},
{
name: '婚庆',
value: 10000
}]
}
}]
export default budgetMock

View File

@@ -1,166 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
const foodMock: MockMethod[] = [
{
url: '/home-service/food/origin',
method: 'get',
response: () => {
return ['中餐']
}
},
{
url: '/home-service/food/summary/page',
method: 'get',
response: () => {
return {
current: 1,
pages: 1,
records: [
{
id: 614660173221957,
name: '鱼头汤',
recommendRate: 5,
workList: [
{
finishBy: '哥哥',
finishTime: '2024-11-13',
id: 614673962934342,
imageUrl: 'files\\food\\image2.png'
},
{
finishBy: '哥哥',
finishTime: '2024-11-20',
id: 614673962934341,
imageUrl: 'files\\food\\image1.png'
}
]
},
{
id: 614665900060741,
name: '红烧肉',
recommendRate: 3,
workList: []
},
{
id: 614668663291973,
name: '红烧排骨',
recommendRate: 5,
workList: []
}
],
size: 6,
total: 3
}
}
},
{
url: '/home-service/food/614660173221957',
method: 'get',
response: () => {
return {
attention: '',
category: '淮扬菜',
cuisine: '清炖',
id: 614660173221957,
materialList: [
{
amount: '一个鱼头',
id: 614673962901573,
name: '胖头鱼',
type: '主料'
},
{
amount: '一块',
id: 614673962901574,
name: '豆腐',
type: '辅料'
},
{
amount: '1小块',
id: 614673962901575,
name: '生姜',
type: '调料'
},
{
amount: '2勺',
id: 614673962901576,
name: '生抽',
type: '调料'
}
],
name: '鱼头汤',
nutritiveValue: '',
origin: '中餐',
recommendRate: 5,
remark: null,
stepList: [{
content: '先把鱼头剁开',
id: 614673962917957,
imageUrl: '',
sort: 0
},
{
content: '洗鱼',
id: 614673962917958,
imageUrl: '',
sort: 1
},
{
content: '倒水放砂锅里煮',
id: 614673962917959,
imageUrl: '',
sort: 2
},
{
content: '放豆腐',
id: 614673962917960,
imageUrl: '',
sort: 3
},
{
content: '放盐',
id: 614673962917961,
imageUrl: '',
sort: 4
}],
workList: [
{
finishBy: '哥哥',
finishTime: '2024-11-20',
id: 614673962934341,
imageUrl: 'files\\food\\image1.png'
}
]
}
}
},
{
url: '/home-service/food/stats/category',
method: 'get',
response: () => {
return [{
name: '淮扬菜',
value: 3
},
{
name: '鲁菜',
value: 2
}]
}
},
{
url: '/home-service/food/stats/cuisine',
method: 'get',
response: () => {
return [{
name: '清炖',
value: 3
},
{
name: '红烧',
value: 2
}]
}
}]
export default foodMock

View File

@@ -1,9 +0,0 @@
import BillMock from './bill'
import BudgetMock from './budget'
import FoodMock from './food'
export default [
...BillMock,
...BudgetMock,
...FoodMock
]

View File

@@ -32,7 +32,8 @@ export const useAnniversaryStore = defineStore('anniversary', {
title: '',
data: {}
} as IDialog<IAnniversary>,
isRefresh: false
isRefresh: false,
isLoading: false
}),
actions: {
async queryAnniversaryList() {

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { setCookie, getCookie, getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { useDark, useToggle } from '@vueuse/core'
import { calcDarken, calcLighten } from '@/utils/element/elColor.ts'
export const useAppStore = defineStore('ipp', {
state: () => ({
@@ -89,7 +90,17 @@ export const useAppStore = defineStore('ipp', {
return getLocalStorage('theme-color') as string || '#009FA8'
},
setThemeColor(value: string) {
document.documentElement.style.setProperty('--el-color-primary', value)
const root = document.documentElement
root.style.setProperty('--el-color-primary', value)
root.style.setProperty('--el-color-primary-light-3', calcLighten(value, 30))
root.style.setProperty('--el-color-primary-light-5', calcLighten(value, 50))
root.style.setProperty('--el-color-primary-light-7', calcLighten(value, 70))
root.style.setProperty('--el-color-primary-light-8', calcLighten(value, 80))
root.style.setProperty('--el-color-primary-light-9', calcLighten(value, 90))
root.style.setProperty('--el-color-primary-dark-2', calcDarken(value, 20))
root.style.setProperty('--van-primary-color', value)
setLocalStorage('theme-color', value)
}
}

70
src/store/award.ts Normal file
View File

@@ -0,0 +1,70 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { ImagePathEnum } from '@/types'
import { IAward, IAwardQuery } from '@/types/award.ts'
import { addAwardApi, deleteAwardApi, queryAwardListApi, updateAwardApi } from '@/apis/award.ts'
export const useAwardStore = defineStore('award', {
state: () => ({
awardList: [] as IAward[],
queryInfo: {
type: ''
} as IAwardQuery,
awardApiType: 'ADD' as IHandleApi,
currentAward: {
name: '',
type: '',
owner: '',
date: '',
imageList: [] as string[]
} as IAward,
awardImagePath: ImagePathEnum.AWARD,
pageInfo: {
currentPage: 1,
pageSize: 3,
totalSize: 1
},
isScrollEnd: false,
isRefresh: false
}),
actions: {
async queryAwardList() {
this.awardList = await queryAwardListApi(this.queryInfo)
},
async handleAwardApi(award: IAward) {
switch (this.awardApiType) {
case 'ADD':
await addAwardApi(award)
ElMessage.success('新增成功')
break
case 'UPDATE':
await updateAwardApi(award.id as number, award)
ElMessage.success('更新成功')
break
case 'DELETE':
await deleteAwardApi(award.id as number)
ElMessage.success('删除成功')
break
default:
break
}
},
async refreshInfo() {
await this.queryAwardList()
this.isRefresh = !this.isRefresh
},
getEmptyAward(): IAward {
return {
id: 0,
date: '',
name: '',
owner: '',
type: '',
location: '',
imageList: [],
remark: ''
}
}
}
})

View File

@@ -8,13 +8,12 @@ import {
queryBillBookApi,
queryBillCategoryApi, queryBillPayApi,
queryBillRecordApi,
queryBillStatsApi, queryBillSummaryApi, updateBillRecordApi
queryBillStatsApi, queryBillSummaryApi, searchBillSummaryApi, updateBillRecordApi
} from '@/apis/bill'
import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
import { formatDate, getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
import { IDateType, ImagePathEnum } from '@/types'
import { IDialog, IStatsChart, IHandleApi } from 'vue3-common/types'
import { IStatsChart, IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { isMobile } from 'vue3-common/utils/layoutUtil'
export const useBillStore = defineStore('bill', {
state: () => ({
@@ -35,7 +34,7 @@ export const useBillStore = defineStore('bill', {
billPayList: [] as IBillPay[],
billBookList: [] as IBillBook[],
billCategoryList: [] as IBillCategory[],
billType: 'expensive' as IBillType,
billType: 'expense' as IBillType,
billDateType: 'month' as IDateType,
billDateList: [] as string[],
billImagePath: ImagePathEnum.BILL,
@@ -44,15 +43,16 @@ export const useBillStore = defineStore('bill', {
billYear: getCurrentYear(),
billMonth: getCurrentMonth()
},
billSearchForm: {
content: '',
category: '',
location: ''
},
billDateStatsList: [] as IStatsChart[],
billBookStatsList: [] as IStatsChart[],
billCategoryStatsList: [] as IStatsChart[],
billApiType: 'ADD' as IHandleApi,
billDialog: {
title: '',
visible: false,
data: {}
} as IDialog<IBillRecord>,
billDrawer: false,
isRefresh: false,
isLoading: false
}),
@@ -119,31 +119,39 @@ export const useBillStore = defineStore('bill', {
default:
break
}
this.billDialog.visible = false
if (!isMobile()) {
await this.refreshInfo()
}
},
async refreshInfo() {
this.isLoading = true
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
getBillDateList() {
const { billYear, billMonth } = this.billQueryForm
const date = this.billDateType === 'year' ? billYear : billMonth
this.billDateList = getStartEndDate(date, this.billDateType)
},
async refreshInfo(isStats = false) {
this.isLoading = true
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
this.getBillDateList()
await this.queryBillSummary(bookName)
if (isStats) {
await this.queryBillStats(bookName)
}
this.isRefresh = !this.isRefresh
this.isLoading = false
},
async searchInfo() {
this.isLoading = true
this.billRecordList = await searchBillSummaryApi(this.billSearchForm)
this.isLoading = false
},
getEmptyBillRecord(): IBillRecord {
return {
amount: 0,
bookName: '',
category: '',
content: '',
date: '',
date: formatDate(new Date()),
location: '',
payAccount: '',
remark: '',

View File

@@ -4,12 +4,10 @@ import {
IBlogLatest, IBlogNestedComment, IBlogStats, INewBlog, IQueryBlog
} from '@/types/blog'
import {
addBlogApi, addBlogCommentApi, deleteBlogApi,
queryBlogApi, queryBlogByConditionApi, queryBlogByIdApi,
queryBlogCategoryApi, queryBlogCommentApi, queryBlogStatsApi, queryLatestBlogApi, updateBlogApi
queryBlogCategoryApi, queryBlogCommentApi, queryBlogStatsApi, queryLatestBlogApi
} from '@/apis/blog.ts'
import { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { ImagePathEnum } from '@/types'
import { flattenToTwoLevel } from '@/utils/blog/blogUtil.ts'
@@ -90,10 +88,6 @@ export const useBlogStore = defineStore('blog', {
async queryBlogComment(blogId: number) {
this.blogCommentList = await queryBlogCommentApi(blogId)
},
async addBlogComment(blogComment: IBlogComment) {
await addBlogCommentApi(blogComment)
await this.queryBlogById(blogComment.blogId)
},
async queryBlogCategory() {
this.blogCategoryList = await queryBlogCategoryApi()
},
@@ -103,25 +97,6 @@ export const useBlogStore = defineStore('blog', {
async queryLatestBlog() {
this.latestBlogList = await queryLatestBlogApi()
},
async handleBlogApi(id?: number) {
switch (this.blogApiType) {
case 'ADD':
await addBlogApi(this.newBlog)
ElMessage.success('新增博客成功')
break
case 'UPDATE':
await updateBlogApi(this.newBlog.id as number, this.newBlog)
ElMessage.success('更新博客成功')
break
case 'DELETE':
await deleteBlogApi(id as number)
ElMessage.success('删除博客成功')
break
default:
break
}
await this.refreshInfo()
},
async refreshInfo() {
this.pageInfo.currentPage = 1
await this.queryBlog()

Some files were not shown because too many files have changed in this diff Show More