Compare commits
50 Commits
930202a6b6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a71bef851 | |||
| 8ecc38befd | |||
| e205d1ddf4 | |||
| be05e4695c | |||
| 4b5f853603 | |||
| 6ff52e9d50 | |||
| 3535d33c4b | |||
| 0ec0bba2fa | |||
| 98f2842869 | |||
| f7e98f2a28 | |||
| b9f4936dcc | |||
| 3201a1c32b | |||
| 6aed1854ca | |||
| f8a605b8c0 | |||
| 08999c608d | |||
| 7eeac7c465 | |||
| 420f250bac | |||
| 3e59b58f84 | |||
| ac914f6c05 | |||
| 11887bf3d5 | |||
| 1fb76fc220 | |||
| d7d167f3e2 | |||
| 5b7dcd614a | |||
| 4dc541da85 | |||
| 223a59cd8a | |||
| 2fbae7f80f | |||
| 8940b3972a | |||
| 1b8bd5a7e2 | |||
| 45342fb55c | |||
| e5e1e5e2b3 | |||
| 94270b923c | |||
| aa7abbdde3 | |||
| 7b96e60b8d | |||
| 71639cad51 | |||
| d87732dbf7 | |||
| c0cfbbaa15 | |||
| f5e75b745f | |||
| 4d7946759c | |||
| 2bcc61fdaa | |||
| 63a1e126ad | |||
| f62613205c | |||
| 5ec0781a53 | |||
| e875a8e2d5 | |||
| 7032070d3f | |||
| ee20c19b72 | |||
| 5f3a7ca5b4 | |||
| ed0c8e6664 | |||
| 355a6c256d | |||
| 9e0b8ead79 | |||
| cc33d3028b |
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_ALIST_PATH=/local/test
|
||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_ALIST_PATH=/local
|
||||
114
components.d.ts
vendored
Normal file
114
components.d.ts
vendored
Normal 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']
|
||||
}
|
||||
}
|
||||
14
nginx.conf
14
nginx.conf
@@ -8,6 +8,8 @@ server {
|
||||
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 500M;
|
||||
|
||||
location /sweet-hut/ {
|
||||
alias /usr/share/nginx/html/;
|
||||
try_files $uri $uri/ /sweet-hut/index.html;
|
||||
@@ -17,10 +19,22 @@ server {
|
||||
proxy_pass http://host.docker.internal:8081/;
|
||||
}
|
||||
|
||||
location /alist-api/ {
|
||||
proxy_pass http://host.docker.internal:5244/;
|
||||
}
|
||||
|
||||
location /blog-api/ {
|
||||
proxy_pass http://host.docker.internal:8082/;
|
||||
}
|
||||
|
||||
location /image-api/ {
|
||||
proxy_pass http://host.docker.internal:3000/;
|
||||
}
|
||||
|
||||
location /agent-api/ {
|
||||
proxy_pass http://host.docker.internal:8000/;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
18
package.json
18
package.json
@@ -2,6 +2,7 @@
|
||||
"name": "sweet-hut-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
@@ -23,7 +24,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts-liquidfill": "^3.1.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "~2.9.7",
|
||||
"element-plus": "^2.9.7",
|
||||
"file-icons-vue": "^1.3.4",
|
||||
"font-awesome": "^4.7.0",
|
||||
"lunar-javascript": "^1.7.2",
|
||||
@@ -31,6 +32,7 @@
|
||||
"pinia": "^2.3.1",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vant": "^4.9.22",
|
||||
"vue": "3.5.12",
|
||||
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common",
|
||||
"vue3-emoji-picker": "^1.1.8",
|
||||
@@ -38,22 +40,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.19.86",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vant/auto-import-resolver": "^1.3.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"postcss": "^8.5.3",
|
||||
"sass": "^1.86.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "5.2.0",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-mock": "^3.0.2",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^1.8.27"
|
||||
|
||||
350
pnpm-lock.yaml
generated
350
pnpm-lock.yaml
generated
@@ -54,7 +54,7 @@ importers:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ~2.9.7
|
||||
specifier: ^2.9.7
|
||||
version: 2.9.7(vue@3.5.12(typescript@5.8.3))
|
||||
file-icons-vue:
|
||||
specifier: ^1.3.4
|
||||
@@ -71,9 +71,15 @@ importers:
|
||||
pinia:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3))
|
||||
ua-parser-js:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.6
|
||||
v-calendar:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(@popperjs/core@2.11.8)(vue@3.5.12(typescript@5.8.3))
|
||||
vant:
|
||||
specifier: ^4.9.22
|
||||
version: 4.9.22(vue@3.5.12(typescript@5.8.3))
|
||||
vue:
|
||||
specifier: 3.5.12
|
||||
version: 3.5.12(typescript@5.8.3)
|
||||
@@ -90,36 +96,15 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^18.19.86
|
||||
version: 18.19.86
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^5.62.0
|
||||
version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^5.62.0
|
||||
version: 5.62.0(eslint@7.32.0)(typescript@5.8.3)
|
||||
'@vant/auto-import-resolver':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.2.1
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.21
|
||||
version: 10.4.21(postcss@8.5.3)
|
||||
eslint:
|
||||
specifier: ^7.32.0
|
||||
version: 7.32.0
|
||||
eslint-config-standard:
|
||||
specifier: ^16.0.3
|
||||
version: 16.0.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0)
|
||||
eslint-plugin-import:
|
||||
specifier: ^2.31.0
|
||||
version: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)
|
||||
eslint-plugin-node:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0(eslint@7.32.0)
|
||||
eslint-plugin-promise:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(eslint@7.32.0)
|
||||
eslint-plugin-vue:
|
||||
specifier: ^8.7.1
|
||||
version: 8.7.1(eslint@7.32.0)
|
||||
postcss:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3
|
||||
@@ -132,11 +117,14 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
unplugin-vue-components:
|
||||
specifier: ^32.1.0
|
||||
version: 32.1.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))
|
||||
vfonts:
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3
|
||||
vite:
|
||||
specifier: 5.2.0
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(@types/node@18.19.86)(sass@1.86.3)
|
||||
vite-plugin-mock:
|
||||
specifier: ^3.0.2
|
||||
@@ -489,6 +477,9 @@ packages:
|
||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -500,6 +491,9 @@ packages:
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
@@ -996,6 +990,17 @@ packages:
|
||||
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@vant/auto-import-resolver@1.3.0':
|
||||
resolution: {integrity: sha512-lJyWtCyFizR4bHZvMiNMF3w+WTFTUWAvka1eqTnPK9ticUcKTCOx6qEmHcm8JPb3g1t3GaD2W3MnHkBp/nHamw==}
|
||||
|
||||
'@vant/popperjs@1.3.0':
|
||||
resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
|
||||
|
||||
'@vant/use@1.6.0':
|
||||
resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
'@vavt/util@2.1.0':
|
||||
resolution: {integrity: sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==}
|
||||
|
||||
@@ -1064,6 +1069,9 @@ packages:
|
||||
'@vue/shared@3.5.13':
|
||||
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||
|
||||
'@vue/shared@3.5.28':
|
||||
resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==}
|
||||
|
||||
'@vueuse/core@13.1.0':
|
||||
resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==}
|
||||
peerDependencies:
|
||||
@@ -1096,8 +1104,13 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.14.1:
|
||||
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.18.0:
|
||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1337,6 +1350,10 @@ packages:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chokidar@5.0.0:
|
||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
class-utils@0.3.6:
|
||||
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1396,6 +1413,12 @@ packages:
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
|
||||
confbox@0.2.4:
|
||||
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||
|
||||
connect@3.7.0:
|
||||
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@@ -1530,6 +1553,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
detect-europe-js@0.1.2:
|
||||
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -1829,6 +1855,9 @@ packages:
|
||||
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
exsolve@1.1.1:
|
||||
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1860,6 +1889,15 @@ packages:
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@@ -2228,6 +2266,9 @@ packages:
|
||||
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-standalone-pwa@0.1.1:
|
||||
resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==}
|
||||
|
||||
is-string@1.1.1:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2354,6 +2395,10 @@ packages:
|
||||
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
local-pkg@1.2.1:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
@@ -2385,6 +2430,9 @@ packages:
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
map-cache@0.2.2:
|
||||
resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2472,6 +2520,9 @@ packages:
|
||||
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
mlly@1.8.2:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
mockjs@1.1.0:
|
||||
resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==}
|
||||
hasBin: true
|
||||
@@ -2567,6 +2618,10 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obug@2.1.4:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
on-finished@2.3.0:
|
||||
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2625,6 +2680,9 @@ packages:
|
||||
pathe@0.2.0:
|
||||
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -2632,6 +2690,10 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@4.0.5:
|
||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2649,6 +2711,12 @@ packages:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
pkg-types@2.3.1:
|
||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||
|
||||
posix-character-classes@0.1.1:
|
||||
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2750,6 +2818,9 @@ packages:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
query-string@4.3.4:
|
||||
resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2772,6 +2843,10 @@ packages:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
readdirp@5.0.0:
|
||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3074,6 +3149,10 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
to-object-path@0.3.0:
|
||||
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3148,9 +3227,19 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ua-is-frozen@0.1.2:
|
||||
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
|
||||
|
||||
ua-parser-js@2.0.6:
|
||||
resolution: {integrity: sha512-EmaxXfltJaDW75SokrY4/lXMrVyXomE/0FpIIqP2Ctic93gK7rlme55Cwkz8l3YZ6gqf94fCU7AnIkidd/KXPg==}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
ufo@1.6.4:
|
||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3170,6 +3259,53 @@ packages:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
unplugin-utils@0.3.2:
|
||||
resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
unplugin-vue-components@32.1.0:
|
||||
resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': ^3.2.2 || ^4.0.0
|
||||
vue: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
|
||||
unplugin@3.3.0:
|
||||
resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@farmfe/core': '*'
|
||||
'@rspack/core': '*'
|
||||
bun-types-no-globals: '*'
|
||||
esbuild: '*'
|
||||
rolldown: '*'
|
||||
rollup: '*'
|
||||
unloader: '*'
|
||||
vite: '*'
|
||||
webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
'@farmfe/core':
|
||||
optional: true
|
||||
'@rspack/core':
|
||||
optional: true
|
||||
bun-types-no-globals:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
rolldown:
|
||||
optional: true
|
||||
rollup:
|
||||
optional: true
|
||||
unloader:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
|
||||
unset-value@1.0.0:
|
||||
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3207,6 +3343,11 @@ packages:
|
||||
v8-compile-cache@2.4.0:
|
||||
resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
|
||||
|
||||
vant@4.9.22:
|
||||
resolution: {integrity: sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -3339,6 +3480,9 @@ packages:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3822,12 +3966,19 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/set-array@1.2.1': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
@@ -4376,6 +4527,14 @@ snapshots:
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@vant/auto-import-resolver@1.3.0': {}
|
||||
|
||||
'@vant/popperjs@1.3.0': {}
|
||||
|
||||
'@vant/use@1.6.0(vue@3.5.12(typescript@5.8.3))':
|
||||
dependencies:
|
||||
vue: 3.5.12(typescript@5.8.3)
|
||||
|
||||
'@vavt/util@2.1.0': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.1(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3))':
|
||||
@@ -4481,6 +4640,8 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.13': {}
|
||||
|
||||
'@vue/shared@3.5.28': {}
|
||||
|
||||
'@vueuse/core@13.1.0(vue@3.5.12(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
@@ -4517,13 +4678,15 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 7.4.1
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.14.1):
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
acorn: 8.15.0
|
||||
|
||||
acorn@7.4.1: {}
|
||||
|
||||
acorn@8.14.1: {}
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.18.0: {}
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
@@ -4802,6 +4965,10 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chokidar@5.0.0:
|
||||
dependencies:
|
||||
readdirp: 5.0.0
|
||||
|
||||
class-utils@0.3.6:
|
||||
dependencies:
|
||||
arr-union: 3.1.0
|
||||
@@ -4861,6 +5028,10 @@ snapshots:
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
confbox@0.1.8: {}
|
||||
|
||||
confbox@0.2.4: {}
|
||||
|
||||
connect@3.7.0:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@@ -4989,6 +5160,8 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-europe-js@0.1.2: {}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
optional: true
|
||||
|
||||
@@ -5384,8 +5557,8 @@ snapshots:
|
||||
|
||||
espree@9.6.1:
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
acorn-jsx: 5.3.2(acorn@8.14.1)
|
||||
acorn: 8.15.0
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
esprima@4.0.1: {}
|
||||
@@ -5420,6 +5593,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
exsolve@1.1.1: {}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
@@ -5462,6 +5637,10 @@ snapshots:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.5):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.5
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
@@ -5847,6 +6026,8 @@ snapshots:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
||||
is-standalone-pwa@0.1.1: {}
|
||||
|
||||
is-string@1.1.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -5961,6 +6142,12 @@ snapshots:
|
||||
emojis-list: 3.0.0
|
||||
json5: 1.0.2
|
||||
|
||||
local-pkg@1.2.1:
|
||||
dependencies:
|
||||
mlly: 1.8.2
|
||||
pkg-types: 2.3.1
|
||||
quansync: 0.2.11
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
|
||||
@@ -5985,6 +6172,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
map-cache@0.2.2: {}
|
||||
|
||||
map-visit@1.0.0:
|
||||
@@ -6087,6 +6278,13 @@ snapshots:
|
||||
for-in: 1.0.2
|
||||
is-extendable: 1.0.1
|
||||
|
||||
mlly@1.8.2:
|
||||
dependencies:
|
||||
acorn: 8.18.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.4
|
||||
|
||||
mockjs@1.1.0:
|
||||
dependencies:
|
||||
commander: 13.1.0
|
||||
@@ -6191,6 +6389,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
obug@2.1.4: {}
|
||||
|
||||
on-finished@2.3.0:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
@@ -6243,10 +6443,14 @@ snapshots:
|
||||
|
||||
pathe@0.2.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
picomatch@4.0.5: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pinia@2.3.1(typescript@5.8.3)(vue@3.5.12(typescript@5.8.3)):
|
||||
@@ -6261,6 +6465,18 @@ snapshots:
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
mlly: 1.8.2
|
||||
pathe: 2.0.3
|
||||
|
||||
pkg-types@2.3.1:
|
||||
dependencies:
|
||||
confbox: 0.2.4
|
||||
exsolve: 1.1.1
|
||||
pathe: 2.0.3
|
||||
|
||||
posix-character-classes@0.1.1: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
@@ -6352,6 +6568,8 @@ snapshots:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
query-string@4.3.4:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
@@ -6375,6 +6593,8 @@ snapshots:
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
readdirp@5.0.0: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -6780,6 +7000,11 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.5)
|
||||
picomatch: 4.0.5
|
||||
|
||||
to-object-path@0.3.0:
|
||||
dependencies:
|
||||
kind-of: 3.2.2
|
||||
@@ -6878,8 +7103,18 @@ snapshots:
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
ua-is-frozen@0.1.2: {}
|
||||
|
||||
ua-parser-js@2.0.6:
|
||||
dependencies:
|
||||
detect-europe-js: 0.1.2
|
||||
is-standalone-pwa: 0.1.1
|
||||
ua-is-frozen: 0.1.2
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ufo@1.6.4: {}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -6900,6 +7135,44 @@ snapshots:
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
unplugin-utils@0.3.2:
|
||||
dependencies:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.5
|
||||
|
||||
unplugin-vue-components@32.1.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))(vue@3.5.12(typescript@5.8.3)):
|
||||
dependencies:
|
||||
chokidar: 5.0.0
|
||||
local-pkg: 1.2.1
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.2
|
||||
obug: 2.1.4
|
||||
picomatch: 4.0.5
|
||||
tinyglobby: 0.2.17
|
||||
unplugin: 3.3.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3))
|
||||
unplugin-utils: 0.3.2
|
||||
vue: 3.5.12(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- '@farmfe/core'
|
||||
- '@rspack/core'
|
||||
- bun-types-no-globals
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- unloader
|
||||
- vite
|
||||
- webpack
|
||||
|
||||
unplugin@3.3.0(esbuild@0.20.2)(rollup@4.40.0)(vite@5.2.0(@types/node@18.19.86)(sass@1.86.3)):
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
picomatch: 4.0.5
|
||||
webpack-virtual-modules: 0.6.2
|
||||
optionalDependencies:
|
||||
esbuild: 0.20.2
|
||||
rollup: 4.40.0
|
||||
vite: 5.2.0(@types/node@18.19.86)(sass@1.86.3)
|
||||
|
||||
unset-value@1.0.0:
|
||||
dependencies:
|
||||
has-value: 0.3.1
|
||||
@@ -6936,6 +7209,13 @@ snapshots:
|
||||
|
||||
v8-compile-cache@2.4.0: {}
|
||||
|
||||
vant@4.9.22(vue@3.5.12(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@vant/popperjs': 1.3.0
|
||||
'@vant/use': 1.6.0(vue@3.5.12(typescript@5.8.3))
|
||||
'@vue/shared': 3.5.28
|
||||
vue: 3.5.12(typescript@5.8.3)
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vfonts@0.0.3: {}
|
||||
@@ -7072,6 +7352,8 @@ snapshots:
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
whatwg-mimetype@3.0.0: {}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
35
src/apis/award.ts
Normal file
35
src/apis/award.ts
Normal 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
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
IBillCategory,
|
||||
IBillPay,
|
||||
IBillQuery,
|
||||
IBillRecord
|
||||
IBillRecord, IBillSearch
|
||||
} from '@/types/bill'
|
||||
import { IStatsChart } from 'vue3-common/dist/types'
|
||||
|
||||
@@ -41,6 +41,13 @@ export const queryBillSummaryApi = (query: IBillQuery): Promise<IBillRecord[]> =
|
||||
params: query
|
||||
})
|
||||
|
||||
export const searchBillSummaryApi = (search: IBillSearch): Promise<IBillRecord[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/bill/search',
|
||||
method: 'get',
|
||||
params: search
|
||||
})
|
||||
|
||||
export const queryBillStatsApi = (type: string, query: IBillQuery): Promise<IStatsChart[]> =>
|
||||
cloudService({
|
||||
url: `/home-api/bill/stats/${type}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloudService } from './index'
|
||||
import { IBlog, IBlogCategory, IBlogComment, IBlogStats, INewBlog, IQueryBlog } from '@/types/blog'
|
||||
import { IBlog, IBlogCategory, IBlogComment, IBlogStats, IQueryBlog } from '@/types/blog'
|
||||
|
||||
import qs from 'qs'
|
||||
import { IPageRecord } from '@/types'
|
||||
@@ -23,7 +23,7 @@ export const queryBlogByConditionApi = (query: IQueryBlog): Promise<IBlog[]> =>
|
||||
|
||||
export const queryBlogByIdApi = (id: number): Promise<IBlog> =>
|
||||
cloudService({
|
||||
url: `/blog-api/blog/content/${id}`,
|
||||
url: `/blog-api/blog/${id}/content`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
@@ -47,33 +47,6 @@ export const queryLatestBlogApi = (): Promise<IBlog[]> =>
|
||||
|
||||
export const queryBlogCommentApi = (blogId: number): Promise<IBlogComment[]> =>
|
||||
cloudService({
|
||||
url: `/blog-api/blog/comment/${blogId}`,
|
||||
url: `/blog-api/blog/${blogId}/comment`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const addBlogCommentApi = (blogComment: IBlogComment): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: '/blog-api/blog/comment',
|
||||
method: 'put',
|
||||
data: blogComment
|
||||
})
|
||||
|
||||
export const addBlogApi = (blog: INewBlog): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: '/admin-api/blog-manage',
|
||||
method: 'post',
|
||||
data: blog
|
||||
})
|
||||
|
||||
export const updateBlogApi = (id: number, blog: INewBlog): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/admin-api/blog-manage/${id}`,
|
||||
method: 'put',
|
||||
data: blog
|
||||
})
|
||||
|
||||
export const deleteBlogApi = (id: number): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/admin-api/blog-manage/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { cloudService } from './index'
|
||||
import { IBudgetInfo } from '@/types/budget'
|
||||
|
||||
export const addBudgetApi = (budget: IBudgetInfo): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/home-api/budget',
|
||||
method: 'post',
|
||||
data: budget
|
||||
})
|
||||
|
||||
export const updateBudgetApi = (id: number, budget: IBudgetInfo): Promise<any> =>
|
||||
cloudService({
|
||||
url: `/home-api/budget/${id}`,
|
||||
method: 'put',
|
||||
data: budget
|
||||
})
|
||||
|
||||
export const deleteBudgetApi = (id: number): Promise<any> =>
|
||||
cloudService({
|
||||
url: `/home-api/budget/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
export const queryBudgetApi = (category: string): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/home-api/budget',
|
||||
method: 'get',
|
||||
params: { category }
|
||||
})
|
||||
|
||||
export const queryBudgetCategory = (): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/home-api/budget/category',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const queryBudgetRankStats = (category: string): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/home-api/budget/stats/rank',
|
||||
method: 'get',
|
||||
params: { category }
|
||||
})
|
||||
|
||||
export const queryBudgetProjectStats = (category: string): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/home-api/budget/stats/project',
|
||||
method: 'get',
|
||||
params: { category }
|
||||
})
|
||||
29
src/apis/car.ts
Normal file
29
src/apis/car.ts
Normal 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
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cloudService } from './index'
|
||||
import { IFileInfo } from '@/types/file'
|
||||
import type { AxiosProgressEvent } from 'axios'
|
||||
|
||||
export const uploadFileApi = (form: FormData, path: string, md5: string): Promise<any> =>
|
||||
cloudService({
|
||||
@@ -12,6 +13,19 @@ export const uploadFileApi = (form: FormData, path: string, md5: string): Promis
|
||||
data: form
|
||||
})
|
||||
|
||||
export const uploadAListFileApi = (form: FormData,
|
||||
progress: (event: AxiosProgressEvent) => void): Promise<string> =>
|
||||
cloudService({
|
||||
url: '/home-api/file/alist',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: progress,
|
||||
timeout: 0,
|
||||
method: 'post',
|
||||
data: form
|
||||
})
|
||||
|
||||
export const getFolderInfoApi = (folderName: string): Promise<IFileInfo[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/file/catalog',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cloudService } from './index'
|
||||
import { IFoodQuery, IFoodRecipe, IFoodRecord, IFoodStats } from '@/types/food'
|
||||
import { IPageRecord } from '@/types'
|
||||
|
||||
export const addFoodRecipeApi = (foodRecipe: IFoodRecipe): Promise<boolean> =>
|
||||
cloudService({
|
||||
@@ -21,20 +22,27 @@ export const deleteFoodRecipeApi = (id: number): Promise<boolean> =>
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
export const queryFoodRecipeByIdApi = (id: number): Promise<any> =>
|
||||
export const queryFoodRecipeByIdApi = (id: number): Promise<IFoodRecipe> =>
|
||||
cloudService({
|
||||
url: `/home-api/food/recipe/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const queryFoodRecipeApi = (currentPage: number, pageSize: number,
|
||||
query: IFoodQuery): Promise<any> =>
|
||||
query: IFoodQuery): Promise<IPageRecord<IFoodRecipe>> =>
|
||||
cloudService({
|
||||
url: '/home-api/food/recipe/page',
|
||||
method: 'get',
|
||||
params: { currentPage, pageSize, ...query }
|
||||
})
|
||||
|
||||
export const searchFoodRecipeApi = (name: string): Promise<IFoodRecipe> =>
|
||||
cloudService({
|
||||
url: '/home-api/food/recipe/search',
|
||||
method: 'get',
|
||||
params: { name }
|
||||
})
|
||||
|
||||
export const queryFoodNameListApi = (): Promise<string[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/food/recipe/name',
|
||||
|
||||
16
src/apis/health.ts
Normal file
16
src/apis/health.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { cloudService } from './index'
|
||||
import { IHealth, IHealthQuery } from '@/types/health'
|
||||
|
||||
export const updateHealthApi = (id: number, health: IHealth): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/home-api/health/${id}`,
|
||||
method: 'put',
|
||||
data: health
|
||||
})
|
||||
|
||||
export const queryHealthApi = (query: IHealthQuery): Promise<IHealth[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/health',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloudService } from './index'
|
||||
import { IPassword, IQueryPassword } from '@/types/password'
|
||||
import { IPassword, IPasswordQuery } from '@/types/password'
|
||||
|
||||
export const addPasswordApi = (password: IPassword): Promise<boolean> =>
|
||||
cloudService({
|
||||
@@ -21,7 +21,7 @@ export const deletePasswordApi = (id: number): Promise<boolean> =>
|
||||
method: 'delete'
|
||||
})
|
||||
|
||||
export const queryPasswordApi = (query: IQueryPassword): Promise<IPassword[]> =>
|
||||
export const queryPasswordApi = (query: IPasswordQuery): Promise<IPassword[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/password',
|
||||
method: 'get',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloudService } from './index'
|
||||
import { IPeriodDay, IPeriodSetting } from '@/types/period.ts'
|
||||
import { IMenstrualPeriod, IPeriodDay, IPeriodSetting } from '@/types/period.ts'
|
||||
|
||||
|
||||
export const updatePeriodDayApi = (id: number, periodDay: IPeriodDay): Promise<boolean> =>
|
||||
@@ -16,6 +16,12 @@ export const queryPeriodDayApi = (month: string): Promise<IPeriodDay[]> =>
|
||||
params: { month }
|
||||
})
|
||||
|
||||
export const getMenstrualDaysApi = (): Promise<IMenstrualPeriod[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/period//menstruate/days',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const getMenstruateFirstDateApi = (month: string): Promise<string> =>
|
||||
cloudService({
|
||||
url: '/home-api/period/menstruate',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloudService } from './index'
|
||||
import { IProduct, IQueryProduct } from '@/types/product.ts'
|
||||
import { IProduct, IProductQuery } from '@/types/product.ts'
|
||||
|
||||
export const addProductApi = (product: IProduct): Promise<boolean> =>
|
||||
cloudService({
|
||||
@@ -27,7 +27,7 @@ export const queryProductApi = (id: number): Promise<IProduct> =>
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const queryProductListApi = (query: IQueryProduct): Promise<IProduct[]> =>
|
||||
export const queryProductListApi = (query: IProductQuery): Promise<IProduct[]> =>
|
||||
cloudService({
|
||||
url: '/home-api/product',
|
||||
method: 'get',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="isMobile()" class="mobile-common-query card-item">
|
||||
<div class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>分类:</span>
|
||||
<el-segmented v-model="anniversaryStore.queryInfo.category"
|
||||
@@ -7,27 +7,11 @@
|
||||
@change="anniversaryStore.queryAnniversaryList()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="common-query">
|
||||
<el-select v-model="anniversaryStore.queryInfo.category"
|
||||
placeholder="请选择纪念日类别" clearable
|
||||
@change="anniversaryStore.queryAnniversaryList()"
|
||||
style="width: 180px">
|
||||
<el-option
|
||||
v-for="(item, index) in queryCategoryList"
|
||||
:key="index" :label="item.label" :value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-button :icon="Refresh" circle @click="refreshClick"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAnniversaryStore } from '@/store/anniversary.ts'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { ICategory } from '@/types'
|
||||
|
||||
const anniversaryStore = useAnniversaryStore()
|
||||
@@ -39,11 +23,6 @@ onMounted(async () => {
|
||||
initCategoryList()
|
||||
})
|
||||
|
||||
const refreshClick = async () => {
|
||||
anniversaryStore.queryInfo.category = ''
|
||||
await anniversaryStore.queryAnniversaryList()
|
||||
}
|
||||
|
||||
const initCategoryList = () => {
|
||||
queryCategoryList.value = []
|
||||
queryCategoryList.value.push({
|
||||
|
||||
@@ -12,6 +12,7 @@ import { defineProps, PropType } from 'vue'
|
||||
import { IAnniversaryCategory } from '@/types/anniversary.ts'
|
||||
import { useAnniversaryStore } from '@/store/anniversary.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
categoryItem: {
|
||||
@@ -28,8 +29,12 @@ const router = useRouter()
|
||||
const anniversaryStore = useAnniversaryStore()
|
||||
|
||||
const selectCategoryClick = () => {
|
||||
anniversaryStore.queryInfo.category = props.categoryItem?.name as string
|
||||
router.push({ name: 'AnniversaryList' })
|
||||
if (props.count) {
|
||||
anniversaryStore.queryInfo.category = props.categoryItem?.name as string
|
||||
router.push({ name: 'AnniversaryList' })
|
||||
} else {
|
||||
ElMessage.info('暂无纪念日')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="isMobile()" class="mobile-common-query card-item">
|
||||
<div class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>分类:</span>
|
||||
<el-segmented v-model="assetStore.queryInfo.category"
|
||||
@@ -7,27 +7,11 @@
|
||||
@change="assetStore.refreshInfo()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="common-query">
|
||||
<el-select v-model="assetStore.queryInfo.category"
|
||||
placeholder="请选择纪念日类别" clearable
|
||||
@change="assetStore.refreshInfo()"
|
||||
style="width: 180px">
|
||||
<el-option
|
||||
v-for="(item, index) in queryCategoryList"
|
||||
:key="index" :label="item.label" :value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-button :icon="Refresh" circle @click="refreshClick"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAssetStore } from '@/store/asset.ts'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { ICategory } from '@/types'
|
||||
|
||||
const assetStore = useAssetStore()
|
||||
@@ -39,11 +23,6 @@ onMounted(async () => {
|
||||
initCategoryList()
|
||||
})
|
||||
|
||||
const refreshClick = async () => {
|
||||
assetStore.queryInfo.category = ''
|
||||
await assetStore.queryAssetList()
|
||||
}
|
||||
|
||||
const initCategoryList = () => {
|
||||
queryCategoryList.value = []
|
||||
queryCategoryList.value.push({
|
||||
|
||||
97
src/components/Award/AwardCard.vue
Normal file
97
src/components/Award/AwardCard.vue
Normal 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>
|
||||
37
src/components/Award/AwardQuery.vue
Normal file
37
src/components/Award/AwardQuery.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -67,7 +67,7 @@ const getCalendarAttrDates = () => {
|
||||
})
|
||||
dailyInfo.attrs.push({
|
||||
dot: 'red',
|
||||
dates: getCalendarDateList(billStore.billRecordList, 'expensive')
|
||||
dates: getCalendarDateList(billStore.billRecordList, 'expense')
|
||||
})
|
||||
dailyInfo.attrs.push({
|
||||
dot: 'green',
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bill-chart"
|
||||
<div class="bill-chart common-chart"
|
||||
v-loading="billStore.isLoading"
|
||||
element-loading-text="正在加载中...">
|
||||
<div class="chart-container card-item">
|
||||
@@ -8,19 +8,22 @@
|
||||
</div>
|
||||
<div class="chart" ref="billDailyChartRef"/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊类别统计</span>
|
||||
</div>
|
||||
<div class="chart" ref="billCategoryChartRef"/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊账本统计</span>
|
||||
</div>
|
||||
<div class="chart" ref="billBookChartRef"/>
|
||||
</div>
|
||||
<div v-if="isMobile()" class="chart-container card-item">
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊排行榜</span>
|
||||
</div>
|
||||
@@ -28,12 +31,6 @@
|
||||
<bill-rank />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊排行榜</span>
|
||||
</div>
|
||||
<div class="chart" ref="billRankChartRef"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,8 +41,7 @@ import * as echarts from 'echarts'
|
||||
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
|
||||
import { useBillStore } from '@/store/bill.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import type { IStatsChart } from 'vue3-common/types'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
|
||||
const billStore = useBillStore()
|
||||
const appStore = useAppStore()
|
||||
@@ -53,12 +49,10 @@ const appStore = useAppStore()
|
||||
const billDailyChartRef = ref<HTMLElement>()
|
||||
const billBookChartRef = ref<HTMLElement>()
|
||||
const billCategoryChartRef = ref<HTMLElement>()
|
||||
const billRankChartRef = ref<HTMLElement>()
|
||||
|
||||
let billDailyChart: echarts.ECharts
|
||||
let billBookChart: echarts.ECharts
|
||||
let billCategoryChart: echarts.ECharts
|
||||
let billRankChart: echarts.ECharts
|
||||
|
||||
watch(() => billStore.isRefresh, () => {
|
||||
updateChart()
|
||||
@@ -68,24 +62,21 @@ onMounted(() => {
|
||||
billDailyChart = echarts.init(billDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
billBookChart = echarts.init(billBookChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
billCategoryChart = echarts.init(billCategoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
|
||||
if (!isMobile()) {
|
||||
billRankChart = echarts.init(billRankChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
}
|
||||
})
|
||||
|
||||
const updateChart = () => {
|
||||
const MAX_CATEGORY_COUNT = 5
|
||||
const MAX_RANK_COUNT = 10
|
||||
|
||||
// 收支统计图
|
||||
const dailyXAxis = billStore.billDateStatsList.map((item) => item.name)
|
||||
const dailyXAxis = billStore.billDateStatsList.map((item) => {
|
||||
return formatDate(item.name, 'MM-DD')
|
||||
})
|
||||
const dailyYAxis = billStore.billDateStatsList.map((item) => item.value)
|
||||
billDailyChart.setOption(lineChartOptions('日期', dailyXAxis, '金额(元)', dailyYAxis))
|
||||
billDailyChart.setOption({
|
||||
series: [{
|
||||
itemStyle: {
|
||||
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69'
|
||||
color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
|
||||
},
|
||||
markPoint: {
|
||||
label: {
|
||||
@@ -105,7 +96,7 @@ const updateChart = () => {
|
||||
billBookChart.setOption({
|
||||
series: [{
|
||||
itemStyle: {
|
||||
color: billStore.billType === 'expensive' ? '#CA6C65' : '#6FBB69'
|
||||
color: billStore.billType === 'expense' ? '#CA6C65' : '#6FBB69'
|
||||
},
|
||||
markPoint: {
|
||||
label: {
|
||||
@@ -119,29 +110,13 @@ const updateChart = () => {
|
||||
const billCategoryList = billStore.billCategoryStatsList.slice(0, MAX_CATEGORY_COUNT)
|
||||
billCategoryChart.setOption(pieChartOptions('rank', '元', billCategoryList))
|
||||
|
||||
if (isMobile()) {
|
||||
billCategoryChart.setOption({
|
||||
series: [{
|
||||
radius: ['20%', '50%'],
|
||||
label: {
|
||||
formatter: '{b}: {d}%'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 排行榜统计图
|
||||
const billRankList: IStatsChart[] = []
|
||||
const resultList = billStore.billRecordList.filter((item) => item.type === billStore.billType)
|
||||
resultList.sort((a, b) => b.amount - a.amount).forEach((item) => {
|
||||
billRankList.push({
|
||||
name: item.content,
|
||||
value: item.amount
|
||||
})
|
||||
billCategoryChart.setOption({
|
||||
series: [{
|
||||
radius: ['20%', '50%'],
|
||||
label: {
|
||||
formatter: '{b}: {d}%'
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
if (!isMobile()) {
|
||||
billRankChart.setOption(pieChartOptions('rank', '元', billRankList.slice(0, MAX_RANK_COUNT)))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<daily-summary :date="dailyItemInfo.date">
|
||||
<template #extra>
|
||||
<span>收入:{{ formatAmount(dailyItemInfo.totalIncome, true) }}</span>
|
||||
<span>支出:{{ formatAmount(dailyItemInfo.totalExpensive, true) }}</span>
|
||||
<span>支出:{{ formatAmount(dailyItemInfo.totalExpense, true) }}</span>
|
||||
</template>
|
||||
</daily-summary>
|
||||
|
||||
@@ -40,16 +40,12 @@ import DailySummary from '@/components/DailySummary.vue'
|
||||
import DailyCard from '@/components/DailyCard.vue'
|
||||
import { defineProps, onMounted, reactive, watch } from 'vue'
|
||||
import { useBillStore } from '@/store/bill.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatChineseDate } from 'vue3-common/utils/dateUtil'
|
||||
import { IBillRecord } from '@/types/bill.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
import { getCalendarDailyCount, getCategoryByName } from '@/utils/home/billUtil.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
const billStore = useBillStore()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
date: {
|
||||
@@ -72,7 +68,7 @@ onMounted(() => {
|
||||
|
||||
const dailyItemInfo = reactive({
|
||||
date: '',
|
||||
totalExpensive: 0,
|
||||
totalExpense: 0,
|
||||
totalIncome: 0,
|
||||
itemList: [] as IBillRecord[]
|
||||
})
|
||||
@@ -88,7 +84,7 @@ const getBillDailyByDate = (date: string) => {
|
||||
const calendarDaily = getCalendarDailyCount(billStore.billRecordList, date)
|
||||
dailyItemInfo.itemList = calendarDaily.dailyRecordList
|
||||
dailyItemInfo.totalIncome = calendarDaily.totalIncome
|
||||
dailyItemInfo.totalExpensive = calendarDaily.totalExpensive
|
||||
dailyItemInfo.totalExpense = calendarDaily.totalExpense
|
||||
} else {
|
||||
dailyItemInfo.itemList = []
|
||||
}
|
||||
@@ -101,19 +97,7 @@ const getBillDailyByDate = (date: string) => {
|
||||
const selectBillRecordClick = async (billRecord: IBillRecord) => {
|
||||
await billStore.queryBillRecord(billRecord.id as number)
|
||||
billStore.billApiType = 'UPDATE'
|
||||
|
||||
if (isMobile()) {
|
||||
await router.push({
|
||||
name: 'BillDetailId',
|
||||
params: { id: billStore.currentBillRecord.id }
|
||||
})
|
||||
} else {
|
||||
billStore.billDialog = {
|
||||
title: '编辑账单',
|
||||
visible: true,
|
||||
data: deepCopyObject(billStore.currentBillRecord)
|
||||
}
|
||||
}
|
||||
billStore.billDrawer = true
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
263
src/components/Bill/BillDrawer.vue
Normal file
263
src/components/Bill/BillDrawer.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="common-query">
|
||||
<el-select v-model="billStore.billQueryForm.billBook"
|
||||
@change="billStore.refreshInfo()" style="width: 120px">
|
||||
@change="billStore.refreshInfo(isStats)">
|
||||
<el-option
|
||||
v-for="(item, index) in billQuery.billBookList"
|
||||
:key="index" :label="item.label" :value="item.name"
|
||||
@@ -9,34 +9,32 @@
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="billStore.billDateType"
|
||||
@change="billStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
@change="billStore.refreshInfo(isStats)">
|
||||
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
|
||||
<el-switch
|
||||
v-model="billStore.billType"
|
||||
inline-prompt
|
||||
active-value="income" inactive-value="expensive"
|
||||
active-text="收入账单" inactive-text="支出账单"
|
||||
@change="billStore.refreshInfo()"
|
||||
style="--el-switch-on-color: #6FBB69; --el-switch-off-color: #CA6C65;"
|
||||
/>
|
||||
<el-select v-if="isStats" v-model="billStore.billType"
|
||||
@change="billStore.refreshInfo(isStats)">
|
||||
<el-option v-for="(item, index) in billTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { computed, onMounted, reactive } from 'vue'
|
||||
import { useBillStore } from '@/store/bill.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { IBillBook } from '@/types/bill.ts'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const billStore = useBillStore()
|
||||
const route = useRoute()
|
||||
|
||||
const isStats = computed(() => route.path.includes('chart'))
|
||||
|
||||
onMounted(async () => {
|
||||
await billStore.initBillInfo()
|
||||
|
||||
billQuery.billBookList = deepCopyObject(billStore.billBookList)
|
||||
billQuery.billBookList.push({
|
||||
color: '',
|
||||
@@ -56,6 +54,15 @@ const dateTypeOptions = [{
|
||||
value: 'year'
|
||||
}]
|
||||
|
||||
const billTypeOptions = [{
|
||||
label: '收入',
|
||||
value: 'income'
|
||||
},
|
||||
{
|
||||
label: '支出',
|
||||
value: 'expense'
|
||||
}]
|
||||
|
||||
const billQuery = reactive({
|
||||
query: '',
|
||||
billBookList: [] as IBillBook[]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:key="index" class="rank-item">
|
||||
<span class="rank-index">{{ index+1 }}.</span>
|
||||
<span class="rank-content">{{ item.content }}</span>
|
||||
<span class="rank-amount">¥{{ item.amount }}</span>
|
||||
<span class="rank-amount">¥{{ formatAmount(item.amount) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { useBillStore } from '@/store/bill.ts'
|
||||
import { getBillSortByAmount } from '@/utils/home/billUtil.ts'
|
||||
import { computed } from 'vue'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
|
||||
const billStore = useBillStore()
|
||||
|
||||
|
||||
34
src/components/Bill/BillSearch.vue
Normal file
34
src/components/Bill/BillSearch.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="common-query">
|
||||
<el-form :model="billStore.billSearchForm">
|
||||
<el-form-item label="账单内容">
|
||||
<el-input v-model="billStore.billSearchForm.content"
|
||||
@input="billStore.searchInfo()"
|
||||
placeholder="请输入账单内容" clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="账单类别">
|
||||
<el-input v-model="billStore.billSearchForm.category"
|
||||
@input="billStore.searchInfo()"
|
||||
placeholder="请输入账单类别" clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="账单地址">
|
||||
<el-input v-model="billStore.billSearchForm.location"
|
||||
@input="billStore.searchInfo()"
|
||||
placeholder="请输入账单地址" clearable/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useBillStore } from '@/store/bill.ts'
|
||||
|
||||
const billStore = useBillStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await billStore.searchInfo()
|
||||
})
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="common-statistics">
|
||||
<stats-card
|
||||
title="当前支出" color="#CA6C65" unit="元" icon="fa-solid fa-sack-xmark"
|
||||
:value="formatAmount(billStats.expensive)" />
|
||||
:value="formatAmount(billStats.expense)" />
|
||||
|
||||
<stats-card
|
||||
title="当前收入" color="#6FBB69" unit="元" icon="fa-solid fa-sack-dollar"
|
||||
@@ -34,7 +34,7 @@ watch(() => billStore.isRefresh, () => {
|
||||
|
||||
const billStats = ref<IBillSummaryStats>({
|
||||
balance: 0,
|
||||
expensive: 0,
|
||||
expense: 0,
|
||||
income: 0,
|
||||
rate: 0
|
||||
})
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<div class="blog-comment-edit card-item">
|
||||
<div class="comment-title">
|
||||
<el-input v-model="blogComment.name"
|
||||
:maxlength="10" show-word-limit
|
||||
placeholder="请输入昵称"/>
|
||||
<el-input v-model="blogComment.website"
|
||||
:maxlength="50" show-word-limit
|
||||
placeholder="请输入个人博客网站"/>
|
||||
</div>
|
||||
|
||||
<el-input v-model="blogComment.content"
|
||||
placeholder="有什么想和我说的呢" :maxlength="500"
|
||||
:rows="7" show-word-limit type="textarea"
|
||||
/>
|
||||
|
||||
<div class="comment-button">
|
||||
<el-popover ref="emojiPopoverRef" placement="top" :width="300" trigger="hover">
|
||||
<template #default>
|
||||
<div class="emoji-panel">
|
||||
<EmojiPicker :native="true" @select="onSelectEmoji" />
|
||||
</div>
|
||||
</template>
|
||||
<template #reference>
|
||||
<el-button circle>
|
||||
<i class="fa-regular fa-face-smile"></i>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-button type="primary"
|
||||
:disabled="blogComment.content.length === 0"
|
||||
@click="sendCommentClick"
|
||||
class="send-button">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EmojiPicker from 'vue3-emoji-picker'
|
||||
import 'vue3-emoji-picker/css'
|
||||
import { defineProps, ref, onMounted } from 'vue'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { IBlogComment } from '@/types/blog.ts'
|
||||
|
||||
const blogStore = useBlogStore()
|
||||
const emojiPopoverRef = ref()
|
||||
|
||||
const props = defineProps({
|
||||
parentId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const blogComment = ref<IBlogComment>({
|
||||
id: 0,
|
||||
blogId: 0,
|
||||
name: '',
|
||||
website: '',
|
||||
content: '',
|
||||
ipAddress: '',
|
||||
userAgent: '',
|
||||
parentId: 0,
|
||||
isApproved: true,
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
blogComment.value = deepCopyObject(blogStore.getEmptyBlogComment())
|
||||
})
|
||||
|
||||
const onSelectEmoji = (emoji) => {
|
||||
if (blogComment.value.content.length <= 498) {
|
||||
blogComment.value.content += emoji.i
|
||||
}
|
||||
|
||||
emojiPopoverRef.value.hide()
|
||||
}
|
||||
|
||||
const sendCommentClick = async () => {
|
||||
blogComment.value.parentId = props.parentId
|
||||
blogComment.value.blogId = blogStore.currentBlogId
|
||||
|
||||
if (!blogComment.value.name) {
|
||||
blogComment.value.name = 'Anonymous'
|
||||
}
|
||||
|
||||
await blogStore.addBlogComment(blogComment.value)
|
||||
blogComment.value = deepCopyObject(blogStore.getEmptyBlogComment())
|
||||
blogStore.isShowBlogCommentEdit = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.blog-comment-edit {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.comment-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.comment-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.send-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div class="blog-comment-card">
|
||||
<el-avatar :src="`https://api.dicebear.com/6.x/adventurer/svg?seed=${props.comment.name}`"
|
||||
:size="50" alt="未加载"/>
|
||||
|
||||
<div class="blog-comment-content">
|
||||
<div class="basic-info">
|
||||
<span>{{ props.comment.name }}</span>
|
||||
<span class="content">{{ getBrowser(props.comment.userAgent) }}</span>
|
||||
<span class="content">{{ getOsInfo(props.comment.userAgent) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="sub-info">
|
||||
<span class="content">{{ props.comment.createTime }}</span>
|
||||
<el-button v-if="!blogComment.isShowEdit"
|
||||
type="primary" text @click="replyClick">回复</el-button>
|
||||
<el-button v-else type="primary"
|
||||
text @click="cancelClick">取消</el-button>
|
||||
</div>
|
||||
|
||||
<p>{{ props.comment.content }}</p>
|
||||
|
||||
<blog-comment-edit v-if="blogComment.isShowEdit"
|
||||
:parent-id="props.comment.id"/>
|
||||
|
||||
<div class="reply-list">
|
||||
<div class="blog-comment-card" v-for="(item, index) in props.replyList" :key="index">
|
||||
<el-avatar :src="`https://api.dicebear.com/6.x/adventurer/svg?seed=${item.name}`"
|
||||
:size="50" alt="未加载"/>
|
||||
|
||||
<div class="blog-comment-content">
|
||||
<div class="basic-info">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="content">{{ getBrowser(props.comment.userAgent) }}</span>
|
||||
<span class="content">{{ getOsInfo(props.comment.userAgent) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="sub-info">
|
||||
<span class="content">{{ item.createTime }}</span>
|
||||
</div>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlogCommentEdit from '@/components/Comment/BlogCommentEdit.vue'
|
||||
import 'vue3-emoji-picker/css'
|
||||
import { defineProps, PropType, reactive } from 'vue'
|
||||
import { IBlogComment } from '@/types/blog.ts'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
|
||||
const blogStore = useBlogStore()
|
||||
|
||||
const props = defineProps({
|
||||
comment: {
|
||||
required: true,
|
||||
type: Object as PropType<IBlogComment>
|
||||
},
|
||||
replyList: {
|
||||
required: true,
|
||||
type: Object as PropType<IBlogComment[]>
|
||||
}
|
||||
})
|
||||
|
||||
const blogComment = reactive({
|
||||
isShowEdit: false
|
||||
})
|
||||
|
||||
const replyClick = () => {
|
||||
blogStore.currentBlogComment = blogStore.getEmptyBlogComment()
|
||||
blogComment.isShowEdit = true
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
blogComment.isShowEdit = false
|
||||
}
|
||||
|
||||
const getOsInfo = (userAgent: string) => {
|
||||
const uaParse = new UAParser(userAgent)
|
||||
return `${uaParse.getOS().name} ${uaParse.getOS().version}`
|
||||
}
|
||||
|
||||
const getBrowser = (userAgent: string) => {
|
||||
const uaParse = new UAParser(userAgent)
|
||||
return `${uaParse.getBrowser().name} ${uaParse.getBrowser().version}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.blog-comment-card {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
gap: 5px;
|
||||
|
||||
.blog-comment-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.basic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: small;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
src/components/Car/CarCard.vue
Normal file
108
src/components/Car/CarCard.vue
Normal 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>
|
||||
33
src/components/Car/CarQuery.vue
Normal file
33
src/components/Car/CarQuery.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -12,13 +12,22 @@
|
||||
{{ appStore.isDark ? '🌙' : '☀️' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<span>我的</span>
|
||||
<el-button circle :icon="UserFilled" type="primary" @click="viewProfileClick"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
appStore.themeColor = appStore.getThemeColor()
|
||||
@@ -27,6 +36,11 @@ onMounted(() => {
|
||||
const changeThemeColorEvent = (value: string) => {
|
||||
appStore.setThemeColor(value)
|
||||
}
|
||||
|
||||
const viewProfileClick = () => {
|
||||
router.push('/profile/index')
|
||||
appStore.isShowSetting = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
<el-card class="record-card">
|
||||
<span class="record-content">完成人:{{ item.person }}</span>
|
||||
<el-image v-if="item.imageUrl"
|
||||
:src="serviceFileRootPath + item.imageUrl"
|
||||
:preview-src-list="[serviceFileRootPath + item.imageUrl]"
|
||||
fit="contain" alt="暂无图片" class="record-image"/>
|
||||
:src="`${item.imageUrl}${ThumbImageQuery}`" fit="cover" alt="暂无图片"
|
||||
@click="viewImageClick(item)" class="record-image"/>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
@@ -25,8 +24,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { ThumbImageQuery } from '@/utils'
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import { IFoodRecord } from '@/types/food.ts'
|
||||
import { showImagePreview } from 'vant'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const viewImageClick = (item: IFoodRecord) => {
|
||||
showImagePreview({ images: [item.imageUrl], closeable: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="step-body">
|
||||
<span class="step-content">{{ item.content }}</span>
|
||||
<img v-if="item.imageUrl"
|
||||
:src="serviceFileRootPath + item.imageUrl" alt="暂无图片" class="step-image"/>
|
||||
:src="item.imageUrl" alt="暂无图片" class="step-image"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
@@ -1,79 +1,71 @@
|
||||
<template>
|
||||
<div class="life-record-card card-item">
|
||||
<el-carousel v-if="props.foodItem?.recordList.length > 0"
|
||||
arrow="always" @change="changeFoodEvent">
|
||||
<el-carousel-item v-for="(item, index) in getFoodRecord(props.foodItem?.recordList)"
|
||||
:key="index">
|
||||
<el-image :src="serviceFileRootPath + item.imageUrl"
|
||||
fit="contain" alt="暂无成果">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<el-image v-if="props.foodItem?.recordList.length > 0"
|
||||
:src="`${props.foodItem?.recordList[0].imageUrl}${ThumbImageQuery}`"
|
||||
fit="contain" alt="暂无成果" @click="viewImageClick">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-empty v-else description="暂无成果" />
|
||||
|
||||
<div class="record-info">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.foodItem.name }}</span>
|
||||
<van-swipe-cell>
|
||||
<div class="record-info" @click="viewClick()">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.foodItem.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-info">
|
||||
<el-button v-if="!isMobile()"
|
||||
type="primary" :icon="Edit" circle :size="'small'"
|
||||
@click="editFoodRecipeClick"/>
|
||||
<el-button type="primary" :icon="MoreFilled" circle :size="'small'"
|
||||
@click="viewFoodRecipeClick()" />
|
||||
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
|
||||
@click="deleteFoodRecipeClick()" />
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item category-item">
|
||||
<i class="fa-solid fa-utensils"></i>
|
||||
{{ props.foodItem.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.foodItem?.recordList.length > 0" class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ props.foodItem.recordList[foodCardInfo.currentFoodIndex].person}}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ formatDate(props.foodItem.recordList[foodCardInfo.currentFoodIndex].date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<span>暂无成果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item category-item">
|
||||
<i class="fa-solid fa-utensils"></i>
|
||||
{{ props.foodItem.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.foodItem?.recordList.length > 0" class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ props.foodItem.recordList[foodCardInfo.currentFoodIndex].person}}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ formatDate(props.foodItem.recordList[foodCardInfo.currentFoodIndex].date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<span>暂无成果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #right>
|
||||
<van-button square type="primary" text="编辑" style="height: 100%;"
|
||||
@click="editClick"/>
|
||||
<van-button square type="danger" text="删除" style="height: 100%;"
|
||||
@click="deleteClick()"/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { defineProps, onMounted, PropType, reactive } from 'vue'
|
||||
import { IFoodRecipe, IFoodRecord } from '@/types/food.ts'
|
||||
import { IFoodRecipe } from '@/types/food.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { showImagePreview } from 'vant'
|
||||
import { RawImageQuery, ThumbImageQuery } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const foodStore = useFoodStore()
|
||||
@@ -94,35 +86,28 @@ onMounted(() => {
|
||||
foodCardInfo.recommendRate = props.foodItem?.recommendRate as number
|
||||
})
|
||||
|
||||
const getFoodRecord = (recordList: IFoodRecord[]) => {
|
||||
return recordList.filter((item) => item.imageUrl)
|
||||
const viewImageClick = () => {
|
||||
const imageList = props.foodItem?.recordList.map((item) => `${item.imageUrl}${RawImageQuery}`)
|
||||
showImagePreview({ images: imageList, closeable: true })
|
||||
}
|
||||
|
||||
const editFoodRecipeClick = async () => {
|
||||
const editClick = async () => {
|
||||
const id = props.foodItem?.id
|
||||
foodStore.foodRecipeApiType = 'UPDATE'
|
||||
await foodStore.queryFoodRecipe(id as number)
|
||||
foodStore.foodRecipeDialog = {
|
||||
title: '编辑菜谱',
|
||||
visible: true,
|
||||
data: deepCopyObject(foodStore.currentFoodRecipe)
|
||||
}
|
||||
await router.push({ name: 'FoodRecipeForm' })
|
||||
}
|
||||
|
||||
const viewFoodRecipeClick = async () => {
|
||||
const viewClick = async () => {
|
||||
await router.push({ name: 'FoodDetailId', params: { id: props.foodItem?.id } })
|
||||
}
|
||||
|
||||
const deleteFoodRecipeClick = () => {
|
||||
const deleteClick = () => {
|
||||
foodStore.foodRecipeApiType = 'DELETE'
|
||||
commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => {
|
||||
foodStore.handleFoodRecipeApi(props.foodItem?.id as number)
|
||||
foodStore.handleFoodRecipeApi(props.foodItem)
|
||||
})
|
||||
}
|
||||
|
||||
const changeFoodEvent = (current: number) => {
|
||||
foodCardInfo.currentFoodIndex = current
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -30,8 +30,6 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { formatChineseDate } from 'vue3-common/utils/dateUtil'
|
||||
import { IFoodRecord } from '@/types/food.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
const appStore = useAppStore()
|
||||
@@ -85,18 +83,10 @@ const getFoodDailyByDate = (date: string) => {
|
||||
*/
|
||||
const selectFoodRecordClick = async (foodRecord: IFoodRecord) => {
|
||||
foodStore.foodRecordApiType = 'UPDATE'
|
||||
foodStore.currentFoodRecord = foodRecord
|
||||
|
||||
if (isMobile()) {
|
||||
appStore.isShowMobileBack = true
|
||||
foodStore.currentFoodRecord = foodRecord
|
||||
await router.push({ name: 'FoodRecordForm' })
|
||||
} else {
|
||||
foodStore.foodRecordDialog = {
|
||||
title: '编辑记录',
|
||||
visible: true,
|
||||
data: deepCopyObject(foodRecord)
|
||||
}
|
||||
}
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'FoodRecordForm' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
|
||||
center @open="openDialogEvent" class="food-dialog">
|
||||
<el-form ref="formRef" :model="dialogData.data"
|
||||
:rules=rules label-width="80px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="美食名称" prop="name">
|
||||
<el-input v-model="dialogData.data.name" autocomplete="off"
|
||||
placeholder="请输入美食名称" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="美食菜系" prop="category">
|
||||
<el-select v-model="dialogData.data.category">
|
||||
<el-option v-for="(item, index) in foodCategoryList"
|
||||
:key="index" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推荐指数" prop="recommendRate">
|
||||
<el-rate v-model="dialogData.data.recommendRate"
|
||||
:colors="foodStore.rateColorList"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="食材">
|
||||
<food-material-form />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="步骤">
|
||||
<food-step-form />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="dialogData.data.remark" autocomplete="off"
|
||||
placeholder="请输入备注" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelClick">取消</el-button>
|
||||
<el-button type="primary" @click="confirmClick">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FoodMaterialForm from '@/components/Food/Form/FoodMaterialForm.vue'
|
||||
import FoodStepForm from '@/components/Food/Form/FoodStepForm.vue'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { FormInstance, FormRules } from 'element-plus'
|
||||
import { computed, nextTick, ref, reactive } from 'vue'
|
||||
import { foodRules } from '@/utils/element/elRules.ts'
|
||||
|
||||
const rules = reactive<FormRules>(foodRules)
|
||||
const formRef = ref<FormInstance>()
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const foodCategoryList = ['家常菜', '淮扬菜', '鲁菜', '川菜', '粤菜', '闽菜', '浙菜', '湘菜', '徽菜', '其他']
|
||||
|
||||
const dialogData = computed(() => foodStore.foodRecipeDialog)
|
||||
|
||||
/**
|
||||
* 打开对话框事件
|
||||
*/
|
||||
const openDialogEvent = async () => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
const confirmClick = () => {
|
||||
formRef.value?.validate().then(() => {
|
||||
foodStore.handleFoodRecipeApi()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
dialogData.value.visible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-dialog {
|
||||
width: 75%;
|
||||
|
||||
.el-dialog__body {
|
||||
height: 65vh;
|
||||
|
||||
.el-button.is-text {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.material-table {
|
||||
height: 45vh;
|
||||
}
|
||||
|
||||
.food-step-table {
|
||||
height: 55vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +1,39 @@
|
||||
<template>
|
||||
<div v-if="isMobile()" class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>分类:</span>
|
||||
<el-segmented v-model="foodStore.queryRecipe.category"
|
||||
:options="queryCategoryList"
|
||||
@change="queryFoodSegmented"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="common-query">
|
||||
<el-select v-model="foodStore.queryDateType"
|
||||
@change="foodStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
<div class="common-query">
|
||||
<el-select
|
||||
v-model="foodStore.queryRecipe.name"
|
||||
filterable clearable placeholder="请选择菜谱名称"
|
||||
style="width: 150px;"
|
||||
@change="changeFoodNameEvent">
|
||||
<el-option
|
||||
v-for="(item, index) in foodStore.foodNameList"
|
||||
:key="index" :label="item" :value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ICategory } from '@/types'
|
||||
import { onMounted } from 'vue'
|
||||
import { searchFoodRecipeApi } from '@/apis/food.ts'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const queryCategoryList = ref([] as ICategory[])
|
||||
|
||||
onMounted(async () => {
|
||||
await foodStore.queryFoodCategory()
|
||||
initCategoryList()
|
||||
await foodStore.queryFoodNameList()
|
||||
foodStore.queryRecipe.name = ''
|
||||
})
|
||||
|
||||
const dateTypeOptions = [{
|
||||
label: '按月选择',
|
||||
value: 'month'
|
||||
},
|
||||
{
|
||||
label: '按年选择',
|
||||
value: 'year'
|
||||
}]
|
||||
|
||||
const initCategoryList = () => {
|
||||
queryCategoryList.value = []
|
||||
queryCategoryList.value.push({
|
||||
label: '全部',
|
||||
value: ''
|
||||
})
|
||||
foodStore.foodCategoryList.forEach((item) => {
|
||||
queryCategoryList.value.push({
|
||||
label: item,
|
||||
value: item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const queryFoodSegmented = async () => {
|
||||
foodStore.pageInfo = {
|
||||
currentPage: 1,
|
||||
pageSize: 3,
|
||||
totalSize: 1
|
||||
const changeFoodNameEvent = async (value: string) => {
|
||||
if (value) {
|
||||
const result = await searchFoodRecipeApi(value)
|
||||
foodStore.foodRecipeList = [result]
|
||||
} else {
|
||||
foodStore.pageInfo.currentPage = 1
|
||||
foodStore.pageInfo.pageSize = 3
|
||||
foodStore.foodRecipeList = []
|
||||
await foodStore.queryMobileFoodRecipeList()
|
||||
}
|
||||
foodStore.foodRecipeList = []
|
||||
await foodStore.queryMobileFoodRecipeList()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dialog v-model="foodStore.foodMaterialDialog.visible"
|
||||
:title="foodStore.foodMaterialDialog.title"
|
||||
<el-dialog v-model="foodStore.foodMaterialDialogVisible"
|
||||
:title="foodStore.foodMaterialApiType === 'ADD' ? '新增食材' : '编辑食材'"
|
||||
center @open="openDialogEvent"
|
||||
class="food-material-dialog">
|
||||
<el-form ref="formRef" :model="foodStore.foodMaterialDialog.data"
|
||||
:rules="rules">
|
||||
<el-form ref="formRef" :model="foodStore.currentFoodMaterial"
|
||||
:rules="rules" label-width="auto">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select
|
||||
v-model="foodStore.foodMaterialDialog.data.type"
|
||||
v-model="foodStore.currentFoodMaterial.type"
|
||||
placeholder="请选择类型">
|
||||
<el-option
|
||||
v-for="(item, index) in foodMaterialTypeList"
|
||||
@@ -17,14 +17,14 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="foodStore.foodMaterialDialog.data.name" autocomplete="off"
|
||||
<el-input v-model="foodStore.currentFoodMaterial.name" autocomplete="off"
|
||||
placeholder="请输入名称" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分量" prop="amount">
|
||||
<el-input v-model="foodStore.foodMaterialDialog.data.amount" autocomplete="off"
|
||||
<el-input v-model="foodStore.currentFoodMaterial.amount" autocomplete="off"
|
||||
placeholder="请输入分量(1斤/2勺/适量)" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
@@ -68,22 +68,22 @@ const confirmClick = () => {
|
||||
formRef.value.validate().then(async () => {
|
||||
if (foodStore.foodMaterialApiType === 'UPDATE') {
|
||||
const { editFoodMaterialIndex: index } = foodStore
|
||||
foodStore.foodRecipeDialog.data.materialList[index] = foodStore.foodMaterialDialog.data
|
||||
foodStore.currentFoodRecipe.materialList[index] = foodStore.currentFoodMaterial
|
||||
} else {
|
||||
foodStore.foodRecipeDialog.data.materialList.push(foodStore.foodMaterialDialog.data)
|
||||
foodStore.currentFoodRecipe.materialList.push(foodStore.currentFoodMaterial)
|
||||
}
|
||||
foodStore.foodMaterialDialog.visible = false
|
||||
foodStore.foodMaterialDialogVisible = false
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
foodStore.foodMaterialDialog.visible = false
|
||||
foodStore.foodMaterialDialogVisible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-material-dialog {
|
||||
width: 30%;
|
||||
width: 80%;
|
||||
|
||||
.el-dialog__body {
|
||||
// height: 50vh;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<el-table :data="foodStore.foodRecipeDialog.data.materialList"
|
||||
border stripe class="material-table">
|
||||
<el-table-column type="index" align="center" width="90">
|
||||
<template #header>
|
||||
<el-button type="primary" @click="handleAddClick">新增</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" align="center" min-width="10%"/>
|
||||
<el-table-column prop="name" label="名称" align="center" min-width="20%"/>
|
||||
<el-table-column prop="amount" label="分量" align="center" min-width="20%"/>
|
||||
<el-table-column label="操作" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text
|
||||
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
|
||||
<el-button type="danger" text
|
||||
@click="handleDeleteClick(scope.$index, scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="material-form">
|
||||
<div style="align-self: center;">
|
||||
<el-button type="primary" @click="handleAddClick">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="foodStore.currentFoodRecipe.materialList"
|
||||
border stripe class="material-table">
|
||||
<el-table-column prop="type" label="类型" align="center" min-width="15%"/>
|
||||
<el-table-column prop="name" label="名称" align="center" min-width="20%"/>
|
||||
<el-table-column prop="amount" label="分量" align="center" min-width="20%"/>
|
||||
<el-table-column label="操作" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text style="margin: 0 10px 0 0;padding: 0;"
|
||||
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
|
||||
<el-button type="danger" text style="margin: 0;padding: 0;"
|
||||
@click="handleDeleteClick(scope.$index, scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -30,27 +31,21 @@ const foodStore = useFoodStore()
|
||||
|
||||
const handleAddClick = () => {
|
||||
foodStore.foodMaterialApiType = 'ADD'
|
||||
foodStore.foodMaterialDialog = {
|
||||
title: '新增食材',
|
||||
visible: true,
|
||||
data: foodStore.getEmptyFoodMaterial()
|
||||
}
|
||||
foodStore.currentFoodMaterial = foodStore.getEmptyFoodMaterial()
|
||||
foodStore.foodMaterialDialogVisible = true
|
||||
}
|
||||
|
||||
const handleEditClick = (index: number, material: IFoodMaterial) => {
|
||||
foodStore.editFoodMaterialIndex = index
|
||||
foodStore.foodMaterialApiType = 'UPDATE'
|
||||
foodStore.foodMaterialDialog = {
|
||||
title: '编辑食材',
|
||||
visible: true,
|
||||
data: deepCopyObject(material)
|
||||
}
|
||||
foodStore.currentFoodMaterial = deepCopyObject(material)
|
||||
foodStore.foodMaterialDialogVisible = true
|
||||
}
|
||||
|
||||
const handleDeleteClick = (index: number, material: IFoodMaterial) => {
|
||||
foodStore.foodMaterialApiType = 'DELETE'
|
||||
commonElMessageBox(`确认删除该食材: ${material.name}?`).then(() => {
|
||||
foodStore.foodRecipeDialog.data.materialList.splice(index, 1)
|
||||
foodStore.currentFoodRecipe.materialList.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,23 +1,37 @@
|
||||
<template>
|
||||
<el-dialog v-model="foodStore.foodStepDialog.visible"
|
||||
:title="foodStore.foodStepDialog.title"
|
||||
<el-dialog v-model="foodStore.foodStepDialogVisible"
|
||||
:title="foodStore.foodStepApiType === 'ADD' ? '新增步骤' : '编辑步骤'"
|
||||
center @open="openDialogEvent"
|
||||
class="food-step-dialog">
|
||||
<el-form ref="formRef" :model="foodStore.foodStepDialog.data"
|
||||
:rules="rules">
|
||||
<el-form ref="formRef" :model="foodStore.currentFoodStep"
|
||||
:rules="rules" label-width="auto">
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input v-model="foodStore.foodStepDialog.data.content" autocomplete="off"
|
||||
<el-input v-model="foodStore.currentFoodStep.content" autocomplete="off"
|
||||
placeholder="请输入内容" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片">
|
||||
<upload-image v-model="foodStore.foodStepDialog.data.imageUrl"
|
||||
:service-url="fileServiceUrl"
|
||||
:image-limit="1"
|
||||
:service-file-root-path="serviceFileRootPath"
|
||||
:image-path="foodStore.foodImagePath"/>
|
||||
<el-upload
|
||||
v-model:file-list="imageFileList"
|
||||
list-type="picture-card"
|
||||
:accept="acceptImageTypes"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="httpRequest"
|
||||
:on-success="handleSuccess"
|
||||
:on-exceed="handleExceed"
|
||||
:before-remove="beforeRemove"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传小于10M的图片
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -31,17 +45,58 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UploadImage } from 'vue3-common'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
|
||||
import { foodStepRules } from '@/utils/element/elRules.ts'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
import { acceptImageTypes } from '@/utils/file.ts'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { beforeUploadImage, httpRequestFile } from '@/utils/element/elUpload.ts'
|
||||
|
||||
const rules = reactive<FormRules>(foodStepRules)
|
||||
|
||||
const formRef = ref()
|
||||
const foodStore = useFoodStore()
|
||||
const imageFileList = ref<UploadUserFile[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
initImageFileList()
|
||||
})
|
||||
|
||||
const initImageFileList = () => {
|
||||
imageFileList.value = []
|
||||
const { imageUrl } = foodStore.currentFoodStep
|
||||
if (imageUrl) {
|
||||
imageFileList.value.push({
|
||||
name: 'image',
|
||||
url: imageUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||
return beforeUploadImage(rawFile)
|
||||
}
|
||||
|
||||
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||
return await httpRequestFile(foodStore.foodImagePath, options)
|
||||
}
|
||||
|
||||
const handleSuccess = (response: string) => {
|
||||
foodStore.currentFoodStep.imageUrl = response
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多上传1张图片')
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = () => {
|
||||
foodStore.currentFoodStep.imageUrl = ''
|
||||
}
|
||||
|
||||
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开对话框事件
|
||||
@@ -60,22 +115,22 @@ const confirmClick = () => {
|
||||
.then(async () => {
|
||||
if (foodStore.foodStepApiType === 'UPDATE') {
|
||||
const { editFoodStepIndex: index } = foodStore
|
||||
foodStore.foodRecipeDialog.data.stepList[index] = foodStore.foodStepDialog.data
|
||||
foodStore.currentFoodRecipe.stepList[index] = foodStore.currentFoodStep
|
||||
} else {
|
||||
foodStore.foodRecipeDialog.data.stepList.push(foodStore.foodStepDialog.data)
|
||||
foodStore.currentFoodRecipe.stepList.push(foodStore.currentFoodStep)
|
||||
}
|
||||
foodStore.foodStepDialog.visible = false
|
||||
foodStore.foodStepDialogVisible = false
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
foodStore.foodStepDialog.visible = false
|
||||
foodStore.foodStepDialogVisible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-step-dialog {
|
||||
width: 30%;
|
||||
width: 80%;
|
||||
|
||||
.el-dialog__body {
|
||||
// height: 50vh;
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
<template>
|
||||
<el-table :data="foodStore.foodRecipeDialog.data.stepList"
|
||||
border class="food-step-table">
|
||||
<el-table-column type="index" align="center" width="90">
|
||||
<template #header>
|
||||
<el-button type="primary" @click="handleAddClick">新增</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" align="center" min-width="20%" />
|
||||
<el-table-column prop="imageUrl" label="图片" align="center" min-width="20%">
|
||||
<template #default="scope">
|
||||
<el-image v-if="scope.row.imageUrl" :src="serviceFileRootPath + scope.row.imageUrl" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<div class="step-form">
|
||||
<div style="align-self: center;">
|
||||
<el-button type="primary" @click="handleAddClick">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-table-column label="操作" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text
|
||||
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
|
||||
<el-button type="danger" text
|
||||
@click="handleDeleteClick(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table :data="foodStore.currentFoodRecipe.stepList"
|
||||
border class="food-step-table">
|
||||
<el-table-column prop="content" label="内容" align="center" min-width="20%" />
|
||||
<el-table-column prop="imageUrl" label="图片" align="center" min-width="20%">
|
||||
<template #default="scope">
|
||||
<el-image v-if="scope.row.imageUrl" :src="scope.row.imageUrl" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text style="margin: 0 10px 0 0;padding: 0;"
|
||||
@click="handleEditClick(scope.$index, scope.row)">编辑</el-button>
|
||||
<el-button type="danger" text style="margin: 0;padding: 0;"
|
||||
@click="handleDeleteClick(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -29,35 +30,27 @@ import { useFoodStore } from '@/store/food.ts'
|
||||
import { IFoodStep } from '@/types/food.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const handleAddClick = () => {
|
||||
foodStore.foodStepApiType = 'ADD'
|
||||
foodStore.foodStepImageList = []
|
||||
foodStore.foodStepDialog = {
|
||||
title: '新增步骤',
|
||||
visible: true,
|
||||
data: foodStore.getEmptyFoodStep()
|
||||
}
|
||||
foodStore.currentFoodStep = foodStore.getEmptyFoodStep()
|
||||
foodStore.foodStepDialogVisible = true
|
||||
}
|
||||
|
||||
const handleEditClick = (index: number, step: IFoodStep) => {
|
||||
foodStore.editFoodStepIndex = index
|
||||
foodStore.foodStepApiType = 'UPDATE'
|
||||
foodStore.foodStepDialog = {
|
||||
title: '编辑步骤',
|
||||
visible: true,
|
||||
data: deepCopyObject(step)
|
||||
}
|
||||
foodStore.currentFoodStep = deepCopyObject(step)
|
||||
foodStore.foodStepDialogVisible = true
|
||||
}
|
||||
|
||||
const handleDeleteClick = (index: number) => {
|
||||
commonElMessageBox('确认删除该步骤?').then(() => {
|
||||
foodStore.foodStepApiType = 'DELETE'
|
||||
foodStore.foodRecipeDialog.data.stepList.splice(index, 1)
|
||||
foodStore.foodRecipeDialog.data.stepList.forEach((item) => {
|
||||
foodStore.currentFoodRecipe.stepList.splice(index, 1)
|
||||
foodStore.currentFoodRecipe.stepList.forEach((item) => {
|
||||
if (item.sort > index) {
|
||||
item.sort -= 1
|
||||
}
|
||||
|
||||
129
src/components/Health/HealthChart.vue
Normal file
129
src/components/Health/HealthChart.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="bill-chart common-chart"
|
||||
v-loading="healthStore.isLoading"
|
||||
element-loading-text="正在加载中...">
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊体重趋势</span>
|
||||
</div>
|
||||
<div class="chart" ref="weightDailyChartRef"/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊睡眠时长统计图</span>
|
||||
</div>
|
||||
<div class="chart" ref="sleepDailyChartRef"/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊运动时长统计</span>
|
||||
</div>
|
||||
<div class="chart" ref="healthDurationChartRef"/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container card-item">
|
||||
<div class="content">
|
||||
<span class="title">📊运动类别统计</span>
|
||||
</div>
|
||||
<div class="chart" ref="healthCategoryChartRef"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
|
||||
import { useHealthStore } from '@/store/health'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
|
||||
const healthStore = useHealthStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const weightDailyChartRef = ref<HTMLElement>()
|
||||
const sleepDailyChartRef = ref<HTMLElement>()
|
||||
const healthDurationChartRef = ref<HTMLElement>()
|
||||
const healthCategoryChartRef = ref<HTMLElement>()
|
||||
|
||||
let weightDailyChart: echarts.ECharts
|
||||
let sleepDailyChart: echarts.ECharts
|
||||
let healthDurationChart: echarts.ECharts
|
||||
let healthCategoryChart: echarts.ECharts
|
||||
|
||||
watch(() => healthStore.isRefresh, () => {
|
||||
updateChart()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
weightDailyChart = echarts.init(weightDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
sleepDailyChart = echarts.init(sleepDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
healthDurationChart = echarts.init(healthDurationChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
healthCategoryChart = echarts.init(healthCategoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||
})
|
||||
|
||||
const updateChart = () => {
|
||||
const MAX_CATEGORY_COUNT = 5
|
||||
|
||||
// 体重趋势统计图
|
||||
const weightRecordList = healthStore.healthRecordList
|
||||
.filter((item) => item.weight !== 0)
|
||||
|
||||
const weightXAxis = weightRecordList.map((item) => {
|
||||
return formatDate(item.date, 'MM-DD')
|
||||
}).reverse()
|
||||
const weightYAxis = weightRecordList.map((item) => item.weight).reverse()
|
||||
const sortedRecordList = weightRecordList.sort((a, b) => a.weight - b.weight)
|
||||
|
||||
weightDailyChart.setOption(lineChartOptions('日期', weightXAxis, '体重(千克)', weightYAxis))
|
||||
|
||||
if (sortedRecordList.length > 0) {
|
||||
weightDailyChart.setOption({
|
||||
yAxis: {
|
||||
min: Math.floor(sortedRecordList[0].weight),
|
||||
max: Math.ceil(sortedRecordList[sortedRecordList.length - 1].weight)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 睡眠时长统计图
|
||||
const sleepRecordList = healthStore.healthRecordList
|
||||
.filter((item) => item.sleepDuration !== null)
|
||||
.sort((a, b) => (a.date > b.date ? -1 : 1))
|
||||
|
||||
const sleepXAxis = sleepRecordList.map((item) => {
|
||||
return formatDate(item.date, 'MM-DD')
|
||||
}).reverse()
|
||||
const sleepYAxis = sleepRecordList.map((item) => item.sleepDuration).reverse() as number[]
|
||||
|
||||
sleepDailyChart.setOption(barChartOptions('日期', sleepXAxis, '时间(小时)', sleepYAxis))
|
||||
|
||||
// 运动量统计图
|
||||
const sportRecordList = healthStore.healthRecordList
|
||||
.filter((item) => item.sportProject !== '')
|
||||
.sort((a, b) => (a.date > b.date ? -1 : 1))
|
||||
|
||||
const durationXAxis = sportRecordList.map((item) => {
|
||||
return formatDate(item.date, 'MM-DD')
|
||||
}).reverse()
|
||||
const durationYAxis = sportRecordList.map((item) => item.sportDuration).reverse()
|
||||
|
||||
healthDurationChart.setOption(barChartOptions('日期', durationXAxis, '时间(分钟)', durationYAxis))
|
||||
|
||||
// 类别统计图
|
||||
const healthCategoryList = healthStore.healthRecordList.reduce((acc, item) => {
|
||||
if (item.sportProject && item.sportDuration > 0) {
|
||||
const existing = acc.find((project) => project.name === item.sportProject)
|
||||
if (existing) {
|
||||
existing.value += item.sportDuration
|
||||
} else {
|
||||
acc.push({ name: item.sportProject, value: item.sportDuration })
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
healthCategoryChart.setOption(pieChartOptions('名称', '分钟', healthCategoryList.slice(0, MAX_CATEGORY_COUNT)))
|
||||
}
|
||||
</script>
|
||||
179
src/components/Health/HealthForm.vue
Normal file
179
src/components/Health/HealthForm.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<el-form :model="healthStore.currentHealth" class="health-form card-item">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
<span>日期</span>
|
||||
</template>
|
||||
<el-date-picker v-model="healthStore.currentHealth.date"
|
||||
type="date" :disabled="true"
|
||||
style="width: 150px;"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-person-running"></i>
|
||||
<span>运动项目</span>
|
||||
</template>
|
||||
|
||||
<el-select v-model="healthStore.currentHealth.sportProject"
|
||||
placeholder="请选择运行项目" clearable style="width: 150px;">
|
||||
<el-option v-for="(item, index) in projectList" :key="index"
|
||||
:label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<span>运动时长</span>
|
||||
</template>
|
||||
|
||||
<el-input-number v-model="healthStore.currentHealth.sportDuration"
|
||||
:min="0" :max="300" :precision="0"
|
||||
style="width: 120px;"></el-input-number>
|
||||
|
||||
<span style="margin-left: 5px;">分钟</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-weight-scale"></i>
|
||||
<span>体重</span>
|
||||
</template>
|
||||
|
||||
<el-input-number v-model="healthStore.currentHealth.weight"
|
||||
:min="0" :max="200"
|
||||
style="width: 120px;"></el-input-number>
|
||||
|
||||
<span style="margin-left: 5px;">千克</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-moon"></i>
|
||||
<span>睡眠开始时间</span>
|
||||
</template>
|
||||
|
||||
<el-time-picker
|
||||
v-model="healthStore.currentHealth.sleepStartTime"
|
||||
clearable :editable="false"
|
||||
placeholder="请选择睡眠开始时间"
|
||||
style="width: 180px"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
@clear="healthStore.currentHealth.sleepStartTime = ''"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-sun"></i>
|
||||
<span>睡眠结束时间</span>
|
||||
</template>
|
||||
|
||||
<el-time-picker
|
||||
v-model="healthStore.currentHealth.sleepEndTime"
|
||||
clearable :editable="false"
|
||||
placeholder="请选择睡眠结束时间"
|
||||
style="width: 180px"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
@clear="healthStore.currentHealth.sleepEndTime = ''"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<i class="fa-solid fa-hourglass-start"></i>
|
||||
<span>睡眠时长</span>
|
||||
</template>
|
||||
|
||||
<span>{{ healthStore.currentHealth.sleepDuration }}</span>
|
||||
<span style="margin-left: 5px;">小时</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="align-self: center;">
|
||||
<el-button type="primary" @click="onSubmit"
|
||||
:disabled="isBeforeDate(formatDate(new Date()), healthStore.selectDate)">
|
||||
确认
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useHealthStore } from '@/store/health'
|
||||
import { formatDate, isBeforeDate } from 'vue3-common/utils/dateUtil'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const healthStore = useHealthStore()
|
||||
|
||||
const projectList = ['羽毛球', '乒乓球', '跑步', '其他']
|
||||
|
||||
const onSubmit = async () => {
|
||||
const { sportProject, sportDuration, weight,
|
||||
sleepStartTime, sleepEndTime } = healthStore.currentHealth
|
||||
if (sportProject === '' && sportDuration === 0 && weight === 0
|
||||
&& sleepStartTime === '' && sleepEndTime === '') {
|
||||
return
|
||||
}
|
||||
|
||||
if (sportDuration) {
|
||||
if (!sportProject) {
|
||||
ElMessage.error('请选择运动项目')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (sleepStartTime && sleepEndTime) {
|
||||
healthStore.currentHealth.sleepDuration = calculateSleepDuration(sleepStartTime, sleepEndTime)
|
||||
}
|
||||
|
||||
await healthStore.updateHealthDay()
|
||||
healthStore.currentHealth = healthStore.getHealthDay(healthStore.selectDate)
|
||||
}
|
||||
|
||||
const calculateSleepDuration = (startTime: string, endTime: string) => {
|
||||
const start = dayjs(startTime, 'HH:mm')
|
||||
let end = dayjs(endTime, 'HH:mm')
|
||||
|
||||
// 处理跨天
|
||||
if (end.isSameOrBefore(start)) {
|
||||
end = end.add(1, 'day')
|
||||
}
|
||||
|
||||
// 计算时间差(小时)
|
||||
return end.diff(start, 'hour', true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.health-form {
|
||||
padding: 10px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-form-item {
|
||||
justify-content: space-between;
|
||||
|
||||
.el-form-item__label {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
font-size: 1.2rem;
|
||||
color: #DE3163;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
src/components/Health/HealthStats.vue
Normal file
41
src/components/Health/HealthStats.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="common-statistics">
|
||||
<stats-card
|
||||
title="平均体重" color="#CA6C65" unit="千克" icon="fa-solid fa-weight-scale"
|
||||
:value="formatAmount(healthStats.averageWeight)" />
|
||||
|
||||
<stats-card
|
||||
title="运动时长" color="#6FBB69" unit="分钟" icon="fa-solid fa-person-walking"
|
||||
:value="healthStats.totalDuration" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StatsCard from '@/components/StatsCard.vue'
|
||||
import { useHealthStore } from '@/store/health'
|
||||
import { ref, watch } from 'vue'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
import record from '@/views/travel/record.vue'
|
||||
import { max } from '@popperjs/core/lib/utils/math'
|
||||
|
||||
const healthStore = useHealthStore()
|
||||
|
||||
watch(() => healthStore.isRefresh, () => {
|
||||
updateStats()
|
||||
})
|
||||
|
||||
const healthStats = ref({
|
||||
averageWeight: 0,
|
||||
totalDuration: 0
|
||||
})
|
||||
|
||||
const updateStats = () => {
|
||||
const recordList = healthStore.healthRecordList
|
||||
const totalWeight = recordList.reduce((sum, item) => sum + item.weight, 0)
|
||||
|
||||
if (recordList.length > 0) {
|
||||
healthStats.value.averageWeight = totalWeight / recordList.length
|
||||
healthStats.value.totalDuration = recordList.reduce((sum, item) => sum + item.sportDuration, 0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -5,8 +5,11 @@
|
||||
:is-dark="appStore.isDark"
|
||||
class="journal-calendar card-item"/>
|
||||
|
||||
<journal-card v-if="journalStore.currentJournal.id !== 0"
|
||||
:item="journalStore.currentJournal"/>
|
||||
<div v-if="journalStore.dateJournalList.length > 0"
|
||||
class="journal-list">
|
||||
<journal-card v-for="(item, index) in journalStore.dateJournalList"
|
||||
:key="index" :item="item"/>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="今日暂无日记" />
|
||||
</div>
|
||||
@@ -35,6 +38,7 @@ const dailyInfo = reactive({
|
||||
})
|
||||
|
||||
const updateInfo = () => {
|
||||
journalStore.dateJournalList = []
|
||||
getCalendarMoveDate()
|
||||
getCalendarAttrDates()
|
||||
}
|
||||
@@ -42,7 +46,7 @@ const updateInfo = () => {
|
||||
const getCalendarMoveDate = () => {
|
||||
const recordLength = journalStore.journalList.length
|
||||
if (recordLength > 0) {
|
||||
journalStore.currentJournal = getJournalDay(formatDate(new Date()))
|
||||
journalStore.dateJournalList = getJournalDay(formatDate(new Date()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ const getCalendarAttrDates = () => {
|
||||
dates: new Date()
|
||||
})
|
||||
|
||||
const dateList = journalStore.journalList.map((item) => item.date)
|
||||
const dateList = journalStore.journalList.map((item) => formatDate(item.date))
|
||||
dailyInfo.attrs.push({
|
||||
dot: 'red',
|
||||
dates: [...new Set(dateList)]
|
||||
@@ -66,12 +70,11 @@ const getCalendarAttrDates = () => {
|
||||
* @param day 日期
|
||||
*/
|
||||
const dateClick = (day: CalendarDay) => {
|
||||
journalStore.currentJournal = getJournalDay(formatDate(day.date))
|
||||
journalStore.dateJournalList = getJournalDay(formatDate(day.date))
|
||||
}
|
||||
|
||||
const getJournalDay = (date: string): IJournal => {
|
||||
return journalStore.journalList.find((item) => formatDate(item.date) === date)
|
||||
|| journalStore.getEmptyJournal()
|
||||
const getJournalDay = (date: string): IJournal[] => {
|
||||
return journalStore.journalList.filter((item) => formatDate(item.date) === date) || []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,5 +99,12 @@ const changeMonthClick = async (value) => {
|
||||
.journal-calendar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.journal-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,16 +37,15 @@
|
||||
<!-- 单媒体内容(图片优先于视频) -->
|
||||
<el-image
|
||||
v-if="props.item?.imageUrl"
|
||||
:src="serviceFileRootPath + props.item?.imageUrl"
|
||||
:preview-src-list="[serviceFileRootPath + props.item?.imageUrl]"
|
||||
fit="contain"
|
||||
:src="`${props.item?.imageUrl}${ThumbImageQuery}`"
|
||||
fit="contain" @click="viewImageClick"
|
||||
class="media-content"/>
|
||||
|
||||
<video
|
||||
v-if="props.item?.videoUrl"
|
||||
controls
|
||||
class="media-content">
|
||||
<source :src="serviceFileRootPath + props.item?.videoUrl" type="video/mp4">
|
||||
<source :src="props.item?.videoUrl" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<!-- 展开/收起按钮 -->
|
||||
@@ -71,12 +70,10 @@ import { SvgIcon } from 'vue3-common'
|
||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { IJournal } from '@/types/journal.ts'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { useJournalStore } from '@/store/journal.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { RawImageQuery, ThumbImageQuery } from '@/utils'
|
||||
import { showImagePreview } from 'vant'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -86,8 +83,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const journalStore = useJournalStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
if (overflowLines.value <= 0) expanded.value = true
|
||||
@@ -106,23 +101,16 @@ const overflowLines = computed(() => {
|
||||
|
||||
const showToggle = computed(() => overflowLines.value > 0)
|
||||
|
||||
const toggleExpand = () => expanded.value = !expanded.value
|
||||
const toggleExpand = () => { expanded.value = !expanded.value }
|
||||
|
||||
const viewImageClick = () => {
|
||||
showImagePreview({ images: [props.item?.imageUrl + RawImageQuery], closeable: true })
|
||||
}
|
||||
|
||||
const editClick = async () => {
|
||||
const id = props.item?.id
|
||||
journalStore.journalApiType = 'UPDATE'
|
||||
journalStore.currentJournal = props.item
|
||||
|
||||
if (isMobile()) {
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'JournalDetailId', params: { id } })
|
||||
} else {
|
||||
journalStore.journalDialog = {
|
||||
title: '编辑日记',
|
||||
visible: true,
|
||||
data: deepCopyObject(journalStore.currentJournal)
|
||||
}
|
||||
}
|
||||
journalStore.currentJournal = deepCopyObject(props.item)
|
||||
journalStore.journalDrawer = true
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
248
src/components/Journal/JournalDrawer.vue
Normal file
248
src/components/Journal/JournalDrawer.vue
Normal 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>
|
||||
@@ -38,8 +38,6 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, PropType } from 'vue'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { useLedgerStore } from '@/store/ledger.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ILedger, ILegerItem } from '@/types/ledger.ts'
|
||||
|
||||
@@ -27,10 +27,6 @@ onMounted(async () => {
|
||||
const initCategoryList = () => {
|
||||
queryCategoryList.value = []
|
||||
|
||||
if (ledgerStore.ledgerBookList.length > 0) {
|
||||
ledgerStore.queryInfo.book = ledgerStore.ledgerBookList[0]
|
||||
}
|
||||
|
||||
ledgerStore.ledgerBookList.forEach((item) => {
|
||||
queryCategoryList.value.push({
|
||||
label: item,
|
||||
|
||||
23
src/components/Ledger/LedgerRank.vue
Normal file
23
src/components/Ledger/LedgerRank.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div v-for="(item, index) in ledgerRankList.slice(0, MAX_RANK_COUNT)"
|
||||
:key="index" class="rank-item">
|
||||
<span class="rank-index">{{ index+1 }}.</span>
|
||||
<span class="rank-content">{{ item.content }}</span>
|
||||
<span class="rank-amount">¥{{ formatAmount(item.totalPrice) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLedgerStore } from '@/store/ledger.ts'
|
||||
import { getLedgerSortByPrice } from '@/utils/home/ledgerUtil.ts'
|
||||
import { computed } from 'vue'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
|
||||
const ledgerStore = useLedgerStore()
|
||||
|
||||
const MAX_RANK_COUNT = 10
|
||||
|
||||
const ledgerRankList = computed(() => {
|
||||
return getLedgerSortByPrice(ledgerStore.ledgerRecordList)
|
||||
})
|
||||
</script>
|
||||
@@ -79,7 +79,6 @@ import { onMounted, reactive, ref } from 'vue'
|
||||
import { loginRules } from '@/utils/element/elRules.ts'
|
||||
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
|
||||
import { ILoginType } from '@/types/auth.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
const rules = reactive<FormRules>(loginRules)
|
||||
const authStore = useAuthStore()
|
||||
@@ -98,16 +97,6 @@ onMounted(() => {
|
||||
const handleLogin = async (type: ILoginType) => {
|
||||
loginFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
if (type === 'common' && !isMobile()) {
|
||||
ElMessage.error('目前只支持移动端')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'admin' && isMobile()) {
|
||||
ElMessage.error('目前只支持PC端')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'common') {
|
||||
authStore.loginLoading = true
|
||||
} else {
|
||||
|
||||
114
src/components/NumberInput.vue
Normal file
114
src/components/NumberInput.vue
Normal 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>
|
||||
@@ -1,25 +1,26 @@
|
||||
<template>
|
||||
<div class="password-card card-item">
|
||||
<el-avatar class="avatar-info">{{ passwordItem.title.charAt(0) }}</el-avatar>
|
||||
<van-swipe-cell>
|
||||
<div class="password-card card-item" @click="editClick">
|
||||
<el-avatar class="avatar-info">{{ passwordItem.title.charAt(0) }}</el-avatar>
|
||||
|
||||
<div class="title-info">
|
||||
<span>{{ passwordItem.title }}</span>
|
||||
<span>{{ passwordItem.username }}</span>
|
||||
<div class="title-info">
|
||||
<span>{{ passwordItem.title }}</span>
|
||||
<span>{{ passwordItem.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-info">
|
||||
<el-button type="primary" :icon="Edit" circle
|
||||
@click="editClick"/>
|
||||
<el-button type="danger" :icon="DeleteFilled" circle
|
||||
@click="deleteClick()" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { usePasswordStore } from '@/store/password.ts'
|
||||
import { defineProps, onMounted, PropType } from 'vue'
|
||||
import { defineProps, PropType } from 'vue'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { IPassword } from '@/types/password.ts'
|
||||
@@ -34,10 +35,6 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
const editClick = async () => {
|
||||
const id = props.passwordItem?.id
|
||||
passwordStore.passwordApiType = 'UPDATE'
|
||||
@@ -59,7 +56,7 @@ const deleteClick = () => {
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 80px;
|
||||
grid-template-columns: 40px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { Calendar, DateSelectArg, EventClickArg } from '@fullcalendar/core'
|
||||
import { initCommonCalendar, initMobileCalendar } from '@/utils/home/planUtil.ts'
|
||||
import { initMobileCalendar } from '@/utils/home/planUtil.ts'
|
||||
import { usePlanStore } from '@/store/plan.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
let calendar: Calendar | undefined
|
||||
|
||||
@@ -27,11 +26,7 @@ watch(() => planStore.isRefresh, () => {
|
||||
const initCalendar = async () => {
|
||||
const calendarEl = document.getElementById('planCalendar') as HTMLElement
|
||||
|
||||
if (isMobile()) {
|
||||
calendar = initMobileCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
|
||||
} else {
|
||||
calendar = initCommonCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
|
||||
}
|
||||
calendar = initMobileCalendar(calendarEl, selectDateEvent, eventClickEvent, updateDateEvent)
|
||||
calendar.render()
|
||||
}
|
||||
|
||||
@@ -83,7 +78,7 @@ const eventClickEvent = (eventClick: EventClickArg) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/styles/home.variable.module";
|
||||
@use "@/styles/home.variable.module" as variable;
|
||||
|
||||
$cell-height: 3.8vh;
|
||||
$time-slot-width: 70px;
|
||||
@@ -109,7 +104,7 @@ $month-cell-height: 120px;
|
||||
}
|
||||
|
||||
.fc-button-active {
|
||||
background-color: $theme-color;
|
||||
background-color: variable.$theme-color;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +137,7 @@ $month-cell-height: 120px;
|
||||
|
||||
// 表头区域
|
||||
.fc-col-header {
|
||||
color: $theme-color;
|
||||
color: variable.$theme-color;
|
||||
height: 50px;
|
||||
font-size: 1.4rem;
|
||||
background-color: var(--color-record-card-bg);
|
||||
|
||||
@@ -67,11 +67,9 @@ import { defineProps, PropType } from 'vue'
|
||||
import { AlarmClock, Clock } from '@element-plus/icons-vue'
|
||||
import { IPlanInfo } from '@/types/plan.ts'
|
||||
import { getRemainDays, isBeforeToday } from '@/utils/home/planUtil.ts'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { usePlanStore } from '@/store/plan.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
const planStore = usePlanStore()
|
||||
const router = useRouter()
|
||||
@@ -85,17 +83,8 @@ const props = defineProps({
|
||||
|
||||
const editPlanClick = async () => {
|
||||
planStore.planApiType = 'UPDATE'
|
||||
|
||||
if (isMobile()) {
|
||||
planStore.currentPlan = props.plan as IPlanInfo
|
||||
await router.push({ name: 'MobileHomePlanDetailId', params: { id: planStore.currentPlan.id } })
|
||||
} else {
|
||||
planStore.planDialog = {
|
||||
title: '编辑计划',
|
||||
visible: true,
|
||||
data: deepCopyObject(props.plan)
|
||||
}
|
||||
}
|
||||
planStore.currentPlan = props.plan as IPlanInfo
|
||||
await router.push({ name: 'PlanDetailId', params: { id: planStore.currentPlan.id } })
|
||||
}
|
||||
|
||||
const changeCompleteEvent = (value: boolean) => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="common-query">
|
||||
<el-select v-model="planStore.queryInfo.planType"
|
||||
@change="planStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
<el-option v-for="(item, index) in planTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
<div class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>分类:</span>
|
||||
<el-segmented v-model="planStore.queryInfo.planType"
|
||||
:options="planTypeOptions"
|
||||
@change="planStore.refreshInfo()"/>
|
||||
</div>
|
||||
|
||||
<el-select v-model="planStore.queryInfo.orderType"
|
||||
@change="planStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
<el-option v-for="(item, index) in orderTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
<div class="query-item">
|
||||
<span>排序:</span>
|
||||
<el-segmented v-model="planStore.queryInfo.orderType"
|
||||
:options="orderTypeOptions"
|
||||
@change="planStore.refreshInfo()"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,20 +26,20 @@ const planTypeOptions = [{
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
label: '已完成计划',
|
||||
label: '已完成',
|
||||
value: 'completed'
|
||||
},
|
||||
{
|
||||
label: '未完成计划',
|
||||
label: '未完成',
|
||||
value: 'notCompleted'
|
||||
}]
|
||||
|
||||
const orderTypeOptions = [{
|
||||
label: '按时间排序',
|
||||
label: '时间',
|
||||
value: 'date'
|
||||
},
|
||||
{
|
||||
label: '按重要性排序',
|
||||
label: '重要性',
|
||||
value: 'priority'
|
||||
}]
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="life-record-card card-item">
|
||||
<el-image :src="serviceFileRootPath + props.productItem?.imageUrl"
|
||||
:preview-src-list="[serviceFileRootPath + props.productItem?.imageUrl]"
|
||||
fit="contain" alt="暂无成果">
|
||||
<el-image :src="`${props.productItem?.imageUrl}${ThumbImageQuery}`"
|
||||
fit="contain" alt="暂无图片" @click="viewImageClick">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
@@ -10,70 +9,71 @@
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="record-info">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.productItem.name }}</span>
|
||||
<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.productItem.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item amount-item">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
{{ props.productItem.price }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item amount-item">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
{{ props.productItem.price }}
|
||||
</div>
|
||||
<el-tag v-if="props.productItem.expiryDate" type="danger" effect="dark">
|
||||
{{ props.productItem.expiryDate }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="button-info">
|
||||
<el-button type="primary" :icon="Edit" circle :size="'small'"
|
||||
@click="editClick"/>
|
||||
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
|
||||
@click="deleteClick()" />
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-clock" style="color: #6A5ACD"></i>
|
||||
{{ getTotalDay(props.productItem) }}天
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-chart-line" style="color: #FFA500"></i>
|
||||
{{ (props.productItem.price / getTotalDay(props.productItem)).toFixed(2) }}元/天
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ props.productItem.purchaser }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ props.productItem.purchaseDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-clock" style="color: #6A5ACD"></i>
|
||||
{{ props.productItem.totalDay }}天
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-chart-line" style="color: #FFA500"></i>
|
||||
{{ props.productItem.averagePrice }}元/天
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
{{ props.productItem.channel }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ props.productItem.purchaser }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ props.productItem.purchaseDate }}
|
||||
</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 { Edit, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { useProductStore } from '@/store/product.ts'
|
||||
import { defineProps, onMounted, PropType } from 'vue'
|
||||
import { defineProps, PropType } from 'vue'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { IProduct } from '@/types/product.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { ThumbImageQuery } from '@/utils'
|
||||
import { showImagePreview } from 'vant'
|
||||
import { formatDate, getDateDifference } from 'vue3-common/utils/dateUtil'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
|
||||
const productStore = useProductStore()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
productItem: {
|
||||
@@ -82,16 +82,14 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
const viewImageClick = () => {
|
||||
showImagePreview({ images: [props.productItem?.imageUrl], closeable: true })
|
||||
}
|
||||
|
||||
const editClick = async () => {
|
||||
const id = props.productItem?.id
|
||||
productStore.productApiType = 'UPDATE'
|
||||
|
||||
productStore.currentProduct = props.productItem as IProduct
|
||||
await router.push({ name: 'ProductDetailId', params: { id } })
|
||||
productStore.currentProduct = deepCopyObject(props.productItem) as IProduct
|
||||
productStore.productDrawer = true
|
||||
}
|
||||
|
||||
const deleteClick = () => {
|
||||
@@ -100,6 +98,14 @@ const deleteClick = () => {
|
||||
productStore.handleProductApi(props.productItem as IProduct)
|
||||
})
|
||||
}
|
||||
|
||||
const getTotalDay = (product: IProduct) => {
|
||||
if (product.expiryDate) {
|
||||
return getDateDifference(product.purchaseDate, product.expiryDate, 'days')
|
||||
} else {
|
||||
return getDateDifference(product.purchaseDate, formatDate(new Date()), 'days')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
168
src/components/Product/ProductDrawer.vue
Normal file
168
src/components/Product/ProductDrawer.vue
Normal 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>
|
||||
@@ -35,7 +35,7 @@ const queryOrderList = [
|
||||
label: '购买日期',
|
||||
value: 'date'
|
||||
}, {
|
||||
label: '日均价格',
|
||||
label: '总价格',
|
||||
value: 'price'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,24 +12,15 @@
|
||||
<el-card class="record-card">
|
||||
<span class="record-content">旅行人:{{ item.persons.join('-') }}</span>
|
||||
<el-image v-for="(image, index) in item.imageList" :key="index"
|
||||
:src="serviceFileRootPath + image"
|
||||
:preview-src-list="[serviceFileRootPath + image]"
|
||||
fit="contain" alt="暂无图片" class="record-image"/>
|
||||
:src="`${image}${ThumbImageQuery}`"
|
||||
fit="cover" alt="暂无图片" class="record-image"
|
||||
@click="viewImageClick(item, index)"/>
|
||||
|
||||
<video v-for="(video, index) in item.videoList"
|
||||
:key="index" controls style="width: 60%;min-height: 200px;">
|
||||
<source :src="serviceFileRootPath + video" type="video/mp4">
|
||||
:key="index" controls style="width: 100%;">
|
||||
<source :src="video" type="video/mp4">
|
||||
您的浏览器不支持 HTML5 视频
|
||||
</video>
|
||||
|
||||
<!-- <el-carousel v-if="item.imageList.length > 0"-->
|
||||
<!-- arrow="always">-->
|
||||
<!-- <el-carousel-item v-for="(item, index) in item.imageList" :key="index">-->
|
||||
<!-- -->
|
||||
<!-- </el-carousel-item>-->
|
||||
<!-- </el-carousel>-->
|
||||
|
||||
<!-- <el-empty v-else description="暂无记录"/>-->
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
@@ -41,7 +32,13 @@
|
||||
<script setup lang="ts">
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { showImagePreview } from 'vant'
|
||||
import { ITravelRecord } from '@/types/travel.ts'
|
||||
import { ThumbImageQuery } from '@/utils'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
const viewImageClick = (record: ITravelRecord, index: number) => {
|
||||
showImagePreview({ images: record.imageList, closeable: true, startPosition: index })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,68 +1,63 @@
|
||||
<template>
|
||||
<div class="life-record-card card-item">
|
||||
<el-carousel v-if="props.travelItem?.recordList.length > 0"
|
||||
arrow="always" @change="changeTravelEvent">
|
||||
<el-carousel-item v-for="(item, index) in props.travelItem?.recordList" :key="index">
|
||||
<el-image :src="serviceFileRootPath + item.imageList[0]"
|
||||
fit="contain" alt="暂无图片">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<el-image v-if="props.travelItem?.recordList.length > 0"
|
||||
:src="`${props.travelItem?.recordList[0].imageList[0]}${ThumbImageQuery}`"
|
||||
fit="contain" alt="暂无成果" @click="viewImageClick">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-empty v-else description="暂无记录"/>
|
||||
|
||||
<div class="record-info">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.travelItem.name }}</span>
|
||||
<van-swipe-cell>
|
||||
<div class="record-info" @click="viewClick">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.travelItem.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item location-item">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
{{ props.travelItem.area.join('-') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.travelItem?.recordList.length > 0" class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ parsePerson(props.travelItem.recordList[cardInfo.currentIndex].persons) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ formatDate(props.travelItem.recordList[cardInfo.currentIndex].date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<span>暂无记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item location-item">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
{{ props.travelItem.area.join('-') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.travelItem?.recordList.length > 0" class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ parsePerson(props.travelItem.recordList[cardInfo.currentIndex].persons) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ formatDate(props.travelItem.recordList[cardInfo.currentIndex].date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<span>暂无记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #right>
|
||||
<van-button square type="primary" text="编辑" style="height: 100%;"
|
||||
@click="editClick"/>
|
||||
<van-button square type="danger" text="删除" style="height: 100%;"
|
||||
@click="deleteClick()"/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { defineProps, PropType, reactive } from 'vue'
|
||||
@@ -70,8 +65,9 @@ import { useRouter } from 'vue-router'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { ITravelSpot } from '@/types/travel.ts'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { parsePerson } from '@/utils/home/travelUtil.ts'
|
||||
import { showImagePreview } from 'vant'
|
||||
import { RawImageQuery, ThumbImageQuery } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const travelStore = useTravelStore()
|
||||
@@ -88,6 +84,13 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const viewImageClick = () => {
|
||||
const imageList = props.travelItem.recordList.flatMap((record) => {
|
||||
return record.imageList.map((item) => `${item}${RawImageQuery}`)
|
||||
})
|
||||
showImagePreview({ images: imageList, closeable: true })
|
||||
}
|
||||
|
||||
const editClick = async () => {
|
||||
const id = props.travelItem?.id
|
||||
travelStore.travelSpotApiType = 'UPDATE'
|
||||
@@ -108,10 +111,6 @@ const deleteClick = () => {
|
||||
travelStore.handleTravelSpotApi(props.travelItem)
|
||||
})
|
||||
}
|
||||
|
||||
const changeTravelEvent = (current: number) => {
|
||||
cardInfo.currentIndex = current
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="isMobile()" class="mobile-common-query card-item">
|
||||
<div class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>城市:</span>
|
||||
<el-segmented v-model="travelStore.querySpot.category"
|
||||
@@ -7,20 +7,10 @@
|
||||
@change="queryTravelSegmented"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="common-query">
|
||||
<el-select v-model="travelStore.queryDateType"
|
||||
@change="travelStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ICategory } from '@/types'
|
||||
|
||||
@@ -28,15 +18,6 @@ const travelStore = useTravelStore()
|
||||
|
||||
const queryCategoryList = ref([] as ICategory[])
|
||||
|
||||
const dateTypeOptions = [{
|
||||
label: '按月选择',
|
||||
value: 'month'
|
||||
},
|
||||
{
|
||||
label: '按年选择',
|
||||
value: 'year'
|
||||
}]
|
||||
|
||||
onMounted(async () => {
|
||||
await travelStore.queryTravelCategory()
|
||||
initCategoryList()
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType, defineProps, computed } from 'vue'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { useKpiStore } from '@/store/kpi.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -42,16 +40,8 @@ const editClick = async (kpi: IKpi) => {
|
||||
kpiStore.kpiApiType = 'UPDATE'
|
||||
kpiStore.currentKpi = kpi
|
||||
|
||||
if (isMobile()) {
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'KpiDetailId', params: { id } })
|
||||
} else {
|
||||
kpiStore.kpiDialog = {
|
||||
title: '编辑绩效',
|
||||
visible: true,
|
||||
data: deepCopyObject(kpiStore.currentKpi)
|
||||
}
|
||||
}
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'KpiDetailId', params: { id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<router-link to="/home" @click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-house"></i>
|
||||
<span>主页</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('bill')" to="/bill/data"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
@@ -15,10 +10,10 @@
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('bill')" to="/bill/setting"
|
||||
<router-link v-if="path.includes('bill')" to="/bill/agent"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>设置</span>
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<span>分析</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('food')" to="/food/recipe"
|
||||
@@ -26,6 +21,11 @@
|
||||
<i class="fa-solid fa-utensils"></i>
|
||||
<span>菜谱</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('food')" to="/food/timeline"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-timeline"></i>
|
||||
<span>时间轴</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('food')" to="/food/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
@@ -43,6 +43,17 @@
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('health')" to="/health/calendar"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('health')" to="/health/chart"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('plan')" to="/plan/calendar"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
@@ -70,6 +81,11 @@
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('period')" to="/period/status"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('period')" to="/period/setting"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
@@ -82,6 +98,12 @@
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('car')" to="/car/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('journal')" to="/journal/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
@@ -110,6 +132,11 @@
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('ledger')" to="/ledger/stats"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('password')" to="/password/list"
|
||||
@click="routerClick()" class="item">
|
||||
@@ -117,6 +144,12 @@
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('award')" to="/award/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('server')" to="/server/monitor"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-desktop"></i>
|
||||
@@ -143,12 +176,6 @@
|
||||
<i class="fa-solid fa-box-archive"></i>
|
||||
<span>归档</span>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/profile/index"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>我的</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
type="info" :icon="Back" size="small"
|
||||
circle color="#FFFFFF"
|
||||
@click="goBackClick" />
|
||||
<div v-else class="logo-container">
|
||||
<div v-else class="logo-container" @click="goHomeClick">
|
||||
<img :src="getAssetsImageFile('logo/logo.png')" alt="暂无图片" class="logo"/>
|
||||
<span>{{ appStore.appName }}</span>
|
||||
</div>
|
||||
@@ -44,6 +44,10 @@ const router = useRouter()
|
||||
|
||||
const title = computed(() => router.currentRoute.value.meta.title)
|
||||
|
||||
const goHomeClick = () => {
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
const goBackClick = () => {
|
||||
appStore.isShowMobileBack = false
|
||||
history.back()
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<section class="layout-mobile-home">
|
||||
<title-bar />
|
||||
<van-config-provider :theme="appStore.isDark ? 'dark' : 'light'">
|
||||
<section class="layout-mobile-home"
|
||||
:class="{ 'hide-tab-bar': appStore.isShowMobileBack }">
|
||||
<title-bar />
|
||||
|
||||
<router-view />
|
||||
<router-view />
|
||||
|
||||
<tab-bar />
|
||||
</section>
|
||||
<tab-bar v-if="!appStore.isShowMobileBack"/>
|
||||
</section>
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TitleBar from '@/layout/TitleBar.vue'
|
||||
import TabBar from '@/layout/TabBar.vue'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -25,6 +25,9 @@ import 'animate.css'
|
||||
// 注册svg-icon
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
// vant图片预览样式
|
||||
import 'vant/es/image-preview/style'
|
||||
|
||||
// 创建Vue3实例
|
||||
const app = createApp(App)
|
||||
// 使用路由
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
230
src/mock/bill.ts
230
src/mock/bill.ts
@@ -1,230 +0,0 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
const billMock: MockMethod[] = [
|
||||
{
|
||||
url: '/home-service/bill/pay',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
id: 602111394902085,
|
||||
name: '支付宝',
|
||||
icon: 'fa-brands fa-alipay',
|
||||
color: '#2220B2'
|
||||
},
|
||||
{
|
||||
id: 602130528899141,
|
||||
name: '招商银行',
|
||||
icon: 'fa-solid fa-money-check',
|
||||
color: '#3E5BFF'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/bill/book',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
id: 601082142339141,
|
||||
name: '日常账本',
|
||||
icon: 'fa-solid fa-book',
|
||||
color: '#1E29FF'
|
||||
},
|
||||
{
|
||||
id: 601083198201925,
|
||||
name: '汽车账本',
|
||||
icon: 'fa-solid fa-car',
|
||||
color: '#2E8B57'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/bill/category',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
bookName: '日常账本',
|
||||
color: '#FF1ED6',
|
||||
icon: 'fa-brands fa-shopify',
|
||||
id: 601082333016133,
|
||||
name: '购物',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
bookName: '日常账本',
|
||||
color: '#3C1EFF',
|
||||
icon: 'fa-solid fa-utensils',
|
||||
id: 601082911907909,
|
||||
name: '餐饮',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
bookName: '日常账本',
|
||||
color: '#FF1ED6',
|
||||
icon: 'fa-brands fa-shopify',
|
||||
id: 601082333016133,
|
||||
name: '购物',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
bookName: '日常账本',
|
||||
color: '#FF3C1E',
|
||||
icon: 'fa-solid fa-sack-dollar',
|
||||
id: 601422191939653,
|
||||
name: '工资',
|
||||
type: 'income'
|
||||
},
|
||||
{
|
||||
bookName: '汽车账本',
|
||||
color: '#1ED2FF',
|
||||
icon: 'fa-solid fa-car',
|
||||
id: 601083391709253,
|
||||
name: '汽车保养',
|
||||
type: 'expensive'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/bill/summary',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
amount: 213.32,
|
||||
bookName: '日常账本',
|
||||
category: '购物',
|
||||
content: '买东西',
|
||||
date: '2024-11-11',
|
||||
id: 611071966380101,
|
||||
location: '埃斯顿',
|
||||
payAccount: '支付宝',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
amount: 32.22,
|
||||
bookName: '日常账本',
|
||||
category: '餐饮',
|
||||
content: '肯德基套餐',
|
||||
date: '2024-10-17',
|
||||
id: 601083046211653,
|
||||
location: '肯德基',
|
||||
payAccount: '招商银行',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
amount: 213.32,
|
||||
bookName: '日常账本',
|
||||
category: '购物',
|
||||
content: '买了一件衣服',
|
||||
date: '2024-10-17',
|
||||
id: 601082570399813,
|
||||
location: '江宁奥特莱斯',
|
||||
payAccount: '招商银行',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
amount: 55555,
|
||||
bookName: '日常账本',
|
||||
category: '工资',
|
||||
content: '工资收入',
|
||||
date: '2024-10-17',
|
||||
id: 601422418636869,
|
||||
location: '埃斯顿',
|
||||
payAccount: '招商银行',
|
||||
remark: '',
|
||||
type: 'income'
|
||||
},
|
||||
{
|
||||
amount: 2125.23,
|
||||
bookName: '日常账本',
|
||||
category: '餐饮',
|
||||
content: '聚餐',
|
||||
date: '2024-10-16',
|
||||
id: 601884663570501,
|
||||
location: '江宁',
|
||||
payAccount: '支付宝',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
amount: 532.22,
|
||||
bookName: '日常账本',
|
||||
category: '汽车保养',
|
||||
content: '汽车保养',
|
||||
date: '2024-10-16',
|
||||
id: 601083666997317,
|
||||
location: '溧水4S店',
|
||||
payAccount: '支付宝',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
},
|
||||
{
|
||||
amount: 122.32,
|
||||
bookName: '日常账本',
|
||||
category: '餐饮',
|
||||
content: '老表聚餐',
|
||||
date: '2024-09-12',
|
||||
id: 601876599046213,
|
||||
location: '江宁',
|
||||
payAccount: '招商银行',
|
||||
remark: '',
|
||||
type: 'expensive'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/bill/stats/year',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '2024-10',
|
||||
value: 2902.9
|
||||
},
|
||||
{
|
||||
name: '2024-09',
|
||||
value: 122.32
|
||||
},
|
||||
{
|
||||
name: '2024-11',
|
||||
value: 213.32
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/bill/stats/month',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/bill/stats/book',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '日常账本',
|
||||
value: 3238.54
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/bill/stats/category',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '购物',
|
||||
value: 426.55
|
||||
},
|
||||
{
|
||||
name: '餐饮',
|
||||
value: 2279.77
|
||||
},
|
||||
{
|
||||
name: '汽车保养',
|
||||
value: 532.22
|
||||
}]
|
||||
}
|
||||
}]
|
||||
|
||||
export default billMock
|
||||
159
src/mock/blog.ts
159
src/mock/blog.ts
@@ -1,159 +0,0 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
const blogMock: MockMethod[] = [
|
||||
{
|
||||
url: '/blog-service/blog/stats',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
blogCount: 10,
|
||||
categoryCount: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/blog-service/blog/latest',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
id: 591259203186757,
|
||||
title: '一篇标题很长的博客嘿嘿嘿哦哦哦哦哦哦哦'
|
||||
},
|
||||
{
|
||||
id: 591837462601797,
|
||||
title: '锵锵锵'
|
||||
},
|
||||
{
|
||||
id: 591137032622149,
|
||||
title: '台风'
|
||||
},
|
||||
{
|
||||
id: 591317392101445,
|
||||
title: '一篇新的2336'
|
||||
},
|
||||
{
|
||||
id: 591317151170629,
|
||||
title: '新的一篇文章233500'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/blog-service/blog/category',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
count: 7,
|
||||
name: '学习'
|
||||
},
|
||||
{
|
||||
count: 3,
|
||||
name: '工作'
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/blog-service/blog/page',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
current: 1,
|
||||
pages: 2,
|
||||
records: [
|
||||
{
|
||||
category: '学习',
|
||||
content: null,
|
||||
createTime: '2024-09-18 10:52:50',
|
||||
id: 591837462601797,
|
||||
isGreat: true,
|
||||
readDuration: 1.27,
|
||||
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白,月下,匆匆的步履停顿,凝望,清点心中如帛的情丝。\n月透着初秋的韵致,想念已悄然坠临,无语凝噎的痛楚在心间扩散,丰盈的思念在秋夜沉迷,抒这夜的情愁,',
|
||||
title: '锵锵锵',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-18 10:57:20',
|
||||
visitCount: 19,
|
||||
wordCount: 761
|
||||
},
|
||||
{
|
||||
category: '学习',
|
||||
content: null,
|
||||
createTime: '2024-09-16 11:22:47',
|
||||
id: 591837462601797,
|
||||
isGreat: true,
|
||||
readDuration: 1.27,
|
||||
summary: '# 台风12312321312312',
|
||||
title: '锵锵锵',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-17 00:16:31',
|
||||
visitCount: 19,
|
||||
wordCount: 761
|
||||
},
|
||||
{
|
||||
category: '学习',
|
||||
content: null,
|
||||
createTime: '2024-09-18 10:52:50',
|
||||
id: 591837462601797,
|
||||
isGreat: true,
|
||||
readDuration: 1.27,
|
||||
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白,月下,匆匆的步履停顿,凝望,清点心中如帛的情丝。\n月透着初秋的韵致,想念已悄然坠临,无语凝噎的痛楚在心间扩散,丰盈的思念在秋夜沉迷,抒这夜的情愁,',
|
||||
title: '锵锵锵',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-18 10:57:20',
|
||||
visitCount: 19,
|
||||
wordCount: 761
|
||||
},
|
||||
{
|
||||
category: '学习',
|
||||
content: null,
|
||||
createTime: '2024-09-18 10:52:50',
|
||||
id: 591837462601797,
|
||||
isGreat: true,
|
||||
readDuration: 1.27,
|
||||
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白,月下,匆匆的步履停顿,凝望,清点心中如帛的情丝。\n月透着初秋的韵致,想念已悄然坠临,无语凝噎的痛楚在心间扩散,丰盈的思念在秋夜沉迷,抒这夜的情愁,',
|
||||
title: '锵锵锵',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-18 10:57:20',
|
||||
visitCount: 19,
|
||||
wordCount: 761
|
||||
},
|
||||
{
|
||||
category: '学习',
|
||||
content: null,
|
||||
createTime: '2024-09-18 10:52:50',
|
||||
id: 591837462601797,
|
||||
isGreat: true,
|
||||
readDuration: 1.27,
|
||||
summary: '那半弯新月悬挂于空,洒落一地的晶莹。\n\n夜已被月洗白,月下,匆匆的步履停顿,凝望,清点心中如帛的情丝。\n月透着初秋的韵致,想念已悄然坠临,无语凝噎的痛楚在心间扩散,丰盈的思念在秋夜沉迷,抒这夜的情愁,',
|
||||
title: '锵锵锵',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-18 10:57:20',
|
||||
visitCount: 19,
|
||||
wordCount: 761
|
||||
}
|
||||
],
|
||||
size: 5,
|
||||
total: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/blog-service/blog/content/591837462601797',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
category: '学习',
|
||||
content: '# 台风12312321312312',
|
||||
createTime: '2024-09-16 11:22:47',
|
||||
id: 591137032622149,
|
||||
isGreat: true,
|
||||
readDuration: 0.03,
|
||||
summary: '# 台风12312321312312',
|
||||
title: '台风',
|
||||
topValue: 3,
|
||||
updateTime: '2024-09-17 00:16:31',
|
||||
visitCount: 1,
|
||||
wordCount: 17
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
export default blogMock
|
||||
@@ -1,117 +0,0 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
const budgetMock: MockMethod[] = [
|
||||
{
|
||||
url: '/home-service/budget/category',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return ['结婚预算']
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/budget',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
actual: 26000,
|
||||
budget: 23123,
|
||||
category: '结婚预算',
|
||||
content: '10.2午宴',
|
||||
id: 611115630714949,
|
||||
payTime: null,
|
||||
project: '婚宴',
|
||||
remake: '5桌',
|
||||
vendor: '世纪缘酒店'
|
||||
},
|
||||
{
|
||||
actual: 680,
|
||||
budget: 700,
|
||||
category: '结婚预算',
|
||||
content: '看结婚日子',
|
||||
id: 611116171505733,
|
||||
payTime: null,
|
||||
project: '吉日',
|
||||
remake: '',
|
||||
vendor: '算命先生'
|
||||
},
|
||||
{
|
||||
actual: 0,
|
||||
budget: 0,
|
||||
category: '结婚预算',
|
||||
content: '化妆师',
|
||||
id: 611404639944773,
|
||||
payTime: null,
|
||||
project: '婚庆',
|
||||
remake: '',
|
||||
vendor: ''
|
||||
},
|
||||
{
|
||||
actual: 0,
|
||||
budget: 0,
|
||||
category: '结婚预算',
|
||||
content: '主持人',
|
||||
id: 611404716798021,
|
||||
payTime: null,
|
||||
project: '婚庆',
|
||||
remake: '',
|
||||
vendor: ''
|
||||
},
|
||||
{
|
||||
actual: 10000,
|
||||
budget: 10000,
|
||||
category: '结婚预算',
|
||||
content: '婚车',
|
||||
id: 611404772474949,
|
||||
payTime: null,
|
||||
project: '婚庆',
|
||||
remake: '',
|
||||
vendor: ''
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/budget/stats/rank',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '10.2午宴',
|
||||
value: 26000
|
||||
},
|
||||
{
|
||||
name: '婚车',
|
||||
value: 10000
|
||||
},
|
||||
{
|
||||
name: '看结婚日子',
|
||||
value: 680
|
||||
},
|
||||
{
|
||||
name: '化妆师',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: '主持人',
|
||||
value: 0
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/budget/stats/project',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '婚宴',
|
||||
value: 26000
|
||||
},
|
||||
{
|
||||
name: '吉日',
|
||||
value: 680
|
||||
},
|
||||
{
|
||||
name: '婚庆',
|
||||
value: 10000
|
||||
}]
|
||||
}
|
||||
}]
|
||||
|
||||
export default budgetMock
|
||||
166
src/mock/food.ts
166
src/mock/food.ts
@@ -1,166 +0,0 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
const foodMock: MockMethod[] = [
|
||||
{
|
||||
url: '/home-service/food/origin',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return ['中餐']
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/food/summary/page',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
current: 1,
|
||||
pages: 1,
|
||||
records: [
|
||||
{
|
||||
id: 614660173221957,
|
||||
name: '鱼头汤',
|
||||
recommendRate: 5,
|
||||
workList: [
|
||||
{
|
||||
finishBy: '哥哥',
|
||||
finishTime: '2024-11-13',
|
||||
id: 614673962934342,
|
||||
imageUrl: 'files\\food\\image2.png'
|
||||
},
|
||||
{
|
||||
finishBy: '哥哥',
|
||||
finishTime: '2024-11-20',
|
||||
id: 614673962934341,
|
||||
imageUrl: 'files\\food\\image1.png'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 614665900060741,
|
||||
name: '红烧肉',
|
||||
recommendRate: 3,
|
||||
workList: []
|
||||
},
|
||||
{
|
||||
id: 614668663291973,
|
||||
name: '红烧排骨',
|
||||
recommendRate: 5,
|
||||
workList: []
|
||||
}
|
||||
],
|
||||
size: 6,
|
||||
total: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/food/614660173221957',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
attention: '',
|
||||
category: '淮扬菜',
|
||||
cuisine: '清炖',
|
||||
id: 614660173221957,
|
||||
materialList: [
|
||||
{
|
||||
amount: '一个鱼头',
|
||||
id: 614673962901573,
|
||||
name: '胖头鱼',
|
||||
type: '主料'
|
||||
},
|
||||
{
|
||||
amount: '一块',
|
||||
id: 614673962901574,
|
||||
name: '豆腐',
|
||||
type: '辅料'
|
||||
},
|
||||
{
|
||||
amount: '1小块',
|
||||
id: 614673962901575,
|
||||
name: '生姜',
|
||||
type: '调料'
|
||||
},
|
||||
{
|
||||
amount: '2勺',
|
||||
id: 614673962901576,
|
||||
name: '生抽',
|
||||
type: '调料'
|
||||
}
|
||||
],
|
||||
name: '鱼头汤',
|
||||
nutritiveValue: '',
|
||||
origin: '中餐',
|
||||
recommendRate: 5,
|
||||
remark: null,
|
||||
stepList: [{
|
||||
content: '先把鱼头剁开',
|
||||
id: 614673962917957,
|
||||
imageUrl: '',
|
||||
sort: 0
|
||||
},
|
||||
{
|
||||
content: '洗鱼',
|
||||
id: 614673962917958,
|
||||
imageUrl: '',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
content: '倒水放砂锅里煮',
|
||||
id: 614673962917959,
|
||||
imageUrl: '',
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
content: '放豆腐',
|
||||
id: 614673962917960,
|
||||
imageUrl: '',
|
||||
sort: 3
|
||||
},
|
||||
{
|
||||
content: '放盐',
|
||||
id: 614673962917961,
|
||||
imageUrl: '',
|
||||
sort: 4
|
||||
}],
|
||||
workList: [
|
||||
{
|
||||
finishBy: '哥哥',
|
||||
finishTime: '2024-11-20',
|
||||
id: 614673962934341,
|
||||
imageUrl: 'files\\food\\image1.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/food/stats/category',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '淮扬菜',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
name: '鲁菜',
|
||||
value: 2
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/home-service/food/stats/cuisine',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return [{
|
||||
name: '清炖',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
name: '红烧',
|
||||
value: 2
|
||||
}]
|
||||
}
|
||||
}]
|
||||
|
||||
export default foodMock
|
||||
@@ -1,9 +0,0 @@
|
||||
import BillMock from './bill'
|
||||
import BudgetMock from './budget'
|
||||
import FoodMock from './food'
|
||||
|
||||
export default [
|
||||
...BillMock,
|
||||
...BudgetMock,
|
||||
...FoodMock
|
||||
]
|
||||
@@ -32,7 +32,8 @@ export const useAnniversaryStore = defineStore('anniversary', {
|
||||
title: '',
|
||||
data: {}
|
||||
} as IDialog<IAnniversary>,
|
||||
isRefresh: false
|
||||
isRefresh: false,
|
||||
isLoading: false
|
||||
}),
|
||||
actions: {
|
||||
async queryAnniversaryList() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { setCookie, getCookie, getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { calcDarken, calcLighten } from '@/utils/element/elColor.ts'
|
||||
|
||||
export const useAppStore = defineStore('ipp', {
|
||||
state: () => ({
|
||||
@@ -89,7 +90,17 @@ export const useAppStore = defineStore('ipp', {
|
||||
return getLocalStorage('theme-color') as string || '#009FA8'
|
||||
},
|
||||
setThemeColor(value: string) {
|
||||
document.documentElement.style.setProperty('--el-color-primary', value)
|
||||
const root = document.documentElement
|
||||
|
||||
root.style.setProperty('--el-color-primary', value)
|
||||
root.style.setProperty('--el-color-primary-light-3', calcLighten(value, 30))
|
||||
root.style.setProperty('--el-color-primary-light-5', calcLighten(value, 50))
|
||||
root.style.setProperty('--el-color-primary-light-7', calcLighten(value, 70))
|
||||
root.style.setProperty('--el-color-primary-light-8', calcLighten(value, 80))
|
||||
root.style.setProperty('--el-color-primary-light-9', calcLighten(value, 90))
|
||||
root.style.setProperty('--el-color-primary-dark-2', calcDarken(value, 20))
|
||||
|
||||
root.style.setProperty('--van-primary-color', value)
|
||||
setLocalStorage('theme-color', value)
|
||||
}
|
||||
}
|
||||
|
||||
70
src/store/award.ts
Normal file
70
src/store/award.ts
Normal 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: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -8,13 +8,12 @@ import {
|
||||
queryBillBookApi,
|
||||
queryBillCategoryApi, queryBillPayApi,
|
||||
queryBillRecordApi,
|
||||
queryBillStatsApi, queryBillSummaryApi, updateBillRecordApi
|
||||
queryBillStatsApi, queryBillSummaryApi, searchBillSummaryApi, updateBillRecordApi
|
||||
} from '@/apis/bill'
|
||||
import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
|
||||
import { formatDate, getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
|
||||
import { IDateType, ImagePathEnum } from '@/types'
|
||||
import { IDialog, IStatsChart, IHandleApi } from 'vue3-common/types'
|
||||
import { IStatsChart, IHandleApi } from 'vue3-common/types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
|
||||
export const useBillStore = defineStore('bill', {
|
||||
state: () => ({
|
||||
@@ -35,7 +34,7 @@ export const useBillStore = defineStore('bill', {
|
||||
billPayList: [] as IBillPay[],
|
||||
billBookList: [] as IBillBook[],
|
||||
billCategoryList: [] as IBillCategory[],
|
||||
billType: 'expensive' as IBillType,
|
||||
billType: 'expense' as IBillType,
|
||||
billDateType: 'month' as IDateType,
|
||||
billDateList: [] as string[],
|
||||
billImagePath: ImagePathEnum.BILL,
|
||||
@@ -44,15 +43,16 @@ export const useBillStore = defineStore('bill', {
|
||||
billYear: getCurrentYear(),
|
||||
billMonth: getCurrentMonth()
|
||||
},
|
||||
billSearchForm: {
|
||||
content: '',
|
||||
category: '',
|
||||
location: ''
|
||||
},
|
||||
billDateStatsList: [] as IStatsChart[],
|
||||
billBookStatsList: [] as IStatsChart[],
|
||||
billCategoryStatsList: [] as IStatsChart[],
|
||||
billApiType: 'ADD' as IHandleApi,
|
||||
billDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
data: {}
|
||||
} as IDialog<IBillRecord>,
|
||||
billDrawer: false,
|
||||
isRefresh: false,
|
||||
isLoading: false
|
||||
}),
|
||||
@@ -119,31 +119,39 @@ export const useBillStore = defineStore('bill', {
|
||||
default:
|
||||
break
|
||||
}
|
||||
this.billDialog.visible = false
|
||||
|
||||
if (!isMobile()) {
|
||||
await this.refreshInfo()
|
||||
}
|
||||
await this.refreshInfo()
|
||||
},
|
||||
async refreshInfo() {
|
||||
this.isLoading = true
|
||||
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
|
||||
getBillDateList() {
|
||||
const { billYear, billMonth } = this.billQueryForm
|
||||
const date = this.billDateType === 'year' ? billYear : billMonth
|
||||
this.billDateList = getStartEndDate(date, this.billDateType)
|
||||
},
|
||||
async refreshInfo(isStats = false) {
|
||||
this.isLoading = true
|
||||
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
|
||||
this.getBillDateList()
|
||||
|
||||
await this.queryBillSummary(bookName)
|
||||
await this.queryBillStats(bookName)
|
||||
|
||||
if (isStats) {
|
||||
await this.queryBillStats(bookName)
|
||||
}
|
||||
|
||||
this.isRefresh = !this.isRefresh
|
||||
this.isLoading = false
|
||||
},
|
||||
async searchInfo() {
|
||||
this.isLoading = true
|
||||
this.billRecordList = await searchBillSummaryApi(this.billSearchForm)
|
||||
this.isLoading = false
|
||||
},
|
||||
getEmptyBillRecord(): IBillRecord {
|
||||
return {
|
||||
amount: 0,
|
||||
bookName: '',
|
||||
category: '',
|
||||
content: '',
|
||||
date: '',
|
||||
date: formatDate(new Date()),
|
||||
location: '',
|
||||
payAccount: '',
|
||||
remark: '',
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
IBlogLatest, IBlogNestedComment, IBlogStats, INewBlog, IQueryBlog
|
||||
} from '@/types/blog'
|
||||
import {
|
||||
addBlogApi, addBlogCommentApi, deleteBlogApi,
|
||||
queryBlogApi, queryBlogByConditionApi, queryBlogByIdApi,
|
||||
queryBlogCategoryApi, queryBlogCommentApi, queryBlogStatsApi, queryLatestBlogApi, updateBlogApi
|
||||
queryBlogCategoryApi, queryBlogCommentApi, queryBlogStatsApi, queryLatestBlogApi
|
||||
} from '@/apis/blog.ts'
|
||||
import { IHandleApi } from 'vue3-common/types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ImagePathEnum } from '@/types'
|
||||
import { flattenToTwoLevel } from '@/utils/blog/blogUtil.ts'
|
||||
|
||||
@@ -90,10 +88,6 @@ export const useBlogStore = defineStore('blog', {
|
||||
async queryBlogComment(blogId: number) {
|
||||
this.blogCommentList = await queryBlogCommentApi(blogId)
|
||||
},
|
||||
async addBlogComment(blogComment: IBlogComment) {
|
||||
await addBlogCommentApi(blogComment)
|
||||
await this.queryBlogById(blogComment.blogId)
|
||||
},
|
||||
async queryBlogCategory() {
|
||||
this.blogCategoryList = await queryBlogCategoryApi()
|
||||
},
|
||||
@@ -103,25 +97,6 @@ export const useBlogStore = defineStore('blog', {
|
||||
async queryLatestBlog() {
|
||||
this.latestBlogList = await queryLatestBlogApi()
|
||||
},
|
||||
async handleBlogApi(id?: number) {
|
||||
switch (this.blogApiType) {
|
||||
case 'ADD':
|
||||
await addBlogApi(this.newBlog)
|
||||
ElMessage.success('新增博客成功')
|
||||
break
|
||||
case 'UPDATE':
|
||||
await updateBlogApi(this.newBlog.id as number, this.newBlog)
|
||||
ElMessage.success('更新博客成功')
|
||||
break
|
||||
case 'DELETE':
|
||||
await deleteBlogApi(id as number)
|
||||
ElMessage.success('删除博客成功')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
await this.refreshInfo()
|
||||
},
|
||||
async refreshInfo() {
|
||||
this.pageInfo.currentPage = 1
|
||||
await this.queryBlog()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user