Compare commits

..

31 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
156 changed files with 4199 additions and 3500 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 _; server_name _;
client_max_body_size 500M;
location /sweet-hut/ { location /sweet-hut/ {
alias /usr/share/nginx/html/; alias /usr/share/nginx/html/;
try_files $uri $uri/ /sweet-hut/index.html; try_files $uri $uri/ /sweet-hut/index.html;
@@ -25,6 +27,14 @@ server {
proxy_pass http://host.docker.internal:8082/; 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 / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
} }

View File

@@ -2,6 +2,7 @@
"name": "sweet-hut-ui", "name": "sweet-hut-ui",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"build": "vite build", "build": "vite build",
@@ -23,7 +24,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
"element-china-area-data": "^6.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", "file-icons-vue": "^1.3.4",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"lunar-javascript": "^1.7.2", "lunar-javascript": "^1.7.2",
@@ -31,6 +32,7 @@
"pinia": "^2.3.1", "pinia": "^2.3.1",
"ua-parser-js": "^2.0.3", "ua-parser-js": "^2.0.3",
"v-calendar": "^3.1.2", "v-calendar": "^3.1.2",
"vant": "^4.9.22",
"vue": "3.5.12", "vue": "3.5.12",
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common", "vue3-common": "git+https://gitee.com/Cxx0822/vue3-common",
"vue3-emoji-picker": "^1.1.8", "vue3-emoji-picker": "^1.1.8",
@@ -38,12 +40,14 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.19.86", "@types/node": "^18.19.86",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"sass": "^1.86.3", "sass": "^1.86.3",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"unplugin-vue-components": "^32.1.0",
"vfonts": "^0.0.3", "vfonts": "^0.0.3",
"vite": "^5.2.0", "vite": "^5.2.0",
"vite-plugin-mock": "^3.0.2", "vite-plugin-mock": "^3.0.2",

294
pnpm-lock.yaml generated
View File

@@ -54,7 +54,7 @@ importers:
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0 version: 6.1.0
element-plus: element-plus:
specifier: ~2.9.7 specifier: ^2.9.7
version: 2.9.7(vue@3.5.12(typescript@5.8.3)) version: 2.9.7(vue@3.5.12(typescript@5.8.3))
file-icons-vue: file-icons-vue:
specifier: ^1.3.4 specifier: ^1.3.4
@@ -77,6 +77,9 @@ importers:
v-calendar: v-calendar:
specifier: ^3.1.2 specifier: ^3.1.2
version: 3.1.2(@popperjs/core@2.11.8)(vue@3.5.12(typescript@5.8.3)) 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: vue:
specifier: 3.5.12 specifier: 3.5.12
version: 3.5.12(typescript@5.8.3) version: 3.5.12(typescript@5.8.3)
@@ -93,6 +96,9 @@ importers:
'@types/node': '@types/node':
specifier: ^18.19.86 specifier: ^18.19.86
version: 18.19.86 version: 18.19.86
'@vant/auto-import-resolver':
specifier: ^1.3.0
version: 1.3.0
'@vitejs/plugin-vue': '@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)) 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))
@@ -111,6 +117,9 @@ importers:
typescript: typescript:
specifier: ^5.8.3 specifier: ^5.8.3
version: 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: vfonts:
specifier: ^0.0.3 specifier: ^0.0.3
version: 0.0.3 version: 0.0.3
@@ -468,6 +477,9 @@ packages:
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2': '@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -479,6 +491,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
@@ -975,6 +990,17 @@ packages:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 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': '@vavt/util@2.1.0':
resolution: {integrity: sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==} resolution: {integrity: sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==}
@@ -1043,6 +1069,9 @@ packages:
'@vue/shared@3.5.13': '@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vue/shared@3.5.28':
resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==}
'@vueuse/core@13.1.0': '@vueuse/core@13.1.0':
resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==} resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==}
peerDependencies: peerDependencies:
@@ -1075,8 +1104,13 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.14.1: acorn@8.15.0:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} 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'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
@@ -1316,6 +1350,10 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
class-utils@0.3.6: class-utils@0.3.6:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1375,6 +1413,12 @@ packages:
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 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: connect@3.7.0:
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@@ -1811,6 +1855,9 @@ packages:
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
exsolve@1.1.1:
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
extend-shallow@2.0.1: extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1842,6 +1889,15 @@ packages:
fastq@1.19.1: fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 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: file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
@@ -2339,6 +2395,10 @@ packages:
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
engines: {node: '>=4.0.0'} 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: lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@@ -2370,6 +2430,9 @@ packages:
magic-string@0.30.17: magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 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: map-cache@0.2.2:
resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2457,6 +2520,9 @@ packages:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
mockjs@1.1.0: mockjs@1.1.0:
resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==} resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==}
hasBin: true hasBin: true
@@ -2552,6 +2618,10 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'} 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: on-finished@2.3.0:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -2610,6 +2680,9 @@ packages:
pathe@0.2.0: pathe@0.2.0:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2617,6 +2690,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
pify@2.3.0: pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2634,6 +2711,12 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'} 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: posix-character-classes@0.1.1:
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2735,6 +2818,9 @@ packages:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-string@4.3.4: query-string@4.3.4:
resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==} resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2757,6 +2843,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3059,6 +3149,10 @@ packages:
thenify@3.3.1: thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 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: to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3143,6 +3237,9 @@ packages:
uc.micro@2.1.0: uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.4:
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3162,6 +3259,53 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} 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: unset-value@1.0.0:
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3199,6 +3343,11 @@ packages:
v8-compile-cache@2.4.0: v8-compile-cache@2.4.0:
resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
vant@4.9.22:
resolution: {integrity: sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==}
peerDependencies:
vue: ^3.0.0
vary@1.1.2: vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -3331,6 +3480,9 @@ packages:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'} engines: {node: '>=12'}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
whatwg-mimetype@3.0.0: whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3814,12 +3966,19 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25 '@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/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {} '@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
@@ -4368,6 +4527,14 @@ snapshots:
'@typescript-eslint/types': 5.62.0 '@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3 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': {} '@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))': '@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))':
@@ -4473,6 +4640,8 @@ snapshots:
'@vue/shared@3.5.13': {} '@vue/shared@3.5.13': {}
'@vue/shared@3.5.28': {}
'@vueuse/core@13.1.0(vue@3.5.12(typescript@5.8.3))': '@vueuse/core@13.1.0(vue@3.5.12(typescript@5.8.3))':
dependencies: dependencies:
'@types/web-bluetooth': 0.0.21 '@types/web-bluetooth': 0.0.21
@@ -4509,13 +4678,15 @@ snapshots:
dependencies: dependencies:
acorn: 7.4.1 acorn: 7.4.1
acorn-jsx@5.3.2(acorn@8.14.1): acorn-jsx@5.3.2(acorn@8.15.0):
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.15.0
acorn@7.4.1: {} acorn@7.4.1: {}
acorn@8.14.1: {} acorn@8.15.0: {}
acorn@8.18.0: {}
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
@@ -4794,6 +4965,10 @@ snapshots:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
chokidar@5.0.0:
dependencies:
readdirp: 5.0.0
class-utils@0.3.6: class-utils@0.3.6:
dependencies: dependencies:
arr-union: 3.1.0 arr-union: 3.1.0
@@ -4853,6 +5028,10 @@ snapshots:
concat-map@0.0.1: {} concat-map@0.0.1: {}
confbox@0.1.8: {}
confbox@0.2.4: {}
connect@3.7.0: connect@3.7.0:
dependencies: dependencies:
debug: 2.6.9 debug: 2.6.9
@@ -5378,8 +5557,8 @@ snapshots:
espree@9.6.1: espree@9.6.1:
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.14.1) acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
esprima@4.0.1: {} esprima@4.0.1: {}
@@ -5414,6 +5593,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
exsolve@1.1.1: {}
extend-shallow@2.0.1: extend-shallow@2.0.1:
dependencies: dependencies:
is-extendable: 0.1.1 is-extendable: 0.1.1
@@ -5456,6 +5637,10 @@ snapshots:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
file-entry-cache@6.0.1: file-entry-cache@6.0.1:
dependencies: dependencies:
flat-cache: 3.2.0 flat-cache: 3.2.0
@@ -5957,6 +6142,12 @@ snapshots:
emojis-list: 3.0.0 emojis-list: 3.0.0
json5: 1.0.2 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-es@4.17.21: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
@@ -5981,6 +6172,10 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
map-cache@0.2.2: {} map-cache@0.2.2: {}
map-visit@1.0.0: map-visit@1.0.0:
@@ -6083,6 +6278,13 @@ snapshots:
for-in: 1.0.2 for-in: 1.0.2
is-extendable: 1.0.1 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: mockjs@1.1.0:
dependencies: dependencies:
commander: 13.1.0 commander: 13.1.0
@@ -6187,6 +6389,8 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
obug@2.1.4: {}
on-finished@2.3.0: on-finished@2.3.0:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
@@ -6239,10 +6443,14 @@ snapshots:
pathe@0.2.0: {} pathe@0.2.0: {}
pathe@2.0.3: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}
picomatch@4.0.5: {}
pify@2.3.0: {} pify@2.3.0: {}
pinia@2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3)): pinia@2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3)):
@@ -6257,6 +6465,18 @@ snapshots:
pirates@4.0.7: {} 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: {} posix-character-classes@0.1.1: {}
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
@@ -6348,6 +6568,8 @@ snapshots:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
quansync@0.2.11: {}
query-string@4.3.4: query-string@4.3.4:
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1
@@ -6371,6 +6593,8 @@ snapshots:
readdirp@4.1.2: {} readdirp@4.1.2: {}
readdirp@5.0.0: {}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@@ -6776,6 +7000,11 @@ snapshots:
dependencies: dependencies:
any-promise: 1.3.0 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: to-object-path@0.3.0:
dependencies: dependencies:
kind-of: 3.2.2 kind-of: 3.2.2
@@ -6884,6 +7113,8 @@ snapshots:
uc.micro@2.1.0: {} uc.micro@2.1.0: {}
ufo@1.6.4: {}
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -6904,6 +7135,44 @@ snapshots:
unpipe@1.0.0: {} 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: unset-value@1.0.0:
dependencies: dependencies:
has-value: 0.3.1 has-value: 0.3.1
@@ -6940,6 +7209,13 @@ snapshots:
v8-compile-cache@2.4.0: {} 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: {} vary@1.1.2: {}
vfonts@0.0.3: {} vfonts@0.0.3: {}
@@ -7076,6 +7352,8 @@ snapshots:
webidl-conversions@7.0.0: {} webidl-conversions@7.0.0: {}
webpack-virtual-modules@0.6.2: {}
whatwg-mimetype@3.0.0: {} whatwg-mimetype@3.0.0: {}
which-boxed-primitive@1.1.1: 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
})

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 { cloudService } from './index'
import { IFileInfo } from '@/types/file' import { IFileInfo } from '@/types/file'
import type { AxiosProgressEvent } from 'axios'
export const uploadFileApi = (form: FormData, path: string, md5: string): Promise<any> => export const uploadFileApi = (form: FormData, path: string, md5: string): Promise<any> =>
cloudService({ cloudService({
@@ -12,6 +13,19 @@ export const uploadFileApi = (form: FormData, path: string, md5: string): Promis
data: form 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[]> => export const getFolderInfoApi = (folderName: string): Promise<IFileInfo[]> =>
cloudService({ cloudService({
url: '/home-api/file/catalog', url: '/home-api/file/catalog',

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-if="isMobile()" class="mobile-common-query card-item"> <div class="mobile-common-query card-item">
<div class="query-item"> <div class="query-item">
<span>分类</span> <span>分类</span>
<el-segmented v-model="anniversaryStore.queryInfo.category" <el-segmented v-model="anniversaryStore.queryInfo.category"
@@ -7,27 +7,11 @@
@change="anniversaryStore.queryAnniversaryList()"/> @change="anniversaryStore.queryAnniversaryList()"/>
</div> </div>
</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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useAnniversaryStore } from '@/store/anniversary.ts' import { useAnniversaryStore } from '@/store/anniversary.ts'
import { Refresh } from '@element-plus/icons-vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ICategory } from '@/types' import { ICategory } from '@/types'
const anniversaryStore = useAnniversaryStore() const anniversaryStore = useAnniversaryStore()
@@ -39,11 +23,6 @@ onMounted(async () => {
initCategoryList() initCategoryList()
}) })
const refreshClick = async () => {
anniversaryStore.queryInfo.category = ''
await anniversaryStore.queryAnniversaryList()
}
const initCategoryList = () => { const initCategoryList = () => {
queryCategoryList.value = [] queryCategoryList.value = []
queryCategoryList.value.push({ queryCategoryList.value.push({

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-if="isMobile()" class="mobile-common-query card-item"> <div class="mobile-common-query card-item">
<div class="query-item"> <div class="query-item">
<span>分类</span> <span>分类</span>
<el-segmented v-model="assetStore.queryInfo.category" <el-segmented v-model="assetStore.queryInfo.category"
@@ -7,27 +7,11 @@
@change="assetStore.refreshInfo()"/> @change="assetStore.refreshInfo()"/>
</div> </div>
</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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useAssetStore } from '@/store/asset.ts' import { useAssetStore } from '@/store/asset.ts'
import { Refresh } from '@element-plus/icons-vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ICategory } from '@/types' import { ICategory } from '@/types'
const assetStore = useAssetStore() const assetStore = useAssetStore()
@@ -39,11 +23,6 @@ onMounted(async () => {
initCategoryList() initCategoryList()
}) })
const refreshClick = async () => {
assetStore.queryInfo.category = ''
await assetStore.queryAssetList()
}
const initCategoryList = () => { const initCategoryList = () => {
queryCategoryList.value = [] queryCategoryList.value = []
queryCategoryList.value.push({ 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({ dailyInfo.attrs.push({
dot: 'red', dot: 'red',
dates: getCalendarDateList(billStore.billRecordList, 'expensive') dates: getCalendarDateList(billStore.billRecordList, 'expense')
}) })
dailyInfo.attrs.push({ dailyInfo.attrs.push({
dot: 'green', 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

@@ -41,7 +41,6 @@ import * as echarts from 'echarts'
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil' import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
import { useBillStore } from '@/store/bill.ts' import { useBillStore } from '@/store/bill.ts'
import { useAppStore } from '@/store/app.ts' import { useAppStore } from '@/store/app.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { formatDate } from 'vue3-common/utils/dateUtil' import { formatDate } from 'vue3-common/utils/dateUtil'
const billStore = useBillStore() const billStore = useBillStore()
@@ -77,7 +76,7 @@ const updateChart = () => {
billDailyChart.setOption({ billDailyChart.setOption({
series: [{ series: [{
itemStyle: { itemStyle: {
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69' color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
}, },
markPoint: { markPoint: {
label: { label: {
@@ -97,7 +96,7 @@ const updateChart = () => {
billBookChart.setOption({ billBookChart.setOption({
series: [{ series: [{
itemStyle: { itemStyle: {
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69' color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
}, },
markPoint: { markPoint: {
label: { label: {
@@ -111,7 +110,6 @@ const updateChart = () => {
const billCategoryList = billStore.billCategoryStatsList.slice(0, MAX_CATEGORY_COUNT) const billCategoryList = billStore.billCategoryStatsList.slice(0, MAX_CATEGORY_COUNT)
billCategoryChart.setOption(pieChartOptions('rank', '元', billCategoryList)) billCategoryChart.setOption(pieChartOptions('rank', '元', billCategoryList))
if (isMobile()) {
billCategoryChart.setOption({ billCategoryChart.setOption({
series: [{ series: [{
radius: ['20%', '50%'], radius: ['20%', '50%'],
@@ -120,6 +118,5 @@ const updateChart = () => {
} }
}] }]
}) })
}
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<daily-summary :date="dailyItemInfo.date"> <daily-summary :date="dailyItemInfo.date">
<template #extra> <template #extra>
<span>收入{{ formatAmount(dailyItemInfo.totalIncome, true) }}</span> <span>收入{{ formatAmount(dailyItemInfo.totalIncome, true) }}</span>
<span>支出{{ formatAmount(dailyItemInfo.totalExpensive, true) }}</span> <span>支出{{ formatAmount(dailyItemInfo.totalExpense, true) }}</span>
</template> </template>
</daily-summary> </daily-summary>
@@ -40,14 +40,12 @@ import DailySummary from '@/components/DailySummary.vue'
import DailyCard from '@/components/DailyCard.vue' import DailyCard from '@/components/DailyCard.vue'
import { defineProps, onMounted, reactive, watch } from 'vue' import { defineProps, onMounted, reactive, watch } from 'vue'
import { useBillStore } from '@/store/bill.ts' import { useBillStore } from '@/store/bill.ts'
import { useRouter } from 'vue-router'
import { formatChineseDate } from 'vue3-common/utils/dateUtil' import { formatChineseDate } from 'vue3-common/utils/dateUtil'
import { IBillRecord } from '@/types/bill.ts' import { IBillRecord } from '@/types/bill.ts'
import { formatAmount } from 'vue3-common/utils/numberUtil' import { formatAmount } from 'vue3-common/utils/numberUtil'
import { getCalendarDailyCount, getCategoryByName } from '@/utils/home/billUtil.ts' import { getCalendarDailyCount, getCategoryByName } from '@/utils/home/billUtil.ts'
const billStore = useBillStore() const billStore = useBillStore()
const router = useRouter()
const props = defineProps({ const props = defineProps({
date: { date: {
@@ -70,7 +68,7 @@ onMounted(() => {
const dailyItemInfo = reactive({ const dailyItemInfo = reactive({
date: '', date: '',
totalExpensive: 0, totalExpense: 0,
totalIncome: 0, totalIncome: 0,
itemList: [] as IBillRecord[] itemList: [] as IBillRecord[]
}) })
@@ -86,7 +84,7 @@ const getBillDailyByDate = (date: string) => {
const calendarDaily = getCalendarDailyCount(billStore.billRecordList, date) const calendarDaily = getCalendarDailyCount(billStore.billRecordList, date)
dailyItemInfo.itemList = calendarDaily.dailyRecordList dailyItemInfo.itemList = calendarDaily.dailyRecordList
dailyItemInfo.totalIncome = calendarDaily.totalIncome dailyItemInfo.totalIncome = calendarDaily.totalIncome
dailyItemInfo.totalExpensive = calendarDaily.totalExpensive dailyItemInfo.totalExpense = calendarDaily.totalExpense
} else { } else {
dailyItemInfo.itemList = [] dailyItemInfo.itemList = []
} }
@@ -99,10 +97,7 @@ const getBillDailyByDate = (date: string) => {
const selectBillRecordClick = async (billRecord: IBillRecord) => { const selectBillRecordClick = async (billRecord: IBillRecord) => {
await billStore.queryBillRecord(billRecord.id as number) await billStore.queryBillRecord(billRecord.id as number)
billStore.billApiType = 'UPDATE' billStore.billApiType = 'UPDATE'
await router.push({ billStore.billDrawer = true
name: 'BillDetailId',
params: { id: billStore.currentBillRecord.id }
})
} }
</script> </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> <template>
<div class="common-query"> <div class="common-query">
<el-select v-model="billStore.billQueryForm.billBook" <el-select v-model="billStore.billQueryForm.billBook"
@change="billStore.refreshInfo(isStats)" style="width: 120px"> @change="billStore.refreshInfo(isStats)">
<el-option <el-option
v-for="(item, index) in billQuery.billBookList" v-for="(item, index) in billQuery.billBookList"
:key="index" :label="item.label" :value="item.name" :key="index" :label="item.label" :value="item.name"
@@ -9,20 +9,16 @@
</el-select> </el-select>
<el-select v-model="billStore.billDateType" <el-select v-model="billStore.billDateType"
@change="billStore.refreshInfo(isStats)" @change="billStore.refreshInfo(isStats)">
style="width: 120px;align-self: center;">
<el-option v-for="(item, index) in dateTypeOptions" :key="index" <el-option v-for="(item, index) in dateTypeOptions" :key="index"
:label="item.label" :value="item.value"/> :label="item.label" :value="item.value"/>
</el-select> </el-select>
<el-switch <el-select v-if="isStats" v-model="billStore.billType"
v-model="billStore.billType" @change="billStore.refreshInfo(isStats)">
inline-prompt <el-option v-for="(item, index) in billTypeOptions" :key="index"
active-value="income" inactive-value="expensive" :label="item.label" :value="item.value"/>
active-text="收入账单" inactive-text="支出账单" </el-select>
@change="billStore.refreshInfo(isStats)"
style="--el-switch-on-color: #6FBB69; --el-switch-off-color: #CA6C65;"
/>
</div> </div>
</template> </template>
@@ -58,6 +54,15 @@ const dateTypeOptions = [{
value: 'year' value: 'year'
}] }]
const billTypeOptions = [{
label: '收入',
value: 'income'
},
{
label: '支出',
value: 'expense'
}]
const billQuery = reactive({ const billQuery = reactive({
query: '', query: '',
billBookList: [] as IBillBook[] billBookList: [] as IBillBook[]

View File

@@ -32,7 +32,3 @@ onMounted(async () => {
await billStore.searchInfo() await billStore.searchInfo()
}) })
</script> </script>
<style lang="scss">
@use "@/styles/home/bill.mobile.module";
</style>

View File

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

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

View File

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

View File

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

View File

@@ -1,38 +1,25 @@
<template> <template>
<div class="life-record-card card-item"> <div class="life-record-card card-item">
<el-carousel v-if="props.foodItem?.recordList.length > 0" <el-image v-if="props.foodItem?.recordList.length > 0"
arrow="always" @change="changeFoodEvent"> :src="`${props.foodItem?.recordList[0].imageUrl}${ThumbImageQuery}`"
<el-carousel-item v-for="(item, index) in getFoodRecord(props.foodItem?.recordList)" fit="contain" alt="暂无成果" @click="viewImageClick">
:key="index">
<el-image :src="AListPath + item.imageUrl"
fit="contain" alt="暂无成果">
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<span>暂无图片</span> <span>暂无图片</span>
</div> </div>
</template> </template>
</el-image> </el-image>
</el-carousel-item>
</el-carousel>
<el-empty v-else description="暂无成果" /> <el-empty v-else description="暂无成果" />
<div class="record-info"> <van-swipe-cell>
<div class="record-info" @click="viewClick()">
<div class="top-container"> <div class="top-container">
<div class="info-list"> <div class="info-list">
<div class="info-item"> <div class="info-item">
<span class="title">{{ props.foodItem.name }}</span> <span class="title">{{ props.foodItem.name }}</span>
</div> </div>
</div> </div>
<div class="button-info">
<el-button 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>
<div class="bottom-container"> <div class="bottom-container">
@@ -59,18 +46,26 @@
</div> </div>
</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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
import { useFoodStore } from '@/store/food.ts' import { useFoodStore } from '@/store/food.ts'
import { defineProps, onMounted, PropType, reactive } from 'vue' 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 { useRouter } from 'vue-router'
import { formatDate } from 'vue3-common/utils/dateUtil' import { formatDate } from 'vue3-common/utils/dateUtil'
import { commonElMessageBox } from 'vue3-common/utils/elUtil' import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { AListPath } from '@/utils' import { showImagePreview } from 'vant'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
const router = useRouter() const router = useRouter()
const foodStore = useFoodStore() const foodStore = useFoodStore()
@@ -91,31 +86,28 @@ onMounted(() => {
foodCardInfo.recommendRate = props.foodItem?.recommendRate as number foodCardInfo.recommendRate = props.foodItem?.recommendRate as number
}) })
const getFoodRecord = (recordList: IFoodRecord[]) => { const viewImageClick = () => {
return recordList.filter((item) => item.imageUrl) 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 const id = props.foodItem?.id
foodStore.foodRecipeApiType = 'UPDATE' foodStore.foodRecipeApiType = 'UPDATE'
await foodStore.queryFoodRecipe(id as number) await foodStore.queryFoodRecipe(id as number)
await router.push({ name: 'FoodRecipeForm' }) await router.push({ name: 'FoodRecipeForm' })
} }
const viewFoodRecipeClick = async () => { const viewClick = async () => {
await router.push({ name: 'FoodDetailId', params: { id: props.foodItem?.id } }) await router.push({ name: 'FoodDetailId', params: { id: props.foodItem?.id } })
} }
const deleteFoodRecipeClick = () => { const deleteClick = () => {
foodStore.foodRecipeApiType = 'DELETE' foodStore.foodRecipeApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => { commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => {
foodStore.handleFoodRecipeApi(props.foodItem) foodStore.handleFoodRecipeApi(props.foodItem)
}) })
} }
const changeFoodEvent = (current: number) => {
foodCardInfo.currentFoodIndex = current
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -30,8 +30,6 @@ import { useRouter } from 'vue-router'
import { useAppStore } from '@/store/app.ts' import { useAppStore } from '@/store/app.ts'
import { formatChineseDate } from 'vue3-common/utils/dateUtil' import { formatChineseDate } from 'vue3-common/utils/dateUtil'
import { IFoodRecord } from '@/types/food.ts' import { IFoodRecord } from '@/types/food.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const foodStore = useFoodStore() const foodStore = useFoodStore()
const appStore = useAppStore() const appStore = useAppStore()
@@ -85,18 +83,10 @@ const getFoodDailyByDate = (date: string) => {
*/ */
const selectFoodRecordClick = async (foodRecord: IFoodRecord) => { const selectFoodRecordClick = async (foodRecord: IFoodRecord) => {
foodStore.foodRecordApiType = 'UPDATE' foodStore.foodRecordApiType = 'UPDATE'
if (isMobile()) {
appStore.isShowMobileBack = true
foodStore.currentFoodRecord = foodRecord foodStore.currentFoodRecord = foodRecord
appStore.isShowMobileBack = true
await router.push({ name: 'FoodRecordForm' }) await router.push({ name: 'FoodRecordForm' })
} else {
foodStore.foodRecordDialog = {
title: '编辑记录',
visible: true,
data: deepCopyObject(foodRecord)
}
}
} }
</script> </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,68 +1,39 @@
<template> <template>
<div v-if="isMobile()" class="mobile-common-query card-item"> <div class="common-query">
<div class="query-item"> <el-select
<span>分类</span> v-model="foodStore.queryRecipe.name"
<el-segmented v-model="foodStore.queryRecipe.category" filterable clearable placeholder="请选择菜谱名称"
:options="queryCategoryList" style="width: 150px;"
@change="queryFoodSegmented"/> @change="changeFoodNameEvent">
</div> <el-option
</div> v-for="(item, index) in foodStore.foodNameList"
:key="index" :label="item" :value="item"
<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"/>
</el-select> </el-select>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useFoodStore } from '@/store/food.ts' import { useFoodStore } from '@/store/food.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil' import { onMounted } from 'vue'
import { onMounted, ref } from 'vue' import { searchFoodRecipeApi } from '@/apis/food.ts'
import { ICategory } from '@/types'
const foodStore = useFoodStore() const foodStore = useFoodStore()
const queryCategoryList = ref([] as ICategory[])
onMounted(async () => { onMounted(async () => {
await foodStore.queryFoodCategory() await foodStore.queryFoodNameList()
initCategoryList() foodStore.queryRecipe.name = ''
}) })
const dateTypeOptions = [{ const changeFoodNameEvent = async (value: string) => {
label: '按月选择', if (value) {
value: 'month' const result = await searchFoodRecipeApi(value)
}, foodStore.foodRecipeList = [result]
{ } else {
label: '按年选择', foodStore.pageInfo.currentPage = 1
value: 'year' foodStore.pageInfo.pageSize = 3
}]
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
}
foodStore.foodRecipeList = [] foodStore.foodRecipeList = []
await foodStore.queryMobileFoodRecipeList() await foodStore.queryMobileFoodRecipeList()
}
} }
</script> </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> </script>

View File

@@ -6,7 +6,7 @@
<el-table :data="foodStore.currentFoodRecipe.materialList" <el-table :data="foodStore.currentFoodRecipe.materialList"
border stripe class="material-table"> border stripe class="material-table">
<el-table-column prop="type" label="类型" align="center" min-width="10%"/> <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="name" label="名称" align="center" min-width="20%"/>
<el-table-column prop="amount" 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"> <el-table-column label="操作" align="center" width="100">

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

@@ -13,11 +13,25 @@
</el-form-item> </el-form-item>
<el-form-item label="图片"> <el-form-item label="图片">
<upload-image v-model="foodStore.currentFoodStep.imageUrl" <el-upload
:service-url="fileServiceUrl" v-model:file-list="imageFileList"
:image-limit="1" list-type="picture-card"
:service-file-root-path="serviceFileRootPath" :accept="acceptImageTypes"
:image-path="foodStore.foodImagePath"/> :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-item>
</el-form> </el-form>
@@ -31,17 +45,58 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { UploadImage } from 'vue3-common'
import { useFoodStore } from '@/store/food.ts' import { useFoodStore } from '@/store/food.ts'
import { ref, reactive, nextTick } from 'vue' import { ref, reactive, nextTick, onMounted } from 'vue'
import { FormRules } from 'element-plus' import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { foodStepRules } from '@/utils/element/elRules.ts' 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 rules = reactive<FormRules>(foodStepRules)
const formRef = ref() const formRef = ref()
const foodStore = useFoodStore() 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)
}
/** /**
* 打开对话框事件 * 打开对话框事件

View File

@@ -9,7 +9,7 @@
<el-table-column prop="content" label="内容" align="center" min-width="20%" /> <el-table-column prop="content" label="内容" align="center" min-width="20%" />
<el-table-column prop="imageUrl" label="图片" align="center" min-width="20%"> <el-table-column prop="imageUrl" label="图片" align="center" min-width="20%">
<template #default="scope"> <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> </template>
</el-table-column> </el-table-column>
@@ -30,7 +30,6 @@ import { useFoodStore } from '@/store/food.ts'
import { IFoodStep } from '@/types/food.ts' import { IFoodStep } from '@/types/food.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil' import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { commonElMessageBox } from 'vue3-common/utils/elUtil' import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { serviceFileRootPath } from '@/utils'
const foodStore = useFoodStore() const foodStore = useFoodStore()

View File

@@ -78,12 +78,15 @@ const updateChart = () => {
const sortedRecordList = weightRecordList.sort((a, b) => a.weight - b.weight) const sortedRecordList = weightRecordList.sort((a, b) => a.weight - b.weight)
weightDailyChart.setOption(lineChartOptions('日期', weightXAxis, '体重(千克)', weightYAxis)) weightDailyChart.setOption(lineChartOptions('日期', weightXAxis, '体重(千克)', weightYAxis))
if (sortedRecordList.length > 0) {
weightDailyChart.setOption({ weightDailyChart.setOption({
yAxis: { yAxis: {
min: Math.floor(sortedRecordList[0].weight), min: Math.floor(sortedRecordList[0].weight),
max: Math.ceil(sortedRecordList[sortedRecordList.length - 1].weight) max: Math.ceil(sortedRecordList[sortedRecordList.length - 1].weight)
} }
}) })
}
// 睡眠时长统计图 // 睡眠时长统计图
const sleepRecordList = healthStore.healthRecordList const sleepRecordList = healthStore.healthRecordList

View File

@@ -55,13 +55,13 @@
<span>睡眠开始时间</span> <span>睡眠开始时间</span>
</template> </template>
<el-date-picker <el-time-picker
v-model="healthStore.currentHealth.sleepStartTime" v-model="healthStore.currentHealth.sleepStartTime"
type="datetime" clearable :editable="false" clearable :editable="false"
placeholder="选择时间" placeholder="选择睡眠开始时间"
style="width: 180px" style="width: 180px"
format="YYYY-MM-DD HH:mm" format="HH:mm"
value-format="YYYY-MM-DD HH:mm:ss" value-format="HH:mm"
@clear="healthStore.currentHealth.sleepStartTime = ''" @clear="healthStore.currentHealth.sleepStartTime = ''"
/> />
</el-form-item> </el-form-item>
@@ -72,17 +72,27 @@
<span>睡眠结束时间</span> <span>睡眠结束时间</span>
</template> </template>
<el-date-picker <el-time-picker
v-model="healthStore.currentHealth.sleepEndTime" v-model="healthStore.currentHealth.sleepEndTime"
type="datetime" clearable :editable="false" clearable :editable="false"
placeholder="选择时间" placeholder="选择睡眠结束时间"
style="width: 180px" style="width: 180px"
format="YYYY-MM-DD HH:mm" format="HH:mm"
value-format="YYYY-MM-DD HH:mm:ss" value-format="HH:mm"
@clear="healthStore.currentHealth.sleepEndTime = ''" @clear="healthStore.currentHealth.sleepEndTime = ''"
/> />
</el-form-item> </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-form-item style="align-self: center;">
<el-button type="primary" @click="onSubmit" <el-button type="primary" @click="onSubmit"
:disabled="isBeforeDate(formatDate(new Date()), healthStore.selectDate)"> :disabled="isBeforeDate(formatDate(new Date()), healthStore.selectDate)">
@@ -96,6 +106,7 @@
import { useHealthStore } from '@/store/health' import { useHealthStore } from '@/store/health'
import { formatDate, isBeforeDate } from 'vue3-common/utils/dateUtil' import { formatDate, isBeforeDate } from 'vue3-common/utils/dateUtil'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
const healthStore = useHealthStore() const healthStore = useHealthStore()
@@ -116,14 +127,26 @@ const onSubmit = async () => {
} }
} }
if (sleepStartTime && sleepEndTime && !sleepStartTime && sleepEndTime) { if (sleepStartTime && sleepEndTime) {
ElMessage.error('睡眠结束时间应大于睡眠开始时间') healthStore.currentHealth.sleepDuration = calculateSleepDuration(sleepStartTime, sleepEndTime)
return
} }
await healthStore.updateHealthDay() await healthStore.updateHealthDay()
healthStore.currentHealth = healthStore.getHealthDay(healthStore.selectDate) 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> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -15,6 +15,8 @@ import StatsCard from '@/components/StatsCard.vue'
import { useHealthStore } from '@/store/health' import { useHealthStore } from '@/store/health'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { formatAmount } from 'vue3-common/utils/numberUtil' 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() const healthStore = useHealthStore()
@@ -30,7 +32,10 @@ const healthStats = ref({
const updateStats = () => { const updateStats = () => {
const recordList = healthStore.healthRecordList const recordList = healthStore.healthRecordList
const totalWeight = recordList.reduce((sum, item) => sum + item.weight, 0) const totalWeight = recordList.reduce((sum, item) => sum + item.weight, 0)
if (recordList.length > 0) {
healthStats.value.averageWeight = totalWeight / recordList.length healthStats.value.averageWeight = totalWeight / recordList.length
healthStats.value.totalDuration = recordList.reduce((sum, item) => sum + item.sportDuration, 0) healthStats.value.totalDuration = recordList.reduce((sum, item) => sum + item.sportDuration, 0)
}
} }
</script> </script>

View File

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

View File

@@ -13,7 +13,6 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useLedgerStore } from '@/store/ledger.ts' import { useLedgerStore } from '@/store/ledger.ts'
import { ICategory } from '@/types' import { ICategory } from '@/types'
import { statsByField } from '@/utils/home/ledgerUtil.ts'
const ledgerStore = useLedgerStore() const ledgerStore = useLedgerStore()
@@ -28,10 +27,6 @@ onMounted(async () => {
const initCategoryList = () => { const initCategoryList = () => {
queryCategoryList.value = [] queryCategoryList.value = []
if (ledgerStore.ledgerBookList.length > 0) {
ledgerStore.queryInfo.book = ledgerStore.ledgerBookList[0]
}
ledgerStore.ledgerBookList.forEach((item) => { ledgerStore.ledgerBookList.forEach((item) => {
queryCategoryList.value.push({ queryCategoryList.value.push({
label: item, label: item,

View File

@@ -79,7 +79,6 @@ import { onMounted, reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules.ts' import { loginRules } from '@/utils/element/elRules.ts'
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil' import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { ILoginType } from '@/types/auth.ts' import { ILoginType } from '@/types/auth.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const rules = reactive<FormRules>(loginRules) const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -98,16 +97,6 @@ onMounted(() => {
const handleLogin = async (type: ILoginType) => { const handleLogin = async (type: ILoginType) => {
loginFormRef.value.validate(async (valid: any) => { loginFormRef.value.validate(async (valid: any) => {
if (valid) { if (valid) {
if (type === 'common' && !isMobile()) {
ElMessage.error('目前只支持移动端')
return
}
if (type === 'admin' && isMobile()) {
ElMessage.error('目前只支持PC端')
return
}
if (type === 'common') { if (type === 'common') {
authStore.loginLoading = true authStore.loginLoading = true
} else { } 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> <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> <el-avatar class="avatar-info">{{ passwordItem.title.charAt(0) }}</el-avatar>
<div class="title-info"> <div class="title-info">
<span>{{ passwordItem.title }}</span> <span>{{ passwordItem.title }}</span>
<span>{{ passwordItem.username }}</span> <span>{{ passwordItem.username }}</span>
</div> </div>
</div>
<div class="button-info"> <template #right>
<el-button type="primary" :icon="Edit" circle <van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/> @click="editClick"/>
<el-button type="danger" :icon="DeleteFilled" circle <van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()" /> @click="deleteClick()"/>
</div> </template>
</div> </van-swipe-cell>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { usePasswordStore } from '@/store/password.ts' 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 { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { IPassword } from '@/types/password.ts' import { IPassword } from '@/types/password.ts'
@@ -34,10 +35,6 @@ const props = defineProps({
} }
}) })
onMounted(() => {
})
const editClick = async () => { const editClick = async () => {
const id = props.passwordItem?.id const id = props.passwordItem?.id
passwordStore.passwordApiType = 'UPDATE' passwordStore.passwordApiType = 'UPDATE'
@@ -59,7 +56,7 @@ const deleteClick = () => {
padding: 10px; padding: 10px;
display: grid; display: grid;
grid-template-columns: 40px 1fr 80px; grid-template-columns: 40px 1fr;
align-items: center; align-items: center;
gap: 10px; gap: 10px;

View File

@@ -5,9 +5,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, watch } from 'vue' import { onMounted, watch } from 'vue'
import { Calendar, DateSelectArg, EventClickArg } from '@fullcalendar/core' 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 { usePlanStore } from '@/store/plan.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
let calendar: Calendar | undefined let calendar: Calendar | undefined
@@ -27,11 +26,7 @@ watch(() => planStore.isRefresh, () => {
const initCalendar = async () => { const initCalendar = async () => {
const calendarEl = document.getElementById('planCalendar') as HTMLElement const calendarEl = document.getElementById('planCalendar') as HTMLElement
if (isMobile()) {
calendar = initMobileCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent) calendar = initMobileCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
} else {
calendar = initCommonCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
}
calendar.render() calendar.render()
} }
@@ -83,7 +78,7 @@ const eventClickEvent = (eventClick: EventClickArg) => {
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/styles/home.variable.module"; @use "@/styles/home.variable.module" as variable;
$cell-height: 3.8vh; $cell-height: 3.8vh;
$time-slot-width: 70px; $time-slot-width: 70px;
@@ -109,7 +104,7 @@ $month-cell-height: 120px;
} }
.fc-button-active { .fc-button-active {
background-color: $theme-color; background-color: variable.$theme-color;
color: #FFFFFF; color: #FFFFFF;
} }
} }
@@ -142,7 +137,7 @@ $month-cell-height: 120px;
// 表头区域 // 表头区域
.fc-col-header { .fc-col-header {
color: $theme-color; color: variable.$theme-color;
height: 50px; height: 50px;
font-size: 1.4rem; font-size: 1.4rem;
background-color: var(--color-record-card-bg); 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 { AlarmClock, Clock } from '@element-plus/icons-vue'
import { IPlanInfo } from '@/types/plan.ts' import { IPlanInfo } from '@/types/plan.ts'
import { getRemainDays, isBeforeToday } from '@/utils/home/planUtil.ts' import { getRemainDays, isBeforeToday } from '@/utils/home/planUtil.ts'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { usePlanStore } from '@/store/plan.ts' import { usePlanStore } from '@/store/plan.ts'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { commonElMessageBox } from 'vue3-common/utils/elUtil' import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const planStore = usePlanStore() const planStore = usePlanStore()
const router = useRouter() const router = useRouter()
@@ -85,17 +83,8 @@ const props = defineProps({
const editPlanClick = async () => { const editPlanClick = async () => {
planStore.planApiType = 'UPDATE' planStore.planApiType = 'UPDATE'
if (isMobile()) {
planStore.currentPlan = props.plan as IPlanInfo planStore.currentPlan = props.plan as IPlanInfo
await router.push({ name: 'MobileHomePlanDetailId', params: { id: planStore.currentPlan.id } }) await router.push({ name: 'PlanDetailId', params: { id: planStore.currentPlan.id } })
} else {
planStore.planDialog = {
title: '编辑计划',
visible: true,
data: deepCopyObject(props.plan)
}
}
} }
const changeCompleteEvent = (value: boolean) => { 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> <template>
<div class="common-query"> <div class="mobile-common-query card-item">
<el-select v-model="planStore.queryInfo.planType" <div class="query-item">
@change="planStore.refreshInfo()" <span>分类</span>
style="width: 120px;align-self: center;"> <el-segmented v-model="planStore.queryInfo.planType"
<el-option v-for="(item, index) in planTypeOptions" :key="index" :options="planTypeOptions"
:label="item.label" :value="item.value"/> @change="planStore.refreshInfo()"/>
</el-select> </div>
<el-select v-model="planStore.queryInfo.orderType" <div class="query-item">
@change="planStore.refreshInfo()" <span>排序</span>
style="width: 120px;align-self: center;"> <el-segmented v-model="planStore.queryInfo.orderType"
<el-option v-for="(item, index) in orderTypeOptions" :key="index" :options="orderTypeOptions"
:label="item.label" :value="item.value"/> @change="planStore.refreshInfo()"/>
</el-select> </div>
</div> </div>
</template> </template>
@@ -26,20 +26,20 @@ const planTypeOptions = [{
value: 'all' value: 'all'
}, },
{ {
label: '已完成计划', label: '已完成',
value: 'completed' value: 'completed'
}, },
{ {
label: '未完成计划', label: '未完成',
value: 'notCompleted' value: 'notCompleted'
}] }]
const orderTypeOptions = [{ const orderTypeOptions = [{
label: '时间排序', label: '时间',
value: 'date' value: 'date'
}, },
{ {
label: '重要性排序', label: '重要性',
value: 'priority' value: 'priority'
}] }]
</script> </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> <template>
<div class="life-record-card card-item"> <div class="life-record-card card-item">
<el-image :src="AListPath + props.productItem?.imageUrl" <el-image :src="`${props.productItem?.imageUrl}${ThumbImageQuery}`"
:preview-src-list="[AListPath + props.productItem?.imageUrl]" fit="contain" alt="暂无图片" @click="viewImageClick">
fit="contain" alt="暂无成果">
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<span>暂无图片</span> <span>暂无图片</span>
@@ -10,7 +9,8 @@
</template> </template>
</el-image> </el-image>
<div class="record-info"> <van-swipe-cell>
<div class="record-info" @click="editClick()">
<div class="top-container"> <div class="top-container">
<div class="info-list"> <div class="info-list">
<div class="info-item"> <div class="info-item">
@@ -23,29 +23,21 @@
</div> </div>
</div> </div>
<div class="button-info"> <el-tag v-if="props.productItem.expiryDate" type="danger" effect="dark">
<el-button type="primary" :icon="Edit" circle :size="'small'" {{ props.productItem.expiryDate }}
@click="editClick"/> </el-tag>
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
@click="deleteClick()" />
</div>
</div> </div>
<div class="bottom-container"> <div class="bottom-container">
<div class="info-list"> <div class="info-list">
<div class="info-item"> <div class="info-item">
<i class="fa-solid fa-clock" style="color: #6A5ACD"></i> <i class="fa-solid fa-clock" style="color: #6A5ACD"></i>
{{ props.productItem.totalDay }} {{ getTotalDay(props.productItem) }}
</div> </div>
<div class="info-item"> <div class="info-item">
<i class="fa-solid fa-chart-line" style="color: #FFA500"></i> <i class="fa-solid fa-chart-line" style="color: #FFA500"></i>
{{ props.productItem.averagePrice }}/ {{ (props.productItem.price / getTotalDay(props.productItem)).toFixed(2) }}/
</div>
<div class="info-item">
<i class="fa-solid fa-location-dot"></i>
{{ props.productItem.channel }}
</div> </div>
</div> </div>
<div class="info-list"> <div class="info-list">
@@ -60,20 +52,28 @@
</div> </div>
</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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { useProductStore } from '@/store/product.ts' 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 { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { IProduct } from '@/types/product.ts' import { IProduct } from '@/types/product.ts'
import { useRouter } from 'vue-router' import { ThumbImageQuery } from '@/utils'
import { AListPath } 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 productStore = useProductStore()
const router = useRouter()
const props = defineProps({ const props = defineProps({
productItem: { productItem: {
@@ -82,16 +82,14 @@ const props = defineProps({
} }
}) })
onMounted(() => { const viewImageClick = () => {
showImagePreview({ images: [props.productItem?.imageUrl], closeable: true })
}) }
const editClick = async () => { const editClick = async () => {
const id = props.productItem?.id
productStore.productApiType = 'UPDATE' productStore.productApiType = 'UPDATE'
productStore.currentProduct = deepCopyObject(props.productItem) as IProduct
productStore.currentProduct = props.productItem as IProduct productStore.productDrawer = true
await router.push({ name: 'ProductDetailId', params: { id } })
} }
const deleteClick = () => { const deleteClick = () => {
@@ -100,6 +98,14 @@ const deleteClick = () => {
productStore.handleProductApi(props.productItem as IProduct) 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> </script>
<style lang="scss"> <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: '购买日期', label: '购买日期',
value: 'date' value: 'date'
}, { }, {
label: '日均价格', label: '价格',
value: 'price' value: 'price'
} }
] ]

View File

@@ -12,24 +12,15 @@
<el-card class="record-card"> <el-card class="record-card">
<span class="record-content">旅行人{{ item.persons.join('-') }}</span> <span class="record-content">旅行人{{ item.persons.join('-') }}</span>
<el-image v-for="(image, index) in item.imageList" :key="index" <el-image v-for="(image, index) in item.imageList" :key="index"
:src="AListPath + image" :src="`${image}${ThumbImageQuery}`"
:preview-src-list="[AListPath + image]" fit="cover" alt="暂无图片" class="record-image"
fit="contain" alt="暂无图片" class="record-image"/> @click="viewImageClick(item, index)"/>
<video v-for="(video, index) in item.videoList" <video v-for="(video, index) in item.videoList"
:key="index" controls style="width: 60%;min-height: 200px;"> :key="index" controls style="width: 100%;">
<source :src="AListPath + video" type="video/mp4"> <source :src="video" type="video/mp4">
您的浏览器不支持 HTML5 视频 您的浏览器不支持 HTML5 视频
</video> </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-card>
</el-timeline-item> </el-timeline-item>
</el-timeline> </el-timeline>
@@ -41,7 +32,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { SvgIcon } from 'vue3-common' import { SvgIcon } from 'vue3-common'
import { useTravelStore } from '@/store/travel.ts' import { useTravelStore } from '@/store/travel.ts'
import { AListPath } from '@/utils' import { showImagePreview } from 'vant'
import { ITravelRecord } from '@/types/travel.ts'
import { ThumbImageQuery } from '@/utils'
const travelStore = useTravelStore() const travelStore = useTravelStore()
const viewImageClick = (record: ITravelRecord, index: number) => {
showImagePreview({ images: record.imageList, closeable: true, startPosition: index })
}
</script> </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> <template>
<div class="life-record-card card-item"> <div class="life-record-card card-item">
<el-carousel v-if="props.travelItem?.recordList.length > 0" <el-image v-if="props.travelItem?.recordList.length > 0"
arrow="always" @change="changeTravelEvent"> :src="`${props.travelItem?.recordList[0].imageList[0]}${ThumbImageQuery}`"
<el-carousel-item v-for="(item, index) in props.travelItem?.recordList" :key="index"> fit="contain" alt="暂无成果" @click="viewImageClick">
<el-image :src="AListPath + item.imageList[0]"
fit="contain" alt="暂无图片">
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<span>暂无图片</span> <span>暂无图片</span>
</div> </div>
</template> </template>
</el-image> </el-image>
</el-carousel-item>
</el-carousel>
<el-empty v-else description="暂无记录"/> <el-empty v-else description="暂无记录"/>
<div class="record-info"> <van-swipe-cell>
<div class="record-info" @click="viewClick">
<div class="top-container"> <div class="top-container">
<div class="info-list"> <div class="info-list">
<div class="info-item"> <div class="info-item">
<span class="title">{{ props.travelItem.name }}</span> <span class="title">{{ props.travelItem.name }}</span>
</div> </div>
</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>
<div class="bottom-container"> <div class="bottom-container">
@@ -58,11 +46,18 @@
</div> </div>
</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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
import { useTravelStore } from '@/store/travel.ts' import { useTravelStore } from '@/store/travel.ts'
import { useAppStore } from '@/store/app.ts' import { useAppStore } from '@/store/app.ts'
import { defineProps, PropType, reactive } from 'vue' import { defineProps, PropType, reactive } from 'vue'
@@ -70,8 +65,9 @@ import { useRouter } from 'vue-router'
import { formatDate } from 'vue3-common/utils/dateUtil' import { formatDate } from 'vue3-common/utils/dateUtil'
import { ITravelSpot } from '@/types/travel.ts' import { ITravelSpot } from '@/types/travel.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil' import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { AListPath } from '@/utils'
import { parsePerson } from '@/utils/home/travelUtil.ts' import { parsePerson } from '@/utils/home/travelUtil.ts'
import { showImagePreview } from 'vant'
import { RawImageQuery, ThumbImageQuery } from '@/utils'
const router = useRouter() const router = useRouter()
const travelStore = useTravelStore() 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 editClick = async () => {
const id = props.travelItem?.id const id = props.travelItem?.id
travelStore.travelSpotApiType = 'UPDATE' travelStore.travelSpotApiType = 'UPDATE'
@@ -108,10 +111,6 @@ const deleteClick = () => {
travelStore.handleTravelSpotApi(props.travelItem) travelStore.handleTravelSpotApi(props.travelItem)
}) })
} }
const changeTravelEvent = (current: number) => {
cardInfo.currentIndex = current
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-if="isMobile()" class="mobile-common-query card-item"> <div class="mobile-common-query card-item">
<div class="query-item"> <div class="query-item">
<span>城市</span> <span>城市</span>
<el-segmented v-model="travelStore.querySpot.category" <el-segmented v-model="travelStore.querySpot.category"
@@ -7,20 +7,10 @@
@change="queryTravelSegmented"/> @change="queryTravelSegmented"/>
</div> </div>
</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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useTravelStore } from '@/store/travel.ts' import { useTravelStore } from '@/store/travel.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ICategory } from '@/types' import { ICategory } from '@/types'
@@ -28,15 +18,6 @@ const travelStore = useTravelStore()
const queryCategoryList = ref([] as ICategory[]) const queryCategoryList = ref([] as ICategory[])
const dateTypeOptions = [{
label: '按月选择',
value: 'month'
},
{
label: '按年选择',
value: 'year'
}]
onMounted(async () => { onMounted(async () => {
await travelStore.queryTravelCategory() await travelStore.queryTravelCategory()
initCategoryList() initCategoryList()

View File

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

View File

@@ -1,10 +1,5 @@
<template> <template>
<div class="tab-bar"> <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" <router-link v-if="path.includes('bill')" to="/bill/data"
@click="routerClick()" class="item"> @click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i> <i class="fa-solid fa-calendar-days"></i>
@@ -15,15 +10,10 @@
<i class="fa-solid fa-chart-pie"></i> <i class="fa-solid fa-chart-pie"></i>
<span>统计</span> <span>统计</span>
</router-link> </router-link>
<router-link v-if="path.includes('bill')" to="/bill/search" <router-link v-if="path.includes('bill')" to="/bill/agent"
@click="routerClick()" class="item"> @click="routerClick()" class="item">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<span>搜索</span> <span>分析</span>
</router-link>
<router-link v-if="path.includes('bill')" to="/bill/setting"
@click="routerClick()" class="item">
<i class="fa-solid fa-gear"></i>
<span>设置</span>
</router-link> </router-link>
<router-link v-if="path.includes('food')" to="/food/recipe" <router-link v-if="path.includes('food')" to="/food/recipe"
@@ -31,6 +21,11 @@
<i class="fa-solid fa-utensils"></i> <i class="fa-solid fa-utensils"></i>
<span>菜谱</span> <span>菜谱</span>
</router-link> </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" <router-link v-if="path.includes('food')" to="/food/record"
@click="routerClick()" class="item"> @click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i> <i class="fa-solid fa-calendar-days"></i>
@@ -103,6 +98,12 @@
<span>清单</span> <span>清单</span>
</router-link> </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" <router-link v-if="path.includes('journal')" to="/journal/record"
@click="routerClick()" class="item"> @click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i> <i class="fa-solid fa-calendar-days"></i>
@@ -143,6 +144,12 @@
<span>清单</span> <span>清单</span>
</router-link> </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" <router-link v-if="path.includes('server')" to="/server/monitor"
@click="routerClick()" class="item"> @click="routerClick()" class="item">
<i class="fa-solid fa-desktop"></i> <i class="fa-solid fa-desktop"></i>
@@ -169,12 +176,6 @@
<i class="fa-solid fa-box-archive"></i> <i class="fa-solid fa-box-archive"></i>
<span>归档</span> <span>归档</span>
</router-link> </router-link>
<router-link to="/profile/index"
@click="routerClick()" class="item">
<i class="fa-solid fa-user"></i>
<span>我的</span>
</router-link>
</div> </div>
</template> </template>

View File

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

View File

@@ -1,4 +1,5 @@
<template> <template>
<van-config-provider :theme="appStore.isDark ? 'dark' : 'light'">
<section class="layout-mobile-home" <section class="layout-mobile-home"
:class="{ 'hide-tab-bar': appStore.isShowMobileBack }"> :class="{ 'hide-tab-bar': appStore.isShowMobileBack }">
<title-bar /> <title-bar />
@@ -7,6 +8,7 @@
<tab-bar v-if="!appStore.isShowMobileBack"/> <tab-bar v-if="!appStore.isShowMobileBack"/>
</section> </section>
</van-config-provider>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { setCookie, getCookie, getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil' import { setCookie, getCookie, getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { useDark, useToggle } from '@vueuse/core' import { useDark, useToggle } from '@vueuse/core'
import { calcDarken, calcLighten } from '@/utils/element/elColor.ts'
export const useAppStore = defineStore('ipp', { export const useAppStore = defineStore('ipp', {
state: () => ({ state: () => ({
@@ -89,7 +90,17 @@ export const useAppStore = defineStore('ipp', {
return getLocalStorage('theme-color') as string || '#009FA8' return getLocalStorage('theme-color') as string || '#009FA8'
}, },
setThemeColor(value: string) { 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) 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

@@ -10,11 +10,10 @@ import {
queryBillRecordApi, queryBillRecordApi,
queryBillStatsApi, queryBillSummaryApi, searchBillSummaryApi, updateBillRecordApi queryBillStatsApi, queryBillSummaryApi, searchBillSummaryApi, updateBillRecordApi
} from '@/apis/bill' } 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 { 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 { ElMessage } from 'element-plus'
import { isMobile } from 'vue3-common/utils/layoutUtil'
export const useBillStore = defineStore('bill', { export const useBillStore = defineStore('bill', {
state: () => ({ state: () => ({
@@ -35,7 +34,7 @@ export const useBillStore = defineStore('bill', {
billPayList: [] as IBillPay[], billPayList: [] as IBillPay[],
billBookList: [] as IBillBook[], billBookList: [] as IBillBook[],
billCategoryList: [] as IBillCategory[], billCategoryList: [] as IBillCategory[],
billType: 'expensive' as IBillType, billType: 'expense' as IBillType,
billDateType: 'month' as IDateType, billDateType: 'month' as IDateType,
billDateList: [] as string[], billDateList: [] as string[],
billImagePath: ImagePathEnum.BILL, billImagePath: ImagePathEnum.BILL,
@@ -53,11 +52,7 @@ export const useBillStore = defineStore('bill', {
billBookStatsList: [] as IStatsChart[], billBookStatsList: [] as IStatsChart[],
billCategoryStatsList: [] as IStatsChart[], billCategoryStatsList: [] as IStatsChart[],
billApiType: 'ADD' as IHandleApi, billApiType: 'ADD' as IHandleApi,
billDialog: { billDrawer: false,
title: '',
visible: false,
data: {}
} as IDialog<IBillRecord>,
isRefresh: false, isRefresh: false,
isLoading: false isLoading: false
}), }),
@@ -124,11 +119,7 @@ export const useBillStore = defineStore('bill', {
default: default:
break break
} }
this.billDialog.visible = false
if (!isMobile()) {
await this.refreshInfo() await this.refreshInfo()
}
}, },
getBillDateList() { getBillDateList() {
const { billYear, billMonth } = this.billQueryForm const { billYear, billMonth } = this.billQueryForm
@@ -140,11 +131,12 @@ export const useBillStore = defineStore('bill', {
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
this.getBillDateList() this.getBillDateList()
await this.queryBillSummary(bookName)
if (isStats) { if (isStats) {
await this.queryBillStats(bookName) await this.queryBillStats(bookName)
} else {
await this.queryBillSummary(bookName)
} }
this.isRefresh = !this.isRefresh this.isRefresh = !this.isRefresh
this.isLoading = false this.isLoading = false
}, },
@@ -159,7 +151,7 @@ export const useBillStore = defineStore('bill', {
bookName: '', bookName: '',
category: '', category: '',
content: '', content: '',
date: '', date: formatDate(new Date()),
location: '', location: '',
payAccount: '', payAccount: '',
remark: '', remark: '',

62
src/store/car.ts Normal file
View File

@@ -0,0 +1,62 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { ImagePathEnum } from '@/types'
import { ICarEvent, ICarEventQuery } from '@/types/car.ts'
import { addCarEventApi, deleteCarEventApi, queryCarEventListApi, updateCarEventApi } from '@/apis/car.ts'
export const useCarStore = defineStore('car', {
state: () => ({
carEventList: [] as ICarEvent[],
queryInfo: {
type: ''
} as ICarEventQuery,
carEventApiType: 'ADD' as IHandleApi,
currentCarEvent: {
} as ICarEvent,
carImagePath: ImagePathEnum.CAR,
isScrollEnd: false,
isRefresh: false
}),
actions: {
async queryCarEventList() {
this.carEventList = await queryCarEventListApi(this.queryInfo)
},
async handleCarEventApi(carEvent: ICarEvent) {
switch (this.carEventApiType) {
case 'ADD':
await addCarEventApi(carEvent)
ElMessage.success('新增汽车事件成功')
break
case 'UPDATE':
await updateCarEventApi(carEvent.id as number, carEvent)
ElMessage.success('更新汽车事件成功')
break
case 'DELETE':
await deleteCarEventApi(carEvent.id as number)
ElMessage.success('删除汽车事件成功')
break
default:
break
}
},
async refreshInfo() {
await this.queryCarEventList()
this.isRefresh = !this.isRefresh
},
getEmptyCarEvent(): ICarEvent {
return {
cost: 0,
date: '',
description: '',
imageUrl: '',
location: '',
mileage: 0,
name: '',
remark: '',
type: ''
}
}
}
})

View File

@@ -13,8 +13,6 @@ import type { IDialog } from 'vue3-common/types'
import { IHandleApi } from 'vue3-common/types' import { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil' import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
import { isLargeScreen } from '@/utils'
import { isMobile } from 'vue3-common/utils/layoutUtil'
export const useFoodStore = defineStore('food', { export const useFoodStore = defineStore('food', {
state: () => ({ state: () => ({
@@ -50,7 +48,8 @@ export const useFoodStore = defineStore('food', {
} as IFoodRecord, } as IFoodRecord,
foodNameList: [] as string[], foodNameList: [] as string[],
queryRecipe: { queryRecipe: {
category: '' category: '',
name: ''
}, },
queryRecord: { queryRecord: {
year: getCurrentYear(), year: getCurrentYear(),
@@ -76,7 +75,7 @@ export const useFoodStore = defineStore('food', {
rateColorList: ['#E2DEDC', '#F7BA2A', '#009FA8'], rateColorList: ['#E2DEDC', '#F7BA2A', '#009FA8'],
pageInfo: { pageInfo: {
currentPage: 1, currentPage: 1,
pageSize: isLargeScreen() ? 6 : 2, pageSize: 3,
totalSize: 1 totalSize: 1
}, },
isScrollEnd: false, isScrollEnd: false,
@@ -157,10 +156,6 @@ export const useFoodStore = defineStore('food', {
break break
} }
this.foodRecordDialog.visible = false this.foodRecordDialog.visible = false
if (!isMobile()) {
await this.refreshInfo()
}
}, },
async refreshInfo() { async refreshInfo() {
await this.queryFoodRecipeList() await this.queryFoodRecipeList()

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { IDialog, IHandleApi, IStatsChart } from 'vue3-common/types' import type { IHandleApi, IStatsChart } from 'vue3-common/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { IJournal, IJournalQuery } from '@/types/journal.ts' import { IJournal, IJournalQuery } from '@/types/journal.ts'
import { addJournalApi, deleteJournalApi, queryJournalApi, updateJournalApi } from '@/apis/journal.ts' import { addJournalApi, deleteJournalApi, queryJournalApi, updateJournalApi } from '@/apis/journal.ts'
@@ -28,11 +28,7 @@ export const useJournalStore = defineStore('journal', {
dateJournalList: [] as IJournal[], dateJournalList: [] as IJournal[],
journalImagePath: ImagePathEnum.JOURNAL, journalImagePath: ImagePathEnum.JOURNAL,
journalVideoPath: ImagePathEnum.JOURNAL, journalVideoPath: ImagePathEnum.JOURNAL,
journalDialog: { journalDrawer: false,
visible: false,
title: '',
data: {}
} as IDialog<IJournal>,
isRefresh: false isRefresh: false
}), }),
actions: { actions: {
@@ -56,7 +52,6 @@ export const useJournalStore = defineStore('journal', {
default: default:
break break
} }
this.journalDialog.visible = false
await this.refreshInfo() await this.refreshInfo()
}, },
async refreshInfo() { async refreshInfo() {

View File

@@ -54,7 +54,6 @@ export const usePasswordStore = defineStore('password', {
default: default:
break break
} }
await this.refreshInfo()
}, },
async refreshInfo() { async refreshInfo() {
await this.queryPasswordList() await this.queryPasswordList()

View File

@@ -36,6 +36,7 @@ export const useProductStore = defineStore('product', {
pageSize: 3, pageSize: 3,
totalSize: 1 totalSize: 1
}, },
productDrawer: false,
isScrollEnd: false, isScrollEnd: false,
isRefresh: false isRefresh: false
}), }),

View File

@@ -12,7 +12,6 @@ import {
queryTravelSpotNameListApi, queryTravelStatsApi, queryTravelSpotNameListApi, queryTravelStatsApi,
updateTravelRecordApi, updateTravelSpotApi updateTravelRecordApi, updateTravelSpotApi
} from '@/apis/travel' } from '@/apis/travel'
import { isMobile } from 'vue3-common/utils/layoutUtil'
export const useTravelStore = defineStore('travel', { export const useTravelStore = defineStore('travel', {
state: () => ({ state: () => ({
@@ -151,10 +150,6 @@ export const useTravelStore = defineStore('travel', {
break break
} }
this.travelRecordDialog.visible = false this.travelRecordDialog.visible = false
if (!isMobile()) {
await this.refreshInfo()
}
}, },
async refreshInfo() { async refreshInfo() {
await this.queryTravelSpotList() await this.queryTravelSpotList()

View File

@@ -1,43 +0,0 @@
.mobile-bill-view {
display: flex;
flex-direction: column;
gap: 10px;
.el-date-editor {
align-self: center;
}
.el-switch {
.el-switch__core {
height: 32px;
width: 80px;
}
}
.bill-setting {
display: flex;
flex-direction: column;
gap: 10px;
.button-container {
align-self: center;
display: flex;
gap: 10px;
}
}
.bill-search-list {
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.search-result-title {
align-self: flex-end;
display: flex;
gap: 10px;
}
}
}

View File

@@ -1,20 +0,0 @@
.mobile-food-view {
display: flex;
flex-direction: column;
gap: 10px;
.food-list {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
//align-items: center;
gap: 1vh;
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
}

View File

@@ -1,39 +0,0 @@
.food-view {
.food-list {
background-color: #F5F5F5;
flex-grow: 1;
overflow: auto;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
//align-items: center;
gap: 1vh;
padding: 10px;
.list {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 2vw;
row-gap: 2vh;
}
@media (min-width: 1280px) and (max-width: 1500px) {
.list {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 1280px) {
.list {
grid-template-columns: repeat(1, 1fr);
}
}
.el-pagination {
align-self: center;
}
}
}

View File

@@ -11,6 +11,7 @@
} }
section { section {
width: 100%;
.info-container, .record-container { .info-container, .record-container {
.info-item { .info-item {
padding: 1vh 1vw; padding: 1vh 1vw;
@@ -98,7 +99,7 @@
margin-left: 1vw; margin-left: 1vw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; // justify-content: space-between;
gap: 1vh; gap: 1vh;
min-height: 28vh; min-height: 28vh;

View File

@@ -1,28 +0,0 @@
.mobile-plan-view {
display: flex;
flex-direction: column;
gap: 10px;
#planCalendar {
.fc-view {
height: calc(100vh - 210px);
.fc-timegrid-body {
// 单元格高度
tr {
height: 3.5vh;
}
}
}
}
.plan-list {
.el-scrollbar {
.el-scrollbar__view {
display: flex;
flex-direction: column;
gap: 2vh;
}
}
}
}

View File

@@ -1,13 +0,0 @@
.plan-view {
.plan-list {
height: calc(100vh - 120px);
.el-scrollbar {
.el-scrollbar__view {
display: flex;
flex-direction: column;
gap: 2vh;
}
}
}
}

View File

@@ -1,20 +0,0 @@
.mobile-travel-view {
display: flex;
flex-direction: column;
gap: 10px;
.travel-list {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
//align-items: center;
gap: 1vh;
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
}

View File

@@ -82,4 +82,4 @@ span, input {
font-style: normal; font-style: normal;
} }
@import "//at.alicdn.com/t/c/font_4793264_jis1w9io0s8.css"; @import "//at.alicdn.com/t/c/font_4793264_pi76sk0tqgd.css";

View File

@@ -23,12 +23,32 @@ $tabBar-height: 70px;
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
padding: 10px; padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.query-date {
align-self: center;
}
.content-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.form-button {
align-self: center;
}
} }
.common-query { .common-query {
width: 100%;
align-self: center; align-self: center;
display: flex; display: flex;
justify-content: space-around;
gap: 10px; gap: 10px;
} }
@@ -223,4 +243,19 @@ $tabBar-height: 70px;
border-radius: 10px; border-radius: 10px;
// box-shadow: 3px 3px 3px #CFCFCF; // box-shadow: 3px 3px 3px #CFCFCF;
} }
.form-item {
padding: 10px;
background-color: var(--color-card-bg);
border: 1px solid var(--color-border-bg);
border-radius: 10px;
}
.el-drawer {
border-radius: 16px 16px 0 0;
.el-drawer__body {
padding: 0;
}
}
} }

16
src/types/agent.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface IChatMessage {
role: 'user' | 'ai';
content: string;
}
export interface IAgentMessage {
username: string;
message: string;
thread_id: string;
}
export interface ISessionMessage {
username: string;
title: string;
thread_id: string;
}

14
src/types/award.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface IAward {
id: number;
name: string;
type: string;
owner: string;
location: string;
date: string;
imageList: string[];
remark: string;
}
export interface IAwardQuery {
type: string;
}

View File

@@ -1,4 +1,4 @@
export type IBillType = 'expensive' | 'income' | '' export type IBillType = 'expense' | 'income' | ''
/** /**
* 账单记录 * 账单记录
@@ -68,12 +68,12 @@ export interface IBillSearch {
export interface IBillCalendarDaily { export interface IBillCalendarDaily {
dailyRecordList: IBillRecord[]; dailyRecordList: IBillRecord[];
totalExpensive: number; totalExpense: number;
totalIncome: number; totalIncome: number;
} }
export interface IBillSummaryStats { export interface IBillSummaryStats {
expensive: number; expense: number;
income: number; income: number;
balance: number; balance: number;
rate: number; rate: number;

16
src/types/car.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface ICarEvent {
id?: number;
name: string;
type: string;
date: string;
mileage: number;
cost: number;
location: string;
description: string;
imageUrl: string;
remark: string;
}
export interface ICarEventQuery {
type: string;
}

View File

@@ -6,7 +6,9 @@ export enum ImagePathEnum {
PRODUCT = 'product', PRODUCT = 'product',
JOURNAL = 'journal', JOURNAL = 'journal',
LEDGER = 'ledger', LEDGER = 'ledger',
USER = 'user' AWARD = 'award',
USER = 'user',
CAR = 'car',
} }
export interface IPageRecord<T> { export interface IPageRecord<T> {

View File

@@ -15,18 +15,10 @@ export interface IProduct {
* 到期日期 * 到期日期
*/ */
expiryDate: string; expiryDate: string;
/**
* 总天数
*/
totalDay?: number;
/** /**
* 金额 * 金额
*/ */
price: number; price: number;
/**
* 日平均价格
*/
averagePrice?: number;
/** /**
* 分类 * 分类
*/ */

View File

@@ -1,3 +1,16 @@
/**
* 旅行景点
*/
export interface ITravelRecord {
id: number;
name: string;
area: string[];
date: string;
persons: string[];
imageList: string[];
videoList: string[];
}
/** /**
* 景点信息 * 景点信息
*/ */
@@ -16,19 +29,6 @@ export interface ITravelSpot {
recordList: ITravelRecord[]; recordList: ITravelRecord[];
} }
/**
* 旅行景点
*/
export interface ITravelRecord {
id: number;
name: string;
area: string[];
date: string;
persons: string[];
imageList: string[];
videoList: string[];
}
/** /**
* 统计信息 * 统计信息
*/ */

View File

@@ -0,0 +1,47 @@
interface RGB {
r: number;
g: number;
b: number;
}
/**
* 十六进制颜色转RGB对象
*/
export const hexToRgb = (hex: string): RGB | null => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
/**
* 计算变亮后的颜色
*/
export const calcLighten = (baseColor: string, ratio: number): string => {
const rgb = hexToRgb(baseColor)
if (!rgb) return baseColor
const actualRatio = ratio / 100
const r = rgb.r * (1 - actualRatio) + 255 * actualRatio
const g = rgb.g * (1 - actualRatio) + 255 * actualRatio
const b = rgb.b * (1 - actualRatio) + 255 * actualRatio
return `rgb(${r}, ${g}, ${b})`
}
/**
* 计算变暗后的颜色
*/
export const calcDarken = (baseColor: string, ratio: number): string => {
const rgb = hexToRgb(baseColor)
if (!rgb) return baseColor
const actualRatio = ratio / 100
const r = rgb.r * (1 - actualRatio)
const g = rgb.g * (1 - actualRatio)
const b = rgb.b * (1 - actualRatio)
return `rgb(${r}, ${g}, ${b})`
}

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