Compare commits

...

97 Commits

Author SHA1 Message Date
dzl
34c866b221 样式修改以及接口完善 2025-10-24 16:15:40 +08:00
dzl
545c90f13b 样式调整,bug修复 2025-10-23 17:26:14 +08:00
dzl
db228d91ed 样式调整,bug修复 2025-10-22 17:26:55 +08:00
dzl
2b3f00ca2b 页面调整,完善支付宝支付 2025-10-21 17:28:59 +08:00
dzl
d2816a7f78 支付方式显示 2025-10-17 14:29:22 +08:00
dzl
f058a6f4e2 完善优惠券管理页面 2025-10-16 17:29:49 +08:00
dzl
d1327abeae 新增优惠券的选择与使用 2025-10-15 17:25:43 +08:00
dzl
0dc9f602f1 新增优惠卷管理相关 2025-10-14 16:24:29 +08:00
dzl
9dc618b255 支付及优惠券页面更新 2025-10-13 17:27:49 +08:00
60a3cba975 新增优惠券领取按钮 2025-10-12 17:14:54 +08:00
dzl
64868b47b3 优惠券更改 2025-10-11 17:32:43 +08:00
dzl
7146a0330e 页面更改 2025-10-10 17:33:26 +08:00
dzl
4b9277282a 功能增加 2025-10-09 17:24:09 +08:00
dzl
513819ddce 删除多余代码 2025-09-30 17:11:08 +08:00
dzl
2b66a28446 新版商城页面更新 2025-09-30 16:56:00 +08:00
dzl
4e56b498b8 样式修改 2025-09-29 17:29:28 +08:00
dzl
743abaebb5 增加支付方式 2025-09-29 15:01:55 +08:00
dzl
a0d8823b02 细节调整 2025-09-28 11:53:37 +08:00
dzl
afaa76be91 删除多余函数 2025-09-23 17:30:42 +08:00
dzl
99c692b048 删除多余函数 2025-09-23 17:17:40 +08:00
dzl
baea20728e 增加自动切换功能 2025-09-23 16:28:24 +08:00
dzl
b1a91f3888 bug修改 2025-09-23 16:05:10 +08:00
dzl
b994378c36 曾加显示选项 2025-09-23 15:59:31 +08:00
dzl
713da3a160 删除多余文本 2025-09-23 13:43:32 +08:00
dzl
94bfc567f2 更改已读判定 2025-09-23 12:20:12 +08:00
dzl
b7933f8cdf 更改协议内容 2025-09-23 11:45:33 +08:00
dzl
d928642652 删除校验 2025-09-18 14:41:09 +08:00
dzl
0a1bcb3cd4 样式更改 2025-09-17 10:44:36 +08:00
dzl
f73418546e 删除 2025-09-16 18:05:08 +08:00
dzl
586056413b 删除提现相关功能 2025-09-16 17:00:42 +08:00
dzl
278a1dd763 文本更改 2025-09-16 15:04:56 +08:00
dzl
fc6d7b9c5c 修复注册省市区选项超过宽度的bug 2025-09-16 11:23:05 +08:00
dzl
bf2ca554d4 删除无用接口调用 2025-09-15 15:25:35 +08:00
dzl
5130bfd12e 更改更新弹窗字体颜色 2025-09-12 11:38:33 +08:00
dzl
1bf29eaa65 更改更新弹窗样式 2025-09-12 11:00:55 +08:00
dzl
0710268420 Merge branch 'master' of http://49.232.99.129:3000/admin/jurong_circle_frontdesk 2025-09-12 09:38:37 +08:00
dzl
e23e4cf248 更改弹窗高度 2025-09-12 09:38:06 +08:00
c13ee7acd6 [修改]
个人中心退出登录样式修改
2025-09-11 17:04:05 +08:00
dzl
ba989b88f7 文本更改 2025-09-11 14:39:52 +08:00
dzl
d0d8235d27 增加重定向 2025-09-11 14:35:41 +08:00
2bfdf5a9eb [修改]
个人中心退出登录样式修改,添加身份
2025-09-11 11:01:20 +08:00
dzl
22c9665932 更改文本 2025-09-11 09:35:52 +08:00
dzl
e76407cee2 跟新图片上传 2025-09-10 16:42:48 +08:00
dzl
ea179428b7 解决了重复报错bug 2025-09-10 15:36:04 +08:00
f3bbb6e25c [修改]
补分销列表接口对接
2025-09-10 11:15:15 +08:00
268fbf5c97 [修改]
分销列表接口对接
个人中心更换头像弹窗
2025-09-10 10:20:37 +08:00
b07fe063a1 Merge remote-tracking branch 'origin/master' 2025-09-09 17:30:40 +08:00
8190e8d689 [修改]
分销列表
个人中心自愿委托出售
2025-09-09 17:30:27 +08:00
dzl
0479451acb 图片更新 2025-09-09 16:41:26 +08:00
dzl
227450566e 轮播图更新 2025-09-09 16:26:46 +08:00
acfb4c3c8d [修改]
个人中心-自愿委托出售
2025-09-09 10:36:34 +08:00
4f890c8b7f [修改]
登录注册
-用户名=手机
-分销带参
2025-09-09 10:02:36 +08:00
97ee58f8da Merge remote-tracking branch 'origin/master' 2025-09-08 17:13:35 +08:00
b0e5c01730 [修改]代理-注册-选择区域省市区 2025-09-08 17:13:08 +08:00
dzl
4eddad3250 调整样式 2025-09-08 16:48:17 +08:00
dzl
c9ed02baf8 添加部分功能 2025-09-08 16:12:56 +08:00
dzl
02251c738e 修改细节 2025-09-08 11:48:46 +08:00
dzl
d9e98355cf 细节调整 2025-09-08 11:28:32 +08:00
dzl
989f2afe4d 优化细节 2025-09-08 11:12:09 +08:00
dzl
0988bdadaf 完善分销部分功能 2025-09-08 10:58:06 +08:00
dzl
75cf9ccbd9 注释了支付宝支付选项 2025-09-08 10:26:02 +08:00
dzl
8ce3c85f9b 更改路由 2025-09-08 09:58:23 +08:00
e284574c7e 更新 2025-09-08 09:43:39 +08:00
1bb83df40b 更新 2025-09-05 16:53:11 +08:00
dzl
82879fc920 修改公告bug 2025-09-05 16:48:54 +08:00
dzl
013b122a96 更新公告完善 2025-09-05 16:31:16 +08:00
dzl
a967254fbc 增加分销功能,调整页面 2025-09-05 15:57:33 +08:00
dzl
d8853a5db1 样式调整 2025-09-05 11:40:34 +08:00
dzl
1f7d4a7896 美化页面,加强适配 2025-09-05 10:38:36 +08:00
dzl
96bc818eb2 细节修改 2025-09-05 09:38:48 +08:00
dzl
d56f375217 修改部分页面 2025-09-04 16:52:57 +08:00
dzl
eed5775c1d 调整了商城整体色系 2025-09-04 16:11:03 +08:00
dzl
95c46ebe67 删除了多余文件,完善了/mypoints-history 2025-09-04 15:50:55 +08:00
dzl
710ff3b6f7 主页样式调整 2025-09-04 15:15:04 +08:00
dzl
4d227a9fce 删除了多余的/mymatching,调整了部分主页样式 2025-09-04 14:38:27 +08:00
dzl
61eff62996 修改主页背景图 2025-09-04 11:28:49 +08:00
dzl
181a04639c 更改协议同意方式 2025-09-04 10:27:03 +08:00
dzl
fa0ef783e6 更改文本描述 2025-09-03 16:23:55 +08:00
dzl
23e77c5186 更新提示 2025-09-03 14:44:04 +08:00
376f701422 微信支付 2025-09-03 14:20:40 +08:00
dzl
34c74d9ad8 更新提示 2025-09-03 14:19:45 +08:00
dzl
4dfe0a10bf 修复了上传凭证按钮错误旋转的bug 2025-09-03 13:36:49 +08:00
dzl
e5a2dd2288 修复了图片显示bug 2025-09-03 12:02:56 +08:00
dzl
2ad870b61e 修复了小图没有并排的bug 2025-09-03 11:37:38 +08:00
dzl
fa174858db 解决报错 2025-09-03 11:16:04 +08:00
dzl
0bec040309 图片改动 2025-09-03 11:00:08 +08:00
532b34784a Merge remote-tracking branch 'origin/master' 2025-09-03 10:32:38 +08:00
dzl
546bc4008d 更改图片获取链接 2025-09-03 09:15:40 +08:00
acaed047ce 支付 2025-09-03 09:13:29 +08:00
dzl
944998ce59 修复开发环境图片显示bug 2025-09-02 17:21:49 +08:00
dzl
8d50c6dadf 图片路径调整 2025-09-02 17:05:49 +08:00
dzl
80ddefec0f 更改文本描述 2025-09-02 14:42:53 +08:00
dzl
2ae07c2ba3 调整细节 2025-09-02 13:56:09 +08:00
dzl
c7fc744e16 调整位置 2025-09-02 13:36:03 +08:00
dzl
127d9ed592 优化价格显示 2025-09-02 11:41:20 +08:00
dzl
b8974a7c6a 修复了bug 2025-09-01 17:42:19 +08:00
dzl
d593ac5c7f ui更新 2025-09-01 17:00:05 +08:00
119 changed files with 7536 additions and 8706 deletions

View File

@@ -1,3 +1,3 @@
# 开发环境配置
VITE_API_BASE_URL=/api
VITE_UPLOAD_BASE_URL=http://localhost:3000/api/upload
VITE_API_BASE_URL=https://minio.zrbjr.com/
VITE_UPLOAD_BASE_URL=/api/upload

View File

@@ -1,5 +1,5 @@
# 生产环境配置
VITE_API_BASE_URL=https://www.zrbjr.com/api
VITE_UPLOAD_BASE_URL=https://www.zrbjr.com/api/upload
VITE_API_BASE_URL=https://minio.zrbjr.com/
VITE_UPLOAD_BASE_URL=/api/upload
# VITE_API_BASE_URL=http://114.55.111.44:3001/api
# VITE_UPLOAD_BASE_URL=http://114.55.111.44:3001/api/upload

306
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"element-plus": "^2.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"qrcode": "^1.5.4",
"swiper": "^8.4.7",
"vue": "^3.3.11",
"vue-awesome-swiper": "^5.0.1",
@@ -1306,6 +1307,30 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@@ -1363,6 +1388,15 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1379,6 +1413,35 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
@@ -1410,6 +1473,15 @@
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1433,6 +1505,12 @@
"node": ">=0.10"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dom7": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-4.0.6.tgz",
@@ -1498,6 +1576,12 @@
"vue": "^3.2.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -1620,6 +1704,19 @@
"node": ">=8"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -1680,6 +1777,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1796,6 +1902,15 @@
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1821,6 +1936,18 @@
"node": ">=0.12.0"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1942,6 +2069,51 @@
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
"license": "MIT"
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1984,6 +2156,15 @@
}
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -2018,6 +2199,23 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2032,6 +2230,21 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rollup": {
"version": "4.44.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz",
@@ -2416,6 +2629,12 @@
"node": ">=14.0.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2431,6 +2650,32 @@
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -2699,6 +2944,67 @@
"vue": "^3.2.0"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",

View File

@@ -16,6 +16,7 @@
"element-plus": "^2.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"qrcode": "^1.5.4",
"swiper": "^8.4.7",
"vue": "^3.3.11",
"vue-awesome-swiper": "^5.0.1",

BIN
public/imgs/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/imgs/line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

BIN
public/imgs/shop/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
public/imgs/shop/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/imgs/shop/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/imgs/shop/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/imgs/shop/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/imgs/shop/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
public/imgs/shop/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/imgs/shop/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/imgs/shop/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/imgs/shop/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

BIN
public/imgs/shop/tips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -19,13 +19,11 @@ const routesWithBottomNav = [
'/',
'/transfers',
'/matching',
'/points-history',
'/profile',
'/mypoints-history',
'/shop',
'/orders',
'/mainpage',
'/myprofile',
'/mymatching',
'/myshop',
'/customerservice'
]

View File

@@ -1,76 +1,82 @@
<template>
<div class="bottom-nav">
<div class="bottom-nav" :style="{ backgroundImage: 'url(' + backgroundImage + ')' }">
<!-- 融豆匹配 -->
<div
class="nav-item"
:class="{ active: isActive('/mainpage') }"
@click="handleNavClick('/mainpage')"
:class="{ active: isActive('/matching') }"
@click="handleNavClick('/matching')"
>
<HomeFilled class="nav-icon" />
<span class="nav-label">主页</span>
</div>
<div
class="nav-item"
:class="{ active: isActive('/mymatching') }"
@click="handleNavClick('/mymatching')"
>
<Connection class="nav-icon" />
<span class="nav-label">融豆匹配</span>
</div>
<div
class="nav-item"
:class="{ active: isActive('/myshop') }"
@click="handleNavClick('/myshop')"
>
<Coin class="nav-icon" />
<span class="nav-label">积分商城</span>
<img
src="/imgs/bottomnav/rongdoupipei.png"
alt="融豆匹配"
class="nav-image"
:class="{ 'nav-image-active': isActive('/matching') }"
/>
<span class="nav-text" :class="{ 'nav-text-active': isActive('/matching') }">融豆匹配</span>
</div>
<!-- 融豆明细 -->
<div
class="nav-item"
:class="{ active: isActive('/transfers') }"
@click="handleNavClick('/transfers')"
>
<Money class="nav-icon" />
<span class="nav-label">融豆明细</span>
<img
src="/imgs/bottomnav/rongdoumingxi.png"
alt="融豆明细"
class="nav-image"
:class="{ 'nav-image-active': isActive('/transfers') }"
/>
<span class="nav-text" :class="{ 'nav-text-active': isActive('/transfers') }">融豆明细</span>
</div>
<!-- 主页 -->
<div
class="nav-item"
:class="{ active: isActive('/mainpage') }"
@click="handleNavClick('/mainpage')"
>
<img
src="/imgs/bottomnav/zhuye.png"
alt="主页"
class="nav-image"
:class="{ 'nav-image-active': isActive('/mainpage') }"
/>
<span class="nav-text" :class="{ 'nav-text-active': isActive('/mainpage') }">主页</span>
</div>
<!-- 积分商城 -->
<div
class="nav-item"
:class="{ active: isActive('/shop') }"
@click="handleNavClick('/shop')"
>
<img
src="/imgs/bottomnav/jifenshangcheng.png"
alt="积分商城"
class="nav-image"
:class="{ 'nav-image-active': isActive('/shop') }"
/>
<span class="nav-text" :class="{ 'nav-text-active': isActive('/shop') }">积分商城</span>
</div>
<!-- 个人中心 -->
<div
class="nav-item"
:class="{ active: isActive('/myprofile') }"
@click="handleNavClick('/myprofile')"
>
<User class="nav-icon" />
<span class="nav-label">个人中心</span>
<img
src="/imgs/bottomnav/gerenzhongxin.png"
alt="个人中心"
class="nav-image"
:class="{ 'nav-image-active': isActive('/myprofile') }"
/>
<span class="nav-text" :class="{ 'nav-text-active': isActive('/myprofile') }">个人中心</span>
</div>
<!-- <div
class="nav-item"
:class="{ active: isActive('/matching') }"
@click="handleNavClick('/matching')"
>
<Connection class="nav-icon" />
<span class="nav-label">资金匹配</span>
</div>
<div
class="nav-item"
:class="{ active: isActive('/points-history') }"
@click="handleNavClick('/points-history')"
>
<Coin class="nav-icon" />
<span class="nav-label">积分记录</span>
</div>
<div
class="nav-item"
:class="{ active: isActive('/profile') }"
@click="handleNavClick('/profile')"
>
<User class="nav-icon" />
<span class="nav-label">个人中心</span>
</div> -->
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import { Money, Coin, User, Connection, HomeFilled } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
@@ -87,6 +93,31 @@ const handleNavClick = (path) => {
}
router.push(path)
}
// 根据当前路由计算背景图片
const backgroundImage = computed(() => {
const currentPath = route.path
switch (currentPath) {
case '/matching':
// return '/imgs/bottomnav/融豆匹配背景.png'
return '/imgs/bottomnav/rongdoupipei-background.png'
case '/transfers':
// return '/imgs/bottomnav/融豆明细背景.png'
return '/imgs/bottomnav/rongdoumingxi-background.png'
case '/mainpage':
// return '/imgs/bottomnav/主页背景.png'
return '/imgs/bottomnav/zhuye-background.png'
case '/shop':
// return '/imgs/bottomnav/积分商城背景.png'
return '/imgs/bottomnav/jifenshangcheng-background.png'
case '/myprofile':
// return '/imgs/bottomnav/个人中心背景.png'
return '/imgs/bottomnav/gerenzhongxin-background.png'
default:
// return '/imgs/bottomnav/默认背景.png'
return '/imgs/bottomnav/moren-background.png'
}
})
</script>
<style scoped>
@@ -95,116 +126,119 @@ const handleNavClick = (path) => {
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #e4e7ed;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
justify-content: space-around;
align-items: center;
padding: 8px 0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
padding: 20px 0;
z-index: 1000;
padding-bottom: env(safe-area-inset-bottom);
height: 80px;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 16px;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 12px;
min-width: 60px;
height: 60px;
}
.nav-item:hover {
transform: translateY(-2px);
background: rgba(64, 158, 255, 0.1);
}
.nav-item:active {
transform: translateY(0);
}
.nav-item.active {
background: rgba(64, 158, 255, 0.15);
}
.nav-item.active .nav-icon {
color: #409eff;
transform: scale(1.1);
}
.nav-item.active .nav-label {
color: #409eff;
font-weight: 600;
}
.nav-icon {
font-size: 20px;
color: #909399;
margin-bottom: 4px;
.nav-image {
width: 50px;
height: 50px;
object-fit: contain;
transition: all 0.3s ease;
}
.nav-item:hover .nav-icon {
color: #409eff;
transform: scale(1.1);
.nav-image-active {
transform: translateY(-50px);
}
.nav-label {
.nav-text {
font-size: 12px;
color: #606266;
color: white;
margin-top: 4px;
transition: all 0.3s ease;
text-align: center;
}
.nav-item:hover .nav-label {
color: #409eff;
.nav-text-active {
color: white;
font-weight: bold;
}
/* 最左边小球向右偏移 */
.nav-item:first-child {
transform: translateX(12px);
}
.nav-item:nth-child(2) {
transform: translateX(10px);
}
.nav-item:nth-child(4) {
transform: translateX(-5px);
}
/* 最右边小球向左偏移 */
.nav-item:last-child {
transform: translateX(-10px);
}
/* 移动端适配 */
@media (max-width: 768px) {
.bottom-nav {
padding: 6px 0;
padding-bottom: max(8px, env(safe-area-inset-bottom));
padding: 15px 0;
padding-bottom: max(15px, env(safe-area-inset-bottom));
height: 70px;
}
.nav-item {
padding: 6px 8px;
min-width: 50px;
min-height: 44px; /* 触摸友好的最小尺寸 */
justify-content: center;
height: 50px;
}
.nav-icon {
font-size: 18px;
margin-bottom: 2px;
.nav-image {
width: 45px;
height: 45px;
}
.nav-label {
.nav-image-active {
transform: translateY(-40px);
}
.nav-text {
font-size: 11px;
line-height: 1.2;
}
}
@media (max-width: 480px) {
.bottom-nav {
padding: 4px 0;
padding-bottom: max(6px, env(safe-area-inset-bottom));
padding: 10px 0;
padding-bottom: max(10px, env(safe-area-inset-bottom));
height: 60px;
}
.nav-item {
padding: 4px 6px;
min-width: 45px;
height: 45px;
}
.nav-icon {
font-size: 16px;
margin-bottom: 1px;
.nav-image {
width: 40px;
height: 40px;
}
.nav-label {
.nav-image-active {
transform: translateY(-20px);
}
.nav-text {
font-size: 10px;
}
}
@@ -212,20 +246,25 @@ const handleNavClick = (path) => {
/* 横屏适配 */
@media (max-height: 500px) and (orientation: landscape) {
.bottom-nav {
padding: 4px 0;
padding: 8px 0;
height: 50px;
}
.nav-item {
padding: 4px 8px;
height: 40px;
}
.nav-icon {
font-size: 16px;
margin-bottom: 1px;
.nav-image {
width: 35px;
height: 35px;
}
.nav-label {
font-size: 10px;
.nav-image-active {
transform: translateY(-30px);
}
.nav-text {
font-size: 9px;
}
}
</style>

View File

@@ -44,7 +44,7 @@
import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, Loading } from '@element-plus/icons-vue'
import api from '@/utils/api'
import api,{captchaAPI} from '@/utils/api'
// Props
const props = defineProps({
@@ -81,7 +81,7 @@ const loading = ref(false)
const getCaptcha = async () => {
try {
loading.value = true
const response = await api.get('/captcha/generate')
const response = await captchaAPI.generate()
if (response.data.success) {
captchaImage.value = response.data.data.image

View File

@@ -1,11 +1,11 @@
// 环境配置
const config = {
development: {
baseURL: 'http://localhost:3000',
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://minio.zrbjr.com/',
uploadURL: import.meta.env.VITE_UPLOAD_BASE_URL || 'http://localhost:3000/api/upload'
},
production: {
baseURL: window.location.origin,
baseURL: 'https://minio.zrbjr.com/',
uploadURL: import.meta.env.VITE_UPLOAD_BASE_URL || `${window.location.origin}/api/upload`
}
}
@@ -21,20 +21,32 @@ export const { baseURL, uploadURL } = config[env]
// 获取完整的图片URL
export const getImageUrl = (imagePath) => {
// console.log('getImageUrl called with:', imagePath)
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
// 在开发环境下直接返回相对路径让Vite代理处理
// 如果图片路径以/uploads开头直接返回原路径
if (imagePath.startsWith('/uploads')) {
const cleanBaseURL = baseURL.replace(/\/$/, '')
// console.log('Image starts with /uploads, returning original path:', imagePath)
return `${imagePath}`
}
// 在开发环境下,也需要根据路径前缀处理
if (env === 'development') {
const cleanBaseURL = baseURL.replace(/\/$/, '')
const cleanImagePath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
return cleanImagePath
const fullUrl = `${cleanBaseURL}${cleanImagePath}`
// console.log('Development environment, returning:', fullUrl)
return fullUrl
}
// 生产环境下使用完整URL
const cleanBaseURL = baseURL.replace(/\/$/, '')
const cleanImagePath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
const fullUrl = `${cleanBaseURL}${cleanImagePath}`
return `${cleanBaseURL}${cleanImagePath}`
return fullUrl
}
// 获取上传配置

View File

@@ -1,376 +1,369 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import {createRouter, createWebHistory} from 'vue-router'
import {useUserStore} from '@/stores/user'
import NProgress from 'nprogress'
import api from '@/utils/api'
const routes = [
{
path: '/',
name: 'Home',
redirect: '/transfers',
meta: {
title: '首页'
{
path: '/',
name: 'Home',
redirect: '/mainpage',
meta: {
title: '首页'
}
},
{
path: '/mylogin',
name: 'MyLogin',
component: () => import('@/views/MyLogin.vue'),
meta: {
title: '登录',
hideForAuth: true
}
},
{
path: '/mainpage',
name: 'MainPage',
component: () => import('@/views/MainPage.vue'),
meta: {
title: '主页'
}
},
{
path: '/myshop',
name: 'MyShop',
component: () => import('@/views/MyShop.vue'),
meta: {
title: '积分商城'
}
},
{
path: '/myprofile',
name: 'MyProfile',
component: () => import('@/views/MyProfile.vue'),
meta: {
title: '个人中心'
}
},
{
path: '/distribution',
name: 'Distribution',
component: () => import('@/views/Distribution.vue'),
meta: {
title: '分享'
}
},
{
path: '/loading',
name: 'Loading',
component: () => import('@/views/Loading.vue'),
meta: {
title: '维护中'
}
},
{
path: '/mypoints-history',
name: 'MyPointsHistory',
component: () => import('@/views/MyPointsHistory.vue'),
meta: {
title: '积分记录',
requiresAuth: true
}
},
{
path: '/editdetailspage',
name: 'EditDetailsPage',
component: () => import('@/views/EditDetailsPage.vue'),
meta: {
title: '编辑信息',
requiresAuth: true
}
},
{
path: '/editpasswordpage',
name: 'EditPasswordPage',
component: () => import('@/views/EditPasswordPage.vue'),
meta: {
title: '编辑密码',
requiresAuth: true
}
},
{
path: '/shop',
name: 'Shop',
component: () => import('@/views/Shop.vue'),
meta: {
title: '积分商城'
}
},
{
path: '/productCategory',
name: 'ProductCategory',
component: () => import('@/views/ProductCategory.vue'),
meta: {
title: '商品分类'
}
},
{
path: '/productCategoryFinal/:name',
name: 'ProductCategoryFinal',
component: () => import('@/views/ProductCategoryFinal.vue'),
meta: {
title: '商品分类'
}
},
{
path: '/product/:id',
name: 'ProductDetail',
component: () => import('@/views/ProductDetail.vue'),
meta: {
title: '商品详情'
}
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
meta: {
title: '注册',
hideForAuth: true
}
},
{
path: '/payment',
name: 'Payment',
component: () => import('@/views/Payment.vue'),
meta: {
title: '支付激活',
requiresAuth: true
}
},
{
path: '/orders',
name: 'Orders',
component: () => import('@/views/Orders.vue'),
meta: {
title: '我的订单',
requiresAuth: true
}
},
{
path: '/transfers',
name: 'Transfers',
component: () => import('@/views/Transfers.vue'),
meta: {
title: '转账',
requiresAuth: true
}
},
{
path: '/matching',
name: 'Matching',
component: () => import('@/views/Matching.vue'),
meta: {
title: '货款匹配',
requiresAuth: true
}
},
{
path: '/agent/login',
name: 'AgentLogin',
component: () => import('@/views/AgentLogin.vue'),
meta: {
title: '代理登录'
}
},
{
path: '/agent/dashboard',
name: 'AgentDashboard',
component: () => import('@/views/AgentDashboard.vue'),
meta: {
title: '代理后台',
requiresAuth: true,
isAgent: true
}
},
{
path: '/customerservice',
name: 'CustomerService',
component: () => import('@/views/CustomerService.vue'),
meta: {
title: '客服中心'
}
},
{
path: '/productsummary/:id',
name: 'productSummary',
component: () => import('@/views/ProductSummary.vue'),
meta: {
title: '商品汇总'
}
},
{
path: '/buydetail',
name: 'BuyDetail',
component: () => import('../views/BuyDetails.vue'),
meta: {title: '确认订单'}
},
{
path: '/pay/:orderId',
name: 'Pay',
component: () => import('@/views/Pay.vue'),
meta: {title: '确认支付'},
props: route => ({orderId: route.query.orderId})
},
{
path: '/cart',
name: 'Cart',
component: () => import('@/views/Cart.vue'),
meta: {title: '购物车'}
},
{
path: '/address',
name: 'Address',
component: () => import('@/views/Address.vue'),
meta: {title: '地址管理', requiresAuth: true}
},
{
path: '/payloading',
name: 'PayLoading',
component: () => import('@/views/PayLoading.vue'),
meta: {title: '支付确认'},
props: route => ({orderId: route.query.orderId})
},
{
path: '/coupon',
name: 'Coupon',
component: () => import('@/views/CouponGet.vue'),
meta: {title: '优惠券', requiresAuth: true}
},
{
path: '/couponmanage',
name: 'CouponManage',
component: () => import('@/views/CouponManage.vue'),
meta: {title: '优惠券管理', requiresAuth: true}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: {
title: '页面不存在'
}
}
},
{
path: '/mylogin',
name: 'MyLogin',
component: () => import('@/views/MyLogin.vue'),
meta: {
title: '登录',
hideForAuth: true
}
},
{
path: '/mainpage',
name: 'MainPage',
component: () => import('@/views/MainPage.vue'),
meta: {
title: '主页'
}
},
{
path: '/mymatching',
name: 'MyMatching',
component: () => import('@/views/Matching.vue'),
meta: {
title: '货款匹配',
requiresAuth: true
}
},
{
path: '/myshop',
name: 'MyShop',
component: () => import('@/views/MyShop.vue'),
meta: {
title: '积分商城'
}
},
{
path: '/myprofile',
name: 'MyProfile',
component: () => import('@/views/MyProfile.vue'),
meta: {
title: '个人中心'
}
},
{
path: '/loading',
name: 'Loading',
component: () => import('@/views/Loading.vue'),
meta: {
title: '维护中'
}
},
{
path: '/mypoints-history',
name: 'MyPointsHistory',
component: () => import('@/views/MyPointsHistory.vue'),
meta: {
title: '积分记录',
requiresAuth: true
}
},
{
path: '/editdetailspage',
name: 'EditDetailsPage',
component: () => import('@/views/EditDetailsPage.vue'),
meta: {
title: '编辑信息',
requiresAuth: true
}
},
{
path: '/editpasswordpage',
name: 'EditPasswordPage',
component: () => import('@/views/EditPasswordPage.vue'),
meta: {
title: '编辑密码',
requiresAuth: true
}
},
{
path: '/home',
name: 'HomePage',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页'
}
},
{
path: '/shop',
name: 'Shop',
component: () => import('@/views/Shop.vue'),
meta: {
title: '积分商城'
}
},
{
path: '/product/:id',
name: 'ProductDetail',
component: () => import('@/views/ProductDetail.vue'),
meta: {
title: '商品详情'
}
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: {
title: '登录',
hideForAuth: true
}
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
meta: {
title: '注册',
hideForAuth: true
}
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: {
title: '个人中心',
requiresAuth: true
}
},
{
path: '/orders',
name: 'Orders',
component: () => import('@/views/Orders.vue'),
meta: {
title: '我的订单',
requiresAuth: true
}
},
{
path: '/points-history',
name: 'PointsHistory',
component: () => import('@/views/PointsHistory.vue'),
meta: {
title: '积分记录',
requiresAuth: true
}
},
{
path: '/task-center',
name: 'TaskCenter',
component: () => import('@/views/TaskCenter.vue'),
meta: {
title: '任务中心',
requiresAuth: true
}
},
{
path: '/transfers',
name: 'Transfers',
component: () => import('@/views/Transfers.vue'),
meta: {
title: '转账',
requiresAuth: true
}
},
{
path: '/matching',
name: 'Matching',
component: () => import('@/views/Matching.vue'),
meta: {
title: '货款匹配',
requiresAuth: true
}
},
{
path: '/agent/login',
name: 'AgentLogin',
component: () => import('@/views/AgentLogin.vue'),
meta: {
title: '代理登录'
}
},
{
path: '/agent/dashboard',
name: 'AgentDashboard',
component: () => import('@/views/AgentDashboard.vue'),
meta: {
title: '代理后台',
requiresAuth: true,
isAgent: true
}
},
{
path: '/agent/withdrawals',
name: 'AgentWithdrawals',
component: () => import('@/views/AgentWithdrawals.vue'),
meta: {
title: '佣金提现',
requiresAuth: true,
isAgent: true
}
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
title: '关于我们'
}
},
{
path: '/customerservice',
name: 'CustomerService',
component: () => import('@/views/CustomerService.vue'),
meta: {
title: '客服中心'
}
},
{
path: '/productsummary/:id',
name: 'productSummary',
component: () => import('@/views/ProductSummary.vue'),
meta: {
title: '商品汇总'
}
},
{
path: '/buydetail',
name: 'BuyDetail',
component: () => import('../views/BuyDetails.vue'),
meta: { title: '确认订单' }
},
{
path: '/pay/:orderId',
name: 'Pay',
component: () => import('@/views/Pay.vue'),
meta: { title: '确认支付' },
props: route => ({ orderId: route.query.orderId })
},
{
path: '/cart',
name: 'Cart',
component: () => import('@/views/Cart.vue'),
meta: { title: '购物车' }
},
{
path: '/address',
name: 'Address',
component: () => import('@/views/Address.vue'),
meta: { title: '地址管理', requiresAuth: true }
},
{
path: '/payloading',
name: 'PayLoading',
component: () => import('@/views/PayLoading.vue'),
meta: { title: '支付确认' },
props: route => ({ orderId: route.query.orderId })
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: {
title: '页面不存在'
}
}
]
const router = createRouter({
history: createWebHistory('/frontend/'),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
history: createWebHistory('/frontend/'),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return {top: 0}
}
}
}
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
const userStore = useUserStore()
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 炬融圈`
}
// 检查维护模式
try {
const {data} = await api.get('/system/maintenance-status', { showLoading: false })
console.log(data,'data');
if (data.success) {
if (data.data.maintenance_mode) {
// 维护模式开启,且不在维护页面,跳转到维护页面
if (to.name !== 'Loading') {
next({ name: 'Loading' })
return
}
} else {
// 维护模式关闭,且在维护页面,跳转到首页
if (to.name === 'Loading') {
next({ name: 'MainPage' })
return
}
}
NProgress.start()
const userStore = useUserStore()
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 炬融圈`
}
} catch (error) {
// 如果检查维护状态失败,继续正常流程
console.warn('检查维护状态失败:', error)
}
// 检查是否需要认证
if (to.meta.requiresAuth) {
// 检查是否是代理页面
if (to.meta.isAgent) {
// 代理页面认证逻辑
const agentInfo = localStorage.getItem('agentInfo')
const agentToken = localStorage.getItem('token')
if (!agentInfo || !agentToken) {
next({
path: '/agent/login',
query: { redirect: to.fullPath }
})
// 检查维护模式
try {
const {data} = await api.get('/system/maintenance-status', {showLoading: false})
console.log(data, 'data');
if (data.success) {
if (data.data.maintenance_mode) {
// 维护模式开启,且不在维护页面,跳转到维护页面
if (to.name !== 'Loading') {
next({name: 'Loading'})
return
}
} else {
// 维护模式关闭,且在维护页面,跳转到首页
if (to.name === 'Loading') {
next({name: 'MainPage'})
return
}
}
}
} catch (error) {
// 如果检查维护状态失败,继续正常流程
console.warn('检查维护状态失败:', error)
}
// 检查是否需要认证
if (to.meta.requiresAuth) {
// 检查是否是代理页面
if (to.meta.isAgent) {
// 代理页面认证逻辑
const agentInfo = localStorage.getItem('agentInfo')
const agentToken = localStorage.getItem('token')
if (!agentInfo || !agentToken) {
next({
path: '/mylogin',
query: {redirect: to.fullPath}
})
return
}
} else {
// 普通用户页面认证逻辑
// console.log(userStore.isAuthenticated, 'isAuthenticated');
if (!userStore.isAuthenticated) {
// 尝试从本地存储恢复登录状态
await userStore.checkAuth()
if (!userStore.isAuthenticated) {
next({
name: 'MyLogin',
query: {redirect: to.fullPath}
})
return
}
}
// 检查支付状态(管理员除外)
// console.log(userStore.user);
if (userStore.user && userStore.user.role !== 'admin' && userStore.user.payment_status === 'unpaid') {
// console.log('进来了');
// 如果当前不在支付页面,静默重定向到支付页面(不显示额外通知)
if (to.name !== 'Payment') {
next({
name: 'Payment',
query: {redirect: to.fullPath}
})
return
} else {
next()
}
}
}
}
// 如果已登录用户访问登录/注册页面,重定向到转账管理(改成了主页)
if (to.meta.hideForAuth && userStore.isAuthenticated) {
next({name: 'MainPage'})
return
}
} else {
// 普通用户页面认证逻辑
if (!userStore.isAuthenticated) {
// 尝试从本地存储恢复登录状态
await userStore.checkAuth()
if (!userStore.isAuthenticated) {
next({
name: 'MyLogin',
query: { redirect: to.fullPath }
})
return
}
}
}
}
// 如果已登录用户访问登录/注册页面,重定向到转账管理(改成了主页)
if (to.meta.hideForAuth && userStore.isAuthenticated) {
next({ name: 'MainPage' })
return
}
next()
next()
})
router.afterEach(() => {
NProgress.done()
NProgress.done()
})
export default router

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/utils/api'
import api,{authAPI} from '@/utils/api'
import { ElMessage } from 'element-plus'
export const useUserStore = defineStore('user', () => {
@@ -23,10 +23,9 @@ export const useUserStore = defineStore('user', () => {
token.value = newToken
if (newToken) {
localStorage.setItem('token', newToken)
api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
} else {
localStorage.removeItem('token')
delete api.defaults.headers.common['Authorization']
console.log('token已移除');
}
}
@@ -39,17 +38,53 @@ export const useUserStore = defineStore('user', () => {
const login = async (credentials) => {
try {
loading.value = true
const response = await api.post('/auth/login', credentials)
const response = await authAPI.login(credentials)
console.log('response',response);
if (response.data.token) {
if (response.data.success && response.data.token) {
setToken(response.data.token)
setUser(response.data.user)
startStatusCheck() // 登录成功后开始状态检查
ElMessage.success(response.data.message || '登录成功')
return { success: true, data: response.data }
} else if (response.data.needPayment) {
console.log(response.data.needPayment,'response.data.needPayment');
console.log({
success: false,
needPayment: true,
userId: response.data.userId,
message: response.data.message
});
setToken(response.data.token)
setUser(response.data.user)
// 用户需要支付激活,不显示错误消息,由前端页面处理
const returnData = {
success: false,
needPayment: true,
userId: response.data.userId,
message: response.data.message
}
console.log('即将返回needPayment数据:', returnData);
return returnData
} else {
const message = response.data.message || '登录失败'
ElMessage.error(message)
return { success: false, message }
}
} catch (error) {
const message = error.response?.data?.message || '登录失败'
console.log(error,'error');
const errorData = error.response?.data
if (errorData?.needPayment) {
// 处理403状态码返回的需要支付情况
return {
success: false,
needPayment: true,
userId: errorData.userId,
message: errorData.message
}
}
const message = errorData?.message || '登录失败'
ElMessage.error(message)
return { success: false, message }
} finally {
@@ -63,11 +98,30 @@ export const useUserStore = defineStore('user', () => {
loading.value = true
const response = await api.post('/auth/register', userData)
if (response.data.token) {
setToken(response.data.token)
setUser(response.data.user)
ElMessage.success(response.data.message || '注册成功')
return { success: true, data: response.data }
if (response.data.success) {
// 检查是否需要支付
if (response.data.needPayment) {
setToken(response.data.token)
setUser(response.data.user)
// 需要支付的情况,返回成功状态和支付相关信息
return {
success: true,
needPayment: true,
token: response.data.token,
user: response.data.user,
message: response.data.message
}
} else {
// 直接注册成功的情况
setToken(response.data.token)
setUser(response.data.user)
ElMessage.success(response.data.message || '注册成功')
return { success: true, data: response.data }
}
} else {
const message = response.data.message || '注册失败'
ElMessage.error(message)
return { success: false, message }
}
} catch (error) {
const message = error.response?.data?.message || '注册失败'
@@ -94,15 +148,13 @@ export const useUserStore = defineStore('user', () => {
try {
// 确保请求头已设置
if (token.value && !api.defaults.headers.common['Authorization']) {
api.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
}
const response = await api.get('/auth/me')
setUser(response.data.user)
return true
} catch (error) {
// token无效清除本地存储
console.log('token无效清除本地存储',error);
setToken('')
setUser(null)
return false

View File

@@ -1,65 +1,66 @@
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import router from '@/router'
import NProgress from 'nprogress'
// 创建axios实例
const api = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
// 工厂函数(复用拦截器逻辑)
export const createRequest = (baseURL) => {
const request = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 初始化时设置token
const token = localStorage.getItem('token')
if (token) {
request.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
})
// 初始化时设置token
const token = localStorage.getItem('token')
if (token) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
// 请求拦截器
let loadingInstance = null
api.interceptors.request.use(
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 从localStorage获取token
// 开始进度条
NProgress.start()
// 显示加载动画(除了某些不需要的请求)
if (!config.hideLoading) {
showLoading()
}
// 添加认证token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 显示加载动画(可选)
if (config.showLoading !== false) {
loadingInstance = ElLoading.service({
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
return config
},
(error) => {
if (loadingInstance) {
loadingInstance.close()
}
hideLoading()
NProgress.done()
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
// 响应拦截器
request.interceptors.response.use(
(response) => {
if (loadingInstance) {
loadingInstance.close()
}
hideLoading()
NProgress.done()
return response
},
(error) => {
if (loadingInstance) {
loadingInstance.close()
}
hideLoading()
NProgress.done()
const { response } = error
// 处理不同的错误状态码
if (error.response) {
if (response) {
const { status, data } = error.response
switch (status) {
@@ -72,10 +73,10 @@ api.interceptors.response.use(
// 判断当前是否在代理相关页面
const currentPath = router.currentRoute.value.path
if (currentPath.startsWith('/agent')) {
router.push('/agent/login')
router.push('/agent/mylogin')
ElMessage.error('代理登录已过期,请重新登录')
} else {
router.push({ name: 'Login' })
router.push({ name: 'MyLogin' })
ElMessage.error('登录已过期,请重新登录')
}
break
@@ -86,8 +87,15 @@ api.interceptors.response.use(
// 清除token并跳转到登录页
localStorage.removeItem('token')
delete api.defaults.headers.common['Authorization']
router.push({ name: 'Login' })
router.push({ name: 'MyLogin' })
ElMessage.error(data.message || '账户已被拉黑,请联系管理员')
} else if (data.code === 'PAYMENT_REQUIRED') {
// 需要支付,跳转到支付页面
// 只在不是支付页面时才跳转和显示消息,避免重复通知
if (router.currentRoute.value.name !== 'Payment') {
router.push({ name: 'Payment' })
ElMessage.warning(data.message || '您的账户尚未激活,请完成支付后再使用')
}
} else {
ElMessage.error(data.message || '权限不足')
}
@@ -129,13 +137,53 @@ api.interceptors.response.use(
}
)
return request
}
let loadingInstance = null
let requestCount = 0
let isLoggingOut = false // 防止重复登出
// 显示加载
const showLoading = () => {
if (requestCount === 0) {
loadingInstance = ElLoading.service({
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
requestCount++
}
// 隐藏加载
const hideLoading = () => {
requestCount--
if (requestCount <= 0) {
requestCount = 0
if (loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
}
// 生成不同的实例
export const apiRequest = createRequest('/api')
export const midRequest = createRequest('http://192.168.0.12:3005/mid')
// 初始化时设置token
const token = localStorage.getItem('token')
if (token) {
apiRequest.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
// 封装常用的请求方法
export const request = {
get: (url, config = {}) => api.get(url, config),
post: (url, data = {}, config = {}) => api.post(url, data, config),
put: (url, data = {}, config = {}) => api.put(url, data, config),
delete: (url, config = {}) => api.delete(url, config),
patch: (url, data = {}, config = {}) => api.patch(url, data, config)
const api = {
get: (url, config = {}) => apiRequest.get(url, config),
post: (url, data = {}, config = {}) => apiRequest.post(url, data, config),
put: (url, data = {}, config = {}) => apiRequest.put(url, data, config),
delete: (url, config = {}) => apiRequest.delete(url, config),
patch: (url, data = {}, config = {}) => apiRequest.patch(url, data, config)
}
@@ -143,43 +191,46 @@ export const request = {
// 用户相关API
export const userAPI = {
// 获取用户列表
getList: (params = {}) => request.get('/users', { params }),
getList: (params = {}) => apiRequest.get('/users', { params }),
// 获取用户详情
getDetail: (id) => request.get(`/users/${id}`),
getDetail: (id) => apiRequest.get(`/users/${id}`),
// 更新用户信息
update: (id, data) => request.put(`/users/${id}`, data),
update: (id, data) => apiRequest.put(`/users/${id}`, data),
// 删除用户
delete: (id) => request.delete(`/users/${id}`),
delete: (id) => apiRequest.delete(`/users/${id}`),
// 获取用户统计
getStats: () => request.get('/users/stats/overview')
getStats: () => apiRequest.get('/users/stats/overview')
}
// 认证相关API
export const authAPI = {
// 登录
login: (data) => request.post('/auth/login', data),
// login: (data) => midRequest.post('/auth/login', data),
login: (data) => apiRequest.post('/auth/login', data),
// 注册
register: (data) => request.post('/auth/register', data),
register: (data) => midRequest.post('/auth/register', data),
// 获取当前用户信息
me: () => request.get('/auth/me'),
me: () => apiRequest.get('/auth/me'),
// 修改密码
changePassword: (data) => request.put('/auth/change-password', data)
changePassword: (data) => apiRequest.put('/auth/change-password', data)
}
// 验证码相关API
export const captchaAPI = {
// 生成验证码
generate: () => request.get('/captcha/generate'),
// generate: () => midRequest.get('/captcha/generate'),
generate: () => apiRequest.get('/captcha/generate'),
// 验证验证码
verify: (data) => request.post('/captcha/verify', data)
// verify: (data) => midRequest.post('/captcha/verify', data)
verify: (data) => apiRequest.post('/captcha/verify', data)
}
// 文件上传API
@@ -188,7 +239,7 @@ export const uploadAPI = {
uploadImage: (file) => {
const formData = new FormData()
formData.append('image', file)
return request.post('/upload/image', formData, {
return midRequest.post('/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -199,7 +250,7 @@ export const uploadAPI = {
uploadFile: (file) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/upload/file', formData, {
return midRequest.post('/upload/file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -207,10 +258,34 @@ export const uploadAPI = {
}
}
// 支付相关API
export const paymentAPI = {
// 获取支付方式
getMethods: () => apiRequest.get('/payment/methods'),
// 创建支付订单
createOrder: (data) => apiRequest.post('/payment/create-order', data),
// 查询支付状态
queryStatus: (outTradeNo) => apiRequest.get(`/payment/query-status/${outTradeNo}`),
getOrder: () => apiRequest.get('/payment/check-status'),
// 获取支付记录
getOrders: (params = {}) => apiRequest.get('/payment/orders', { params })
}
// 购买商品
export const buyAPI = {
buy: (data) => midRequest.post('/payment/create-order', data),
pay: (outTradeNo, params) => midRequest.get(`/payment/query-status/${outTradeNo}`, {params}),
test : () => apiRequest.get('/payment/pay-product/test'),
}
// 转账相关API
export const transferAPI = {
// 获取公户信息
getPublicAccount: () => request.get('/transfers/public-account'),
getPublicAccount: () => apiRequest.get('/transfers/public-account'),
// 创建转账记录
create: (data) => {
@@ -218,7 +293,7 @@ export const transferAPI = {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
return request.post('/transfers', formData, {
return apiRequest.post('/transfers', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -226,34 +301,38 @@ export const transferAPI = {
},
// 确认转账
confirm: (id) => request.put(`/transfers/${id}/confirm`),
confirm: (id) => apiRequest.put(`/transfers/${id}/confirm`),
// 拒绝转账
reject: (id) => request.put(`/transfers/${id}/reject`),
reject: (id) => apiRequest.put(`/transfers/${id}/reject`),
// 确认收款
confirmReceived: (id) => request.post('/transfers/confirm-received', { transfer_id: id }),
confirmReceived: (id) => apiRequest.post('/transfers/confirm-received', { transfer_id: id }),
// 确认未收到款
confirmNotReceived: (id) => request.post('/transfers/confirm-not-received', { transfer_id: id }),
confirmNotReceived: (id) => apiRequest.post('/transfers/confirm-not-received', { transfer_id: id }),
// 获取用户转账记录
getUserTransfers: (params = {}) => request.get('/transfers/user', { params }),
getUserTransfers: (params = {}) => apiRequest.get('/transfers/user', { params }),
// 获取指定用户的转账记录
getUserTransfersByUserId: (userId, params = {}) => request.get(`/transfers/user/${userId}`, { params }),
getUserTransfersByUserId: (userId, params = {}) => apiRequest.get(`/transfers/user/${userId}`, { params }),
// 获取待确认转账
getPendingTransfers: (params = {}) => request.get('/transfers/pending', { params }),
getPendingTransfers: (params = {}) => apiRequest.get('/transfers/pending', { params }),
// 获取用户账户信息
getUserAccount: () => request.get('/transfers/account'),
getUserAccount: () => apiRequest.get('/transfers/account'),
// 获取转账列表(管理员)
getList: (params = {}) => request.get('/transfers', { params }),
getList: (params = {}) => apiRequest.get('/transfers', { params }),
// 获取转账统计
getStats: () => request.get('/transfers/stats')
getStats: () => apiRequest.get('/transfers/stats')
}
export const distributionAPI = {
getLowerUsers: (params) => apiRequest.get('/agents/distribution', { params }),
}
export default api

View File

@@ -1,453 +0,0 @@
<template>
<div class="about-page">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-left">
<el-button
type="text"
@click="$router.go(-1)"
class="back-btn"
>
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
</div>
<div class="nav-center">
<h1 class="nav-title">关于我们</h1>
</div>
<div class="nav-right"></div>
</nav>
<!-- 页面内容 -->
<div class="about-content">
<!-- 网站介绍 -->
<section class="intro-section">
<div class="intro-header">
<div class="logo">
<el-icon size="60"><Platform /></el-icon>
</div>
<h2>融互通</h2>
<p class="tagline">专业的积分兑换与商品管理平台</p>
</div>
<div class="intro-content">
<p>
积分商城系统是一个现代化的积分兑换与商品管理平台致力于为用户提供丰富的商品选择和便捷的积分兑换体验
我们相信积分的价值在于为用户带来实际的收益和满足感通过技术的力量让积分兑换变得更加简单高效
</p>
</div>
</section>
<!-- 功能特色 -->
<section class="features-section">
<h3>功能特色</h3>
<div class="features-grid">
<div class="feature-item">
<div class="feature-icon">
<el-icon><ShoppingBag /></el-icon>
</div>
<h4>丰富商品</h4>
<p>精选优质商品涵盖生活用品数码产品虚拟服务等多个品类</p>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><Coin /></el-icon>
</div>
<h4>积分兑换</h4>
<p>灵活的积分兑换机制让您的积分发挥最大价值享受购物乐趣</p>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<h4>积分管理</h4>
<p>完整的积分获取和消费记录让您清楚了解每一分积分的来源和去向</p>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><User /></el-icon>
</div>
<h4>个人中心</h4>
<p>完善的个人资料管理记录您的兑换历程和积分成长轨迹</p>
</div>
</div>
</section>
<!-- 技术栈 -->
<section class="tech-section">
<h3>技术栈</h3>
<div class="tech-grid">
<div class="tech-category">
<h4>前端技术</h4>
<ul>
<li>Vue 3 + Composition API</li>
<li>Element Plus UI 组件库</li>
<li>Vue Router 路由管理</li>
<li>Pinia 状态管理</li>
<li>Vite 构建工具</li>
<li>响应式设计</li>
</ul>
</div>
<div class="tech-category">
<h4>后端技术</h4>
<ul>
<li>Node.js + Express</li>
<li>MySQL 数据库</li>
<li>JWT 身份认证</li>
<li>RESTful API 设计</li>
<li>积分系统管理</li>
<li>订单处理系统</li>
</ul>
</div>
</div>
</section>
<!-- 联系我们 -->
<section class="contact-section">
<h3>联系我们</h3>
<div class="contact-info">
<div class="contact-item">
<el-icon><Message /></el-icon>
<div>
<h4>邮箱</h4>
<p>contact@example.com</p>
</div>
</div>
<div class="contact-item">
<el-icon><Phone /></el-icon>
<div>
<h4>电话</h4>
<p>400-123-4567</p>
</div>
</div>
<div class="contact-item">
<el-icon><Location /></el-icon>
<div>
<h4>地址</h4>
<p>北京市朝阳区科技园区</p>
</div>
</div>
</div>
</section>
<!-- 版本信息 -->
<section class="version-section">
<div class="version-info">
<p><strong>版本</strong>v1.0.0</p>
<p><strong>更新时间</strong>{{ updateTime }}</p>
<p><strong>开发团队</strong>积分商城系统开发团队</p>
</div>
</section>
</div>
<!-- 页脚 -->
<footer class="about-footer">
<p>&copy; 2024 积分商城系统. All rights reserved.</p>
</footer>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
ArrowLeft,
Platform,
ShoppingBag,
Coin,
TrendCharts,
User,
Message,
Phone,
Location
} from '@element-plus/icons-vue'
// 响应式数据
const updateTime = ref('2024-01-15')
</script>
<style scoped>
.about-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
background: white;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-left,
.nav-right {
flex: 1;
}
.back-btn {
color: #409eff;
font-size: 14px;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #333;
}
.about-content {
padding: 20px 16px;
max-width: 800px;
margin: 0 auto;
}
.intro-section {
background: white;
border-radius: 12px;
padding: 40px 30px;
margin-bottom: 24px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.intro-header {
margin-bottom: 30px;
}
.logo {
color: #409eff;
margin-bottom: 16px;
}
.intro-header h2 {
margin: 0 0 8px 0;
font-size: 28px;
color: #333;
font-weight: 600;
}
.tagline {
margin: 0;
font-size: 16px;
color: #666;
}
.intro-content p {
font-size: 16px;
line-height: 1.8;
color: #555;
text-align: left;
margin: 0;
}
.features-section,
.tech-section,
.contact-section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.features-section h3,
.tech-section h3,
.contact-section h3 {
margin: 0 0 24px 0;
font-size: 20px;
color: #333;
font-weight: 600;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.feature-item {
text-align: center;
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
transition: all 0.3s;
}
.feature-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.feature-icon {
color: #409eff;
font-size: 32px;
margin-bottom: 12px;
}
.feature-item h4 {
margin: 0 0 8px 0;
font-size: 16px;
color: #333;
font-weight: 500;
}
.feature-item p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.6;
}
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.tech-category h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
font-weight: 500;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
.tech-category ul {
margin: 0;
padding: 0;
list-style: none;
}
.tech-category li {
padding: 8px 0;
color: #555;
font-size: 14px;
position: relative;
padding-left: 16px;
}
.tech-category li::before {
content: '•';
color: #409eff;
position: absolute;
left: 0;
font-weight: bold;
}
.contact-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
}
.contact-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.contact-item .el-icon {
color: #409eff;
font-size: 24px;
}
.contact-item h4 {
margin: 0 0 4px 0;
font-size: 14px;
color: #333;
font-weight: 500;
}
.contact-item p {
margin: 0;
font-size: 14px;
color: #666;
}
.version-section {
background: white;
border-radius: 12px;
padding: 20px 30px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.version-info p {
margin: 8px 0;
font-size: 14px;
color: #666;
}
.about-footer {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.about-content {
padding: 15px 10px;
}
.intro-section,
.features-section,
.tech-section,
.contact-section {
padding: 20px 15px;
}
.intro-header h2 {
font-size: 24px;
}
.features-grid {
grid-template-columns: 1fr;
}
.tech-grid {
grid-template-columns: 1fr;
}
.contact-info {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.intro-section,
.features-section,
.tech-section,
.contact-section,
.version-section {
padding: 15px;
}
.intro-header h2 {
font-size: 20px;
}
.features-section h3,
.tech-section h3,
.contact-section h3 {
font-size: 18px;
}
}
</style>

View File

@@ -234,80 +234,7 @@ const loadRegionOptions = async () => {
} catch (error) {
console.error('获取省市区数据失败:', error)
ElMessage.error(error.message || '获取省市区数据失败')
// 如果API获取失败使用浙江省数据作为默认数据
await loadFallbackRegionData()
}
}
// 回退方案:加载浙江省数据
const loadFallbackRegionData = async () => {
try {
const zhejiangResponse = await api.get('/regions/zhejiang')
if (zhejiangResponse.data.success) {
const zhejiangData = zhejiangResponse.data.data || []
// 将浙江省数据转换为级联选择器格式
const cityMap = new Map()
zhejiangData.forEach(item => {
if (!cityMap.has(item.city_name)) {
cityMap.set(item.city_name, {
value: item.city_name,
label: item.city_name,
children: []
})
}
// 添加区县数据移除is_available过滤条件以确保所有区县都显示
if (item.district_name) {
cityMap.get(item.city_name).children.push({
value: item.district_name,
label: item.district_name,
code: item.region_code
})
}
})
regionOptions.value = [{
value: '浙江省',
label: '浙江省',
children: Array.from(cityMap.values())
}]
console.log('已加载浙江省地区数据作为默认选项')
} else {
throw new Error('获取浙江省数据也失败')
}
} catch (fallbackError) {
console.error('浙江省数据获取失败,使用硬编码数据:', fallbackError)
// 最终回退到硬编码数据
regionOptions.value = [
{
value: '浙江省',
label: '浙江省',
children: [
{
value: '宁波市',
label: '宁波市',
children: [
{ value: '鄞州区', label: '鄞州区' },
{ value: '海曙区', label: '海曙区' },
{ value: '江北区', label: '江北区' },
{ value: '北仑区', label: '北仑区' }
]
},
{
value: '杭州市',
label: '杭州市',
children: [
{ value: '西湖区', label: '西湖区' },
{ value: '上城区', label: '上城区' },
{ value: '拱墅区', label: '拱墅区' },
{ value: '余杭区', label: '余杭区' }
]
}
]
}
]
}
}

View File

@@ -47,7 +47,7 @@
</div>
<div class="stat-content">
<div class="stat-number">¥{{ (Number(stats.total_commission) || 0).toFixed(2) }}</div>
<div class="stat-label">佣金</div>
<div class="stat-label">营收</div>
<div class="stat-trend">¥{{ (Number(stats.monthly_commission) || 0).toFixed(2) }} 本月</div>
</div>
</div>
@@ -58,7 +58,7 @@
</div>
<div class="stat-content">
<div class="stat-number">¥{{ (Number(stats.daily_commission) || 0).toFixed(2) }}</div>
<div class="stat-label">今日佣金</div>
<div class="stat-label">今日营收</div>
<div class="stat-trend">{{ stats.daily_commission_records || 0 }} 笔记录</div>
</div>
</div>
@@ -79,10 +79,10 @@
<!-- 数据可视化图表 -->
<div class="charts-section">
<!-- 佣金趋势图 -->
<!-- 营收趋势图 -->
<div class="chart-card">
<div class="card-header">
<h3>佣金趋势</h3>
<h3>营收趋势</h3>
<el-select v-model="chartPeriod" size="small" @change="loadChartData">
<el-option label="近7天" value="7d" />
<el-option label="近30天" value="30d" />
@@ -105,20 +105,6 @@
</div>
</div>
<!-- 快速操作 -->
<div class="quick-actions">
<div class="action-card" @click="goToWithdrawals">
<div class="action-icon withdraw">
<el-icon><CreditCard /></el-icon>
</div>
<div class="action-content">
<div class="action-title">佣金提现</div>
<div class="action-desc">快速提现到账</div>
</div>
<el-icon class="arrow"><ArrowRight /></el-icon>
</div>
</div>
<!-- 标签页 -->
<el-tabs v-model="activeTab" class="main-tabs">
<!-- 商户管理 -->
@@ -144,7 +130,7 @@
<div class="stat-item">
<span class="stat-label">早期商户</span>
<span class="stat-value early">{{ stats.early_merchants || 0 }}</span>
<el-tooltip content="注册时间早于代理加入时间的商户,不参与佣金计算" placement="top">
<el-tooltip content="注册时间早于代理加入时间的商户,不参与营收计算" placement="top">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
@@ -189,7 +175,7 @@
</el-tag>
<el-tooltip
v-if="row.is_early_merchant"
content="该商户注册时间早于代理加入时间不参与佣金计算"
content="该商户注册时间早于代理加入时间不参与营收计算"
placement="top"
>
<el-icon class="info-icon"><InfoFilled /></el-icon>
@@ -240,11 +226,11 @@
</div>
</el-tab-pane>
<!-- 佣金记录 -->
<el-tab-pane label="佣金记录" name="commissions">
<!-- 营收记录 -->
<el-tab-pane label="营收记录" name="commissions">
<div class="commissions-section">
<div class="section-header">
<h3>佣金记录</h3>
<h3>营收记录</h3>
<el-button @click="loadCommissions" :loading="loadingCommissions">
刷新
</el-button>
@@ -266,25 +252,18 @@
</template>
</el-table-column>
<el-table-column prop="real_name" label="真实姓名" :width="null" min-width="80" class-name="hide-on-small-mobile" />
<el-table-column prop="commission_amount" label="佣金金额" :width="null" min-width="90">
<el-table-column prop="commission_amount" label="营收金额" :width="null" min-width="90">
<template #default="{ row }">
¥{{ (Number(row.commission_amount) || 0).toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="commission_type" label="佣金类型" :width="null" min-width="90">
<el-table-column prop="commission_type" label="营收类型" :width="null" min-width="90">
<template #default="{ row }">
<el-tag size="small" :type="getCommissionTypeTagType(row.commission_type)">
{{ getCommissionTypeText(row.commission_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" :width="null" min-width="70">
<template #default="{ row }">
<el-tag size="small" :type="getCommissionStatusType(row.status)">
{{ getCommissionStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="生成时间" :width="null" min-width="120" class-name="hide-on-mobile">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
@@ -318,43 +297,6 @@
</div>
</el-tab-pane>
<!-- 佣金提现 -->
<el-tab-pane label="佣金提现" name="withdrawals">
<div class="withdrawals-section">
<div class="section-header">
<h3>佣金提现</h3>
<el-button type="primary" @click="goToWithdrawals">
进入提现页面
</el-button>
</div>
<div class="withdrawal-summary">
<div class="summary-card">
<div class="summary-item">
<span class="label">可提现金额:</span>
<span class="value amount">¥{{ (Number(stats.available_amount) || 0).toFixed(2) }}</span>
</div>
<div class="summary-item">
<span class="label">提现中金额:</span>
<span class="value">¥{{ (Number(stats.pending_withdrawal) || 0).toFixed(2) }}</span>
</div>
<div class="summary-item">
<span class="label">已提现金额:</span>
<span class="value">¥{{ (Number(stats.withdrawn_amount) || 0).toFixed(2) }}</span>
</div>
</div>
<div class="quick-actions">
<el-button type="primary" size="large" @click="goToWithdrawals">
立即提现
</el-button>
</div>
</div>
</div>
</el-tab-pane>
<!-- 商户转账记录 -->
<el-tab-pane label="商户转账记录" name="transfers">
<div class="transfers-section">
@@ -505,7 +447,7 @@ const merchantsPage = ref(1)
const merchantsPageSize = ref(10)
const merchantsTotal = ref(0)
// 佣金数据
// 营收数据
const commissions = ref([])
const loadingCommissions = ref(false)
const commissionsPage = ref(1)
@@ -611,7 +553,7 @@ const loadCommissions = async () => {
commissions.value = data.data.commissions
commissionsTotal.value = data.data.summary.total_records
} catch (error) {
ElMessage.error('加载佣金记录失败')
ElMessage.error('加载营收记录失败')
} finally {
loadingCommissions.value = false
}
@@ -699,8 +641,8 @@ const getAuditStatusText = (status) => {
}
/**
* 获取佣金类型标签类型
* @param {string} type - 佣金类型
* 获取营收类型标签类型
* @param {string} type - 营收类型
* @returns {string} 标签类型
*/
const getCommissionTypeTagType = (type) => {
@@ -712,37 +654,29 @@ const getCommissionTypeTagType = (type) => {
}
/**
* 获取佣金类型文本
* @param {string} type - 佣金类型
* 获取营收类型文本
* @param {string} type - 营收类型
* @returns {string} 显示文本
*/
const getCommissionTypeText = (type) => {
const texts = {
registration: '注册佣金',
matching: '匹配佣金'
registration: '注册营收',
matching: '匹配营收'
}
return texts[type] || type
}
/**
* 获取佣金状态标签类型
* @param {string} status - 佣金状态
* 获取营收状态标签类型
* @param {string} status - 营收状态
* @returns {string} 标签类型
*/
const getCommissionStatusType = (status) => {
// 由于agent_commission_records表没有status字段所有佣金记录都是已生成状态
return 'success'
}
/**
* 获取佣金状态文本
* @param {string} status - 佣金状态
* 获取营收状态文本
* @param {string} status - 营收状态
* @returns {string} 显示文本
*/
const getCommissionStatusText = (status) => {
// 由于agent_commission_records表没有status字段所有佣金记录都是已生成状态
return '已生成'
}
/**
* 获取转账状态标签类型
@@ -805,17 +739,9 @@ const getTransferTypeType = (type) => {
*/
const getTransferTypeText = (type) => {
const texts = {
deposit: '充值',
withdraw: '提现',
transfer: '转账',
commission: '佣金',
refund: '退款',
penalty: '罚金',
initial: '初始转账',
return: '返还转账',
user_to_user: '用户转账',
user_to_public: '用户转公户',
public_to_user: '户转用户'
user_to_agent: '用户转代理',
user_to_system: '户转系统',
}
return texts[type] || type
}
@@ -874,13 +800,6 @@ const maskUsername = (username) => {
return username[0] + '*'.repeat(username.length - 2) + username[username.length - 1]
}
/**
* 跳转到提现页面
*/
const goToWithdrawals = () => {
router.push('/agent/withdrawals')
}
/**
* 刷新所有数据
*/
@@ -933,7 +852,7 @@ const loadChartData = async () => {
* 生成模拟图表数据(用于演示)
*/
const generateMockChartData = () => {
// 模拟佣金趋势数据
// 模拟营收趋势数据
const days = chartPeriod.value === '7d' ? 7 : chartPeriod.value === '30d' ? 30 : 90
const trendData = []
const today = new Date()
@@ -968,11 +887,11 @@ const updateChartOptions = () => {
return
}
// 佣金趋势图配置
// 营收趋势图配置
commissionTrendOption.value = {
tooltip: {
trigger: 'axis',
formatter: '{b}<br/>佣金: ¥{c}'
formatter: '{b}<br/>营收: ¥{c}'
},
grid: {
left: '3%',
@@ -1208,6 +1127,7 @@ onMounted(async () => {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: center;
}
.stat-item {
@@ -1381,6 +1301,25 @@ onMounted(async () => {
padding: 0 16px;
}
:deep(.el-tabs__nav) {
width: 100%;
display: flex;
}
:deep(.el-tabs__item) {
flex: 1;
text-align: center;
justify-content: center;
padding: 12px 8px;
border-radius: 12px;
margin: 0 2px;
transition: all 0.3s ease;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-header {
display: flex;
justify-content: space-between;
@@ -1421,10 +1360,17 @@ onMounted(async () => {
}
:deep(.el-tabs__item) {
padding: 12px 20px;
flex: 1;
text-align: center;
justify-content: center;
padding: 12px 8px;
border-radius: 12px;
margin: 0 4px;
margin: 0 2px;
transition: all 0.3s ease;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-tabs__item.is-active) {
@@ -1436,70 +1382,6 @@ onMounted(async () => {
display: none;
}
/* 提现相关样式 */
.withdrawals-section {
min-height: 300px;
}
.withdrawal-summary {
display: flex;
flex-direction: column;
gap: 20px;
}
.summary-card {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(64, 158, 255, 0.1);
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid rgba(64, 158, 255, 0.1);
}
.summary-item:last-child {
border-bottom: none;
}
.summary-item .label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.summary-item .value {
font-size: 16px;
font-weight: 700;
color: #303133;
}
.summary-item .value.amount {
color: #f56c6c;
font-size: 18px;
}
.withdrawals-section .quick-actions {
padding: 0;
margin: 0;
background: none;
box-shadow: none;
}
.withdrawals-section .quick-actions .el-button {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
/* 表格样式 */
:deep(.el-table) {
border-radius: 12px;
@@ -1543,7 +1425,8 @@ onMounted(async () => {
/* 商户统计信息样式 */
.merchant-stats {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 24px;
padding: 16px;
background: #f8f9fa;
@@ -1559,7 +1442,6 @@ onMounted(async () => {
}
.merchant-stats .stat-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
@@ -1580,7 +1462,6 @@ onMounted(async () => {
/* 商户类型相关样式 */
.info-icon {
margin-left: 4px;
color: #E6A23C;
cursor: help;
font-size: 14px;
@@ -1659,8 +1540,17 @@ onMounted(async () => {
}
:deep(.el-tabs__item) {
padding: 8px 12px;
flex: 1;
text-align: center;
justify-content: center;
padding: 8px 4px;
font-size: 14px;
border-radius: 8px;
margin: 0 1px;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-table) {

View File

@@ -96,25 +96,34 @@
label-width="100px"
>
<el-form-item label="选择区域" prop="region_id">
<el-select
v-model="applyForm.region_id"
placeholder="请选择代理区域"
style="width: 100%"
filterable
>
<el-option-group
v-for="city in groupedRegions"
:key="city.name"
:label="city.name"
>
<el-option
v-for="region in city.districts"
:key="region.id"
:label="region.district_name"
:value="region.id"
/>
</el-option-group>
</el-select>
<!-- <el-select-->
<!-- v-model="applyForm.region_id"-->
<!-- placeholder="请选择代理区域"-->
<!-- style="width: 100%"-->
<!-- filterable-->
<!-- >-->
<!-- <el-option-group-->
<!-- v-for="city in groupedRegions"-->
<!-- :key="city.name"-->
<!-- :label="city.name"-->
<!-- >-->
<!-- <el-option-->
<!-- v-for="region in city.districts"-->
<!-- :key="region.id"-->
<!-- :label="region.district_name"-->
<!-- :value="region.id"-->
<!-- />-->
<!-- </el-option-group>-->
<!-- </el-select>-->
<el-cascader v-model="applyForm.region_id"
popper-class="custom-cascader"
:show-all-levels="false"
placeholder="请选择代理区域"
:options="regions"
:props="regionsProp"
clearable />
</el-form-item>
<el-form-item label="真实姓名" prop="real_name">
@@ -208,6 +217,13 @@ const applyRules = {
]
}
// 设置省市区级联选择器配置
const regionsProp = {
value: 'code',
label: 'label',
children: 'children'
}
// 计算属性
const groupedRegions = computed(() => {
const grouped = {}
@@ -226,7 +242,7 @@ const groupedRegions = computed(() => {
// 方法
const loadRegions = async () => {
try {
const { data } = await api.get('/agents/regions')
const { data } = await api.get('/regions/provinces')
regions.value = data.data
} catch (error) {
ElMessage.error('加载区域列表失败')
@@ -262,11 +278,12 @@ const handleLogin = async () => {
const handleApply = async () => {
if (!applyFormRef.value) return
try {
await applyFormRef.value.validate()
applyLoading.value = true
// 将区id设置为最终id
applyForm.region_id = applyForm.region_id[2]
const { data } = await api.post('/agents/apply', applyForm)
if (data.success) {
@@ -294,7 +311,7 @@ onMounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
/* 使用与主登录页面一致的样式 */
.agent-login-page {
min-height: 100vh;
@@ -505,4 +522,14 @@ onMounted(() => {
padding: 30px 20px;
}
}
</style>
<style>
.custom-cascader{
.el-cascader-panel{
.el-cascader-menu{
min-width: 100px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -30,8 +30,16 @@
<div class="product-details">
<div class="product-price">
<span class="price-label">实付</span>
<el-icon class="coin-icon"><Coin /></el-icon>
<span class="price-value">{{ totalPrice }}</span>
<div class="price-container">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ totalPrice }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ totalPointsPrice }}</span>
</div>
</div>
</div>
<div class="quantity-selector">
<el-button size="small" @click="decreaseQuantity" :disabled="quantity <= 1">-</el-button>
@@ -56,10 +64,10 @@
class="spec-item"
:class="{
active: selectedSpecs[specName]?.id === option.id,
disabled: availableSpecs[specName] && !availableSpecs[specName][option.id]
disabled: availableSpecs[specName] && !availableSpecs[specName][option.id] && selectedSpecs[specName]?.id !== option.id
}"
@click="selectSpec(specName, option)"
:disabled="availableSpecs[specName] && !availableSpecs[specName][option.id]"
:disabled="availableSpecs[specName] && !availableSpecs[specName][option.id] && selectedSpecs[specName]?.id !== option.id"
>
<span class="spec-label">{{ option.name }}</span>
</div>
@@ -122,6 +130,7 @@ import {
ArrowRight
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { getImageUrl } from '@/config'
const route = useRoute()
const router = useRouter()
@@ -141,12 +150,61 @@ const specIdToOrder = ref({}) // 规格ID到顺序编号的映射
// 计算属性
const totalPrice = computed(() => {
if (!product.value) return 0
return product.value.points * quantity.value
return product.value.rongdou_price * quantity.value
})
const totalPointsPrice = computed(() => {
if (!product.value) return 0
return product.value.points_price * quantity.value
})
// 动态计算当前场景下必须选择的规格名称集合(基于已选项与后端可用组合)
const requiredSpecNames = computed(() => {
const allSpecNames = Object.keys(specGroups.value)
if (!product.value || !Array.isArray(product.value.specifications)) {
return allSpecNames
}
const availableSpecs = product.value.specifications.filter(s => s.is_available)
// 已选的规格项ID
const selectedIds = allSpecNames
.map(name => selectedSpecs.value[name]?.id)
.filter(Boolean)
// 能与当前已选项兼容的可用规格规则集合
const candidateSpecs = availableSpecs.filter(spec => {
const detailIds = spec.spec_details.map(d => d.id)
return selectedIds.every(id => detailIds.includes(id))
})
if (candidateSpecs.length === 0) {
// 若暂无兼容组合,默认要求全部(防止误判)
return allSpecNames
}
// 计算每个候选组合涉及的规格名称集
const nameSets = candidateSpecs.map(spec => {
const names = spec.spec_details.map(d => d.spec_name)
return Array.from(new Set(names))
})
// 优先选择包含当前已选规格名称的最小集合
const selectedNames = allSpecNames.filter(name => !!selectedSpecs.value[name])
const filteredByIntent = nameSets
.filter(set => selectedNames.every(n => set.includes(n)))
.sort((a, b) => a.length - b.length)
if (filteredByIntent.length > 0) return filteredByIntent[0]
// 兜底:选择最小的集合
nameSets.sort((a, b) => a.length - b.length)
return nameSets[0]
})
const canPurchase = computed(() => {
const specNames = Object.keys(specGroups.value)
const allSpecsSelected = specNames.every(specName => selectedSpecs.value[specName])
const required = requiredSpecNames.value
const allSpecsSelected = required.every(specName => selectedSpecs.value[specName])
return allSpecsSelected && quantity.value > 0
})
@@ -163,40 +221,38 @@ const decreaseQuantity = () => {
}
}
// 检查规格组合是否有效
// 检查规格组合是否有效忽略顺序按ID集合匹配
const isValidCombination = (testSelection) => {
const selectedIds = []
const specNames = Object.keys(specGroups.value)
// 按规格名称顺序收集选中的规格ID转换顺序编号
// 收集已选规格的真实ID不再转换顺序编号
specNames.forEach(specName => {
if (testSelection[specName]) {
const specId = testSelection[specName].id
const orderNumber = specIdToOrder.value[specId]
selectedIds.push(orderNumber)
} else {
selectedIds.push(null) // 未选择的规格用null占位
selectedIds.push(testSelection[specName].id)
}
})
// 如果还没有选择完所有规格,检查部分选择是否与任何有效组合兼容
if (selectedIds.includes(null)) {
return validCombinations.value.some(combinationKey => {
const keyParts = combinationKey.split('-').map(k => parseInt(k))
// 检查当前部分选择是否与这个combination_key兼容
return selectedIds.every((selectedOrder, index) => {
// 如果该位置未选择,则兼容
if (selectedOrder === null) return true
// 如果该位置已选择,检查是否匹配
return selectedOrder === keyParts[index]
})
if (!product.value || !Array.isArray(product.value.specifications)) {
return false
}
// 仅考虑后端标记为可用的规格组合
const availableSpecs = product.value.specifications.filter(s => s.is_available)
// 部分选择只要存在一个规格规则其spec_details包含所有已选ID即可
if (selectedIds.length < specNames.length) {
return availableSpecs.some(spec => {
const detailIds = spec.spec_details.map(d => d.id)
return selectedIds.every(id => detailIds.includes(id))
})
}
// 如果选择了所有规格,检查完整组合是否有效
const combinationKey = selectedIds.join('-')
return validCombinations.value.includes(combinationKey)
// 完整选择必须存在一个规格规则其ID集合与所选集合相等忽略顺序
return availableSpecs.some(spec => {
const detailIds = spec.spec_details.map(d => d.id)
return detailIds.length === selectedIds.length && selectedIds.every(id => detailIds.includes(id))
})
}
// 更新可选规格状态
@@ -218,16 +274,20 @@ const updateAvailableSpecs = () => {
// 选择规格
const selectSpec = (specName, option) => {
// 检查该选项是否被禁用
// 二次点击已选中项则取消选择
if (selectedSpecs.value[specName]?.id === option.id) {
delete selectedSpecs.value[specName]
updateAvailableSpecs()
return
}
// 检查该选项是否被禁用(但允许点击已选项取消)
if (availableSpecs.value[specName] && !availableSpecs.value[specName][option.id]) {
ElMessage.warning('该规格组合不可选,请选择其他规格')
return
}
selectedSpecs.value[specName] = option
console.log(`选择${specName}:`, option)
console.log('当前选中的所有规格:', selectedSpecs.value)
// 更新可选规格状态
updateAvailableSpecs()
}
@@ -293,16 +353,15 @@ const parseSpecifications = (specifications) => {
// 转换Set为数组并解析JSON
const finalSpecGroups = {}
let orderCounter = 1
Object.keys(tempSpecGroups).forEach(specName => {
finalSpecGroups[specName] = Array.from(tempSpecGroups[specName]).map(item => JSON.parse(item))
// 按sort_order排序
finalSpecGroups[specName].sort((a, b) => a.sort_order - b.sort_order)
// 为每个规格选项分配顺序编号从1开始
// 使用规格项自身ID作为映射避免与后端combination_key不一致
finalSpecGroups[specName].forEach(option => {
specIdToOrderMap[option.id] = orderCounter++
specIdToOrderMap[option.id] = option.id
})
})
@@ -313,7 +372,7 @@ const parseSpecifications = (specifications) => {
specIdToOrder.value = specIdToOrderMap
console.log('有效的规格组合键:', validCombinationKeys)
console.log('规格ID到顺序编号映射:', specIdToOrderMap)
console.log('规格ID映射(按ID本身):', specIdToOrderMap)
// 初始化可选规格状态
updateAvailableSpecs()
@@ -322,36 +381,31 @@ const parseSpecifications = (specifications) => {
console.log('解析后的规格分组:', finalSpecGroups)
}
// 根据选中的规格组合找到对应的规格规则ID
// 根据选中的规格组合找到对应的规格规则ID忽略顺序按ID集合匹配
const getSelectedSpecificationId = () => {
const specNames = Object.keys(specGroups.value)
const selectedIds = []
// 按规格名称顺序收集选中的规格ID转换为顺序编号
specNames.forEach(specName => {
if (selectedSpecs.value[specName]) {
const specId = selectedSpecs.value[specName].id
const orderNumber = specIdToOrder.value[specId]
selectedIds.push(orderNumber)
}
const required = requiredSpecNames.value
const selectedIds = required
.map(name => selectedSpecs.value[name]?.id)
.filter(Boolean)
// 只有在必选规格都已选择时才匹配具体规则
if (selectedIds.length !== required.length) return null
const availableSpecs = product.value.specifications?.filter(s => s.is_available) || []
const specification = availableSpecs.find(spec => {
const detailIds = spec.spec_details.map(d => d.id)
return detailIds.length === selectedIds.length && selectedIds.every(id => detailIds.includes(id))
})
// 生成combination_key
const combinationKey = selectedIds.join('-')
// 在specifications数组中找到对应的规格规则
const specification = product.value.specifications?.find(spec =>
spec.combination_key === combinationKey
)
return specification ? specification.id : null
}
// 立即购买功能
const handlePurchase = async () => {
// 检查是否选择了所有必需的规格
const specNames = Object.keys(specGroups.value)
for (const specName of specNames) {
// 只校验当前场景下的“必选规格”是否选择
const required = requiredSpecNames.value
for (const specName of required) {
if (!selectedSpecs.value[specName]) {
ElMessage.warning(`请选择${specName}`)
return
@@ -361,7 +415,7 @@ const handlePurchase = async () => {
// 获取选中规格对应的规格规则ID
const specificationId = getSelectedSpecificationId()
if (!specificationId) {
ElMessage.error('所选规格组合无效,请重新选择')
ElMessage.error('所选规格组合无效或不可用,请重新选择')
return
}
@@ -534,23 +588,58 @@ onMounted(() => {
.product-price {
display: flex;
align-items: center;
gap: 4px;
align-items: flex-start;
gap: 8px;
}
.price-label {
font-size: 14px;
color: #666;
margin-top: 2px;
}
.coin-icon {
color: #ffae00;
.price-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.price-value {
.main-price {
display: flex;
align-items: center;
gap: 4px;
}
.rongdou-icon {
width: 20px;
height: 20px;
object-fit: contain;
}
.rongdou-price {
font-size: 18px;
font-weight: bold;
color: #ffae00;
color: #ff6b35;
}
.sub-price {
display: flex;
align-items: center;
gap: 3px;
margin-left: 0;
}
.points-icon {
width: 14px;
height: 14px;
color: #666;
}
.points-price {
font-size: 14px;
color: #666;
font-weight: normal;
}
.quantity-selector {

View File

@@ -65,7 +65,7 @@
/>
</div>
<div class="item-image">
<img :src="item.image || '/imgs/productdetail/商品主图.png'" :alt="item.name" />
<img :src="getImageUrl(item.product.image_url) || '/imgs/productdetail/商品主图.png'" :alt="item.name" />
</div>
<div class="item-info">
<div class="item-name">{{ item.name }}</div>
@@ -74,8 +74,16 @@
<span v-if="item.size" class="item-size">{{ item.size }}</span>
</div>
<div class="item-price">
<el-icon class="coin-icon"><Coin /></el-icon>
<span class="price-value">{{ item.product.points_price }}</span>
<div class="price-container">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ Math.ceil((item.product.points_price || 0) / 10000) }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ item.product.points_price || 0 }}</span>
</div>
</div>
</div>
</div>
<div class="item-actions">
@@ -113,8 +121,16 @@
<div class="selected-count">已选{{ selectedCount }}</div>
<div class="total-price">
<span class="total-label">合计</span>
<el-icon class="coin-icon"><Coin /></el-icon>
<span class="total-value">{{ totalPrice }}</span>
<div class="total-price-container">
<div class="total-main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="total-rongdou-icon" />
<span class="total-rongdou-price">{{ Math.ceil(totalPrice / 10000) }}</span>
</div>
<div class="total-sub-price">
<el-icon class="total-points-icon"><Coin /></el-icon>
<span class="total-points-price">{{ totalPrice }}</span>
</div>
</div>
</div>
</div>
<el-button
@@ -140,6 +156,7 @@ import {
Coin
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { getImageUrl } from '@/config'
const router = useRouter()
@@ -319,7 +336,7 @@ onMounted(() => {
}
.navbar {
background: #ff6b35;
background: #72c9ff;
padding: 12px 16px;
display: flex;
align-items: center;
@@ -483,16 +500,48 @@ onMounted(() => {
.item-price {
display: flex;
align-items: center;
}
.price-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.main-price {
display: flex;
align-items: center;
gap: 4px;
}
.rongdou-icon {
width: 16px;
height: 16px;
}
.rongdou-price {
color: #ff6b35;
font-weight: 600;
font-size: 16px;
}
.coin-icon {
margin-right: 4px;
.sub-price {
display: flex;
align-items: center;
gap: 2px;
}
.points-icon {
font-size: 12px;
color: #ff6b35;
}
.points-price {
color: #ff6b35;
font-weight: 500;
font-size: 12px;
}
.item-actions {
display: flex;
flex-direction: column;
@@ -591,11 +640,46 @@ onMounted(() => {
margin-right: 8px;
}
.total-value {
.total-price-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.total-main-price {
display: flex;
align-items: center;
gap: 4px;
}
.total-rongdou-icon {
width: 20px;
height: 20px;
}
.total-rongdou-price {
color: #ff6b35;
font-weight: 600;
font-size: 20px;
}
.total-sub-price {
display: flex;
align-items: center;
gap: 2px;
}
.total-points-icon {
font-size: 14px;
color: #ff6b35;
}
.total-points-price {
color: #ff6b35;
font-weight: 500;
font-size: 14px;
}
.checkout-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff4757 100%);
border: none;

306
src/views/CouponGet.vue Normal file
View File

@@ -0,0 +1,306 @@
<template>
<div class="coupon-container">
<div class="header">
<div class="back-btn" @click="$router.go(-1)"><</div>
<div class="header-text">
优惠券包
</div>
</div>
<div class="coupon-content">
<div class="discount_for_a_amount_container">
<div class="container-title">
满减券限量
</div>
<div class="coupons-content">
<div class="coupon-item" v-for="coupon in coupons" :key="coupon.id">
<div class="coupon-filtered-item" v-if="coupon.type === 'discount_for_a_amount'"
:style="{
backgroundImage: coupon.got
? 'url(/imgs/shop/coupon/bg_useless1.png)'
: 'url(/imgs/shop/coupon/bg_useful1.png)'
}">
<div class="text-left">{{ coupon.discount }}</div>
<div class="text-mid-w">{{ coupon.for_a_amount }}可用</div>
<div class="text-right" @click="getCoupon('discount_for_a_amount',coupon.id)" v-if="!coupon.got">立即领取</div>
<div class="text-right" v-else>已领取</div>
</div>
</div>
</div>
</div>
<div class="discount_container">
<div class="container-title" style="margin-bottom: 10px;">
抵扣券限量
</div>
<div class="coupons-content grid-container">
<div class="coupon-item" v-for="coupon in coupons" :key="coupon.id">
<div class="coupon-filtered-item2" v-if="coupon.type === 'deduction'"
:style="{
backgroundImage: coupon.got
? 'url(/imgs/shop/coupon/bg_useless2.png)'
: 'url(/imgs/shop/coupon/bg_useful2.png)'
}">
<div class="text-top">{{ coupon.price }}</div>
<div class="text-mid-h">无门槛</div>
<div class="text-bottom" @click="getCoupon('deduction',coupon.id)" v-if="!coupon.got">立即领取</div>
<div class="text-bottom" v-else>已领取</div>
</div>
</div>
</div>
</div>
<div class="deduction_container">
<div class="container-title">
折扣券限量
</div>
<div class="coupons-content">
<div class="coupon-item" v-for="coupon in coupons" :key="coupon.id">
<div class="coupon-filtered-item" v-if="coupon.type === 'discount'"
:style="{
backgroundImage: coupon.got
? 'url(/imgs/shop/coupon/bg_useless1.png)'
: 'url(/imgs/shop/coupon/bg_useful1.png)'
}">
<div class="text-left">{{ coupon.precent/10 }}</div>
<div class="text-right" @click="getCoupon('discount',coupon.id)" v-if="!coupon.got">立即领取</div>
<div class="text-right" v-else>已领取</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useUserStore } from '@/stores/user'
import api from '@/utils/api'
import { ElMessage } from 'element-plus'
const userStore = useUserStore()
const coupons = ref([])
const getAllCoupons = async () => {
const {data} = await api.get('/coupon',{
params: {
user_id: userStore.user.id
}
})
if (data.success) {
coupons.value = data.coupon
console.log(123,coupons.value)
} else {
ElMessage.error(data.message || '优惠券领取失败')
}
}
const getCoupon = async (coupon_type,coupon_id) => {
try {
const {data} = await api.get(`/coupon/${userStore.user.id}`,{
params: {
coupon_type: coupon_type,
coupon_id: coupon_id
}
})
if (data.success) {
ElMessage.success('优惠券领取成功')
getAllCoupons()
} else {
ElMessage.error(data.msg || '优惠券领取失败')
}
} catch (error) {
ElMessage.error('优惠券领取失败')
console.error('优惠券领取失败:', error)
}
}
onMounted(() => {
getAllCoupons()
})
</script>
<style scoped>
.coupon-container {
width: 100%;
height: 100vh;
background-image: url(/imgs/shop/coupon/background.png);
}
.header {
display: flex;
align-items: center;
justify-content: center;
padding-left: 20px;
position: relative;
}
.back-btn {
font-size: 15px;
width: 26px;
height: 26px;
color: #000000;
background: transparent;
border: none;
position: absolute;
left: 0;
margin-left: 30px;
margin-top: 5px;
}
.header-text {
width: 80px;
height: 28px;
opacity: 1;
font-family: SF Pro;
font-weight: 650;
font-style: Expanded Semibold;
font-size: 20px;
line-height: 28px;
letter-spacing: 0%;
color: #2F4FB5;
text-align: center;
}
.coupon-content {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
padding: 20px;
height: 100vh;
}
.discount_for_a_amount_container,
.discount_container,
.deduction_container {
background: white;
width: 348px;
padding: 10px;
top: 32px;
opacity: 1;
border-radius: 8px;
margin-bottom: 20px;
}
.container-title {
width: 96;
height: 20;
top: 324px;
left: 105px;
opacity: 1;
font-family: SF Pro;
font-weight: 700;
font-style: Bold;
font-size: 16px;
line-height: 20px;
letter-spacing: 0%;
color: #2F4FB5;
text-align: center;
}
.coupon-filtered-item {
width: 324px;
height: 66px;
top: 58px;
left: 12px;
opacity: 1;
background-size: 100% 100%;
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.text-left {
font-size: 25px;
font-weight: bold;
color: #305DEF;
}
.text-mid-w {
font-size: 14px;
color: #305DEF;
flex: 1;
text-align: center;
font-family: SF Pro;
font-weight: 590;
font-style: Semibold;
font-size: 16px;
line-height: 20px;
letter-spacing: 0%;
}
.text-right {
font-size: 14px;
color: #ffffff;
cursor: pointer;
padding: 5px 10px;
border-radius: 100px;
background: #305DEF;
transition: background-color 0.3s;
margin-right: -10px;
}
.text-right:hover {
background-color: #305DEF;
}
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
justify-items: center;
}
.coupon-item {
display: contents;
}
.coupon-filtered-item2 {
width: 83px;
height: 94px;
opacity: 1;
border-radius: 4px;
background-size: 100% 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 10px 0;
}
.text-top {
font-family: SF Pro;
font-weight: 700;
font-style: Bold;
font-size: 18px;
line-height: 20px;
letter-spacing: 0%;
color: #305DEF;
}
.text-mid-h {
font-family: SF Pro;
font-weight: 400;
font-style: Regular;
font-size: 15px;
line-height: 20px;
letter-spacing: 0%;
color: #305DEF;
margin-top: 10px;
}
.text-bottom {
font-size: 10px;
color: #305DEF;
cursor: pointer;
padding: 5px 10px;
border-radius: 100px;
transition: background-color 0.3s;
margin-top: 8px;
}
</style>

267
src/views/CouponManage.vue Normal file
View File

@@ -0,0 +1,267 @@
<template>
<div class="coupon-container">
<div class="header">
<div class="back-btn" @click="$router.go(-1)"><</div>
<div class="header-text">
优惠券包
</div>
<div class="link-btn" @click="$router.push('/coupon')">领券中心</div>
</div>
<div class="coupon-list">
<div class="coupon-item" v-for="coupon in couponList" :key="coupon.id">
<div class="coupon-info">
<div class="coupon-name">{{ getCouponName(coupon.couponInfo.type) }}</div>
<div class="right">
<div class="coupon-title">
{{ getCouponTitle(coupon.couponInfo.type)[0] }}
<span class="value">
{{ coupon.couponInfo.discount === "0.00" ? coupon.couponInfo.price === "0.00" ? coupon.couponInfo.precent/10 : coupon.couponInfo.price : coupon.couponInfo.discount }}
</span>
{{ getCouponTitle(coupon.couponInfo.type)[1] }}
</div>
<div class="coupon-describe">{{ getCouponDescribe(coupon.couponInfo.type, coupon.couponInfo.for_a_amount === "0.00" ? coupon.couponInfo.price === "0.00" ? coupon.couponInfo.precent : coupon.couponInfo.price : coupon.couponInfo.for_a_amount) }}</div>
</div>
</div>
<img src="/imgs/line.png" alt="虚线" class="line-image">
<div class="coupon-bottom">
{{ coupon.use_time === null ? '未使用' : '已使用' }}
<span class="text" @click="showCouponDetail(coupon)">适用商品</span>
</div>
</div>
</div>
<el-drawer
title="优惠券详情"
v-model="dialogVisible"
width="50%"
direction="btt"
size="90%"
show_close
>
<div class="product-item" v-for="(product, index) in couponDetail.products" :key="product.id" @click="$router.push(`/productsummary/${couponDetail.products_id[index]}`)">
{{ product.name }}
</div>
<template #footer>
<el-button type="primary" @click="dialogVisible = false">确定</el-button>
</template>
</el-drawer>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/utils/api'
import { useUserStore } from '@/stores/user'
const user = useUserStore().user
const couponList = ref([])
const dialogVisible = ref(false)
const couponDetail = ref([])
const getCouponList = async () => {
const response = await api.get(`/coupon/user/${user.id}`)
couponList.value = response.data.coupon
}
const getCouponName = (couponType) => {
const couponName = {
'discount_for_a_amount': '满减券',
'deduction': '抵扣券',
'discount': '折扣券',
}
return couponName[couponType] || couponType
}
const getCouponTitle = (couponType) => {
const couponTitle = {
'discount_for_a_amount': ['¥',''],
'deduction': ['¥',''],
'discount': ['商品','折券'],
}
return couponTitle[couponType] || couponType
}
const getCouponDescribe = (couponType, value) => {
const couponDescribe = {
'discount_for_a_amount': '满'+value+'可用',
'deduction': '指定商品可用',
'discount': '指定商品可用',
}
return couponDescribe[couponType] || couponType
}
const showCouponDetail = (coupon) => {
couponDetail.value = coupon.couponInfo
dialogVisible.value = true
console.log(coupon)
}
onMounted(() => {
getCouponList()
})
</script>
<style scoped>
.coupon-container {
background: linear-gradient(180deg, #E3E8FF 0%, #ffffff00 100%);
background-blend-mode: lighten;
min-height: 100vh;
overflow-x: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: center;
padding-left: 20px;
position: relative;
background: #FFFFFF;
height: 46px;
padding-right: 10px;
padding-left: 10px;
opacity: 1;
}
.back-btn {
font-size: 15px;
width: 26px;
height: 26px;
color: #000000;
background: transparent;
border: none;
position: absolute;
left: 0;
margin-left: 30px;
margin-top: 5px;
}
.header-text {
width: 80px;
height: 28px;
opacity: 1;
font-family: SF Pro;
font-weight: 650;
font-style: Expanded Semibold;
font-size: 20px;
line-height: 28px;
letter-spacing: 0%;
text-align: center;
margin-left: -20px;
}
.link-btn {
background: #CADBFF;
width: 80px;
height: 28px;
padding-right: 16px;
padding-left: 16px;
gap: 10px;
opacity: 1;
border-radius: 99px;
border: none;
position: absolute;
right: 0;
font-size: 10px;
line-height: 28px;
text-align: center;
margin-right: 15px;
}
.coupon-list {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.coupon-item {
background: #F5F7FF;
width: 334px;
height: 116px;
padding-top: 8px;
padding-right: 10px;
padding-bottom: 8px;
padding-left: 10px;
gap: 10px;
opacity: 1;
border-radius: 10px;
margin-bottom: 15px;
box-shadow: 2px 4px 4px 0px #00000040;
}
.coupon-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
justify-items: center;
}
.coupon-name {
background: #D4E2FF;
width: 80px;
height: 28px;
padding-right: 16px;
padding-left: 16px;
gap: 10px;
opacity: 1;
border-radius: 99px;
font-size: 15px;
line-height: 28px;
text-align: center;
color: #2F4FB5;
margin-top: 20px;
}
.coupon-title,
.coupon-describe {
width: 300px;
height: 28px;
opacity: 1;
font-family: SF Pro;
font-size: 16px;
line-height: 28px;
vertical-align: middle;
margin-left: 20px;
}
.coupon-title {
margin-top: 10px;
}
.value {
font-size: 25px;
font-weight: bold;
color: #2F4FB5;
}
.line-image {
margin-top: 10px;
}
.coupon-bottom {
margin-top: 5px;
margin-left: 15px;
}
.text {
font-size: 14px;
margin-left: 170px;
margin-top: 5px;
background: #2954E0;
color: #ffffff;
height: 28px;
padding-right: 10px;
padding-left: 10px;
gap: 10px;;
opacity: 1;
border-radius: 999px;
}
.product-item {
margin-bottom: 10px;
}
</style>

View File

@@ -7,7 +7,7 @@
<!-- 客服二维码图片 -->
<div class="customer-service-container">
<img src="/imgs/mainpage/客服码.jpg" alt="客服码" class="customer-service-img">
<img src="/imgs/mainpage/kefuma.jpg" alt="客服码" class="customer-service-img">
<div class="customer-service-text">扫码联系客服长按识别</div>
</div>
</div>

553
src/views/Distribution.vue Normal file
View File

@@ -0,0 +1,553 @@
<template>
<div class="distribution-page">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-left">
<el-button
:text="true"
@click="$router.go(-1)"
class="back-btn"
>
<el-icon>
<ArrowLeft/>
</el-icon>
</el-button>
</div>
<div class="nav-center">
<h1 class="nav-title">分享</h1>
</div>
<div class="nav-right">
<!-- 占位元素保持标题居中 -->
</div>
</nav>
<div class="page-content">
<!-- 分销说明 -->
<div class="intro-section">
<div class="intro-card">
<h3 class="intro-title">邀请好友注册</h3>
<p class="intro-desc">分享您的专属二维码邀请好友注册</p>
</div>
</div>
<!-- 二维码区域 -->
<div class="qrcode-section">
<div class="qrcode-card">
<h4 class="qrcode-title">我的推广二维码</h4>
<div class="qrcode-container">
<canvas
ref="qrcodeCanvas"
class="qrcode-canvas"
v-show="qrcodeGenerated"
></canvas>
<div v-show="!qrcodeGenerated" class="qrcode-loading">
<el-icon class="loading-icon">
<Loading/>
</el-icon>
<span>生成中...</span>
</div>
</div>
<p class="qrcode-tip">扫描二维码好友可直接注册并绑定您的推荐关系</p>
</div>
</div>
<!-- 推广链接 -->
<div class="link-section">
<div class="link-card">
<h4 class="link-title">推广链接</h4>
<div class="link-container">
<el-input
v-model="inviteLink"
readonly
class="link-input"
>
<template #append>
<el-button @click="copyLink" type="primary">
复制
</el-button>
</template>
</el-input>
</div>
</div>
</div>
<!-- 下级用户 -->
<el-button class="lower-users-btn" @click="handleLowerUser">查看下级用户</el-button>
<el-drawer
v-model="drawerLowerUsers"
title="下级用户"
direction="btt"
:lockScroll="false"
size="70%"
@open="requestLowerUsers(params)"
@closed="closeScrollerLowerUsers"
>
<!-- 一级 -->
<ul v-if="!showScrollerLowerUsersByUser" v-infinite-scroll="loadScrollerLowerUsers" class="users-list"
style="overflow: auto"
:infinite-scroll-immediate="false"
:infinite-scroll-disabled="scrollLowerUsers">
<!-- lowerUsers 空数据处理 -->
<el-empty v-if="lowerUsers.length==0" :image-size="200"/>
<li v-else v-for="(item, index) in lowerUsers" :key="item.id">
<el-divider v-if="index!=0" border-style="dashed"/>
<div class="users-item" @click="handleLowerUserByUserId(item)">
<el-image
:src="getImageUrl(item.avatar)"
:preview-src-list="[getImageUrl(item.avatar)]"
class="user-avatar"
fit="cover"
>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
</el-icon>
</div>
</template>
</el-image>
<div class="user-text">
<div class="user-username">用户名{{ item.username }}</div>
<div class="user-date">时间{{ item.created_at }}</div>
</div>
</div>
</li>
<el-divider v-if="scrollLowerUsers" border-style="dashed" content-position="center">到底了~</el-divider>
</ul>
<!-- 二级 -->
<ul v-else v-infinite-scroll="loadScrollerLowerUsersByUser" class="users-list" style="overflow: auto"
:infinite-scroll-immediate="false"
:infinite-scroll-disabled="scrollLowerUsersByUser">
<el-page-header style="margin-bottom: 20px" @back="onBack">
<template #content>
<span class="text-large font-600 mr-3"
style="font-size: 14px"> 正在查看{{ currentUser.username }}的下级 </span>
</template>
<div class="mt-4 text-sm font-bold"></div>
</el-page-header>
<!-- lowerUsers 空数据处理 -->
<el-empty v-if="lowerUsersByUser.length==0" :image-size="200"/>
<li v-else v-for="(item, index) in lowerUsersByUser" :key="item.id">
<el-divider v-if="index!=0" border-style="dashed"/>
<div class="users-item">
<el-image
:src="getImageUrl(item.avatar)"
:preview-src-list="[getImageUrl(item.avatar)]"
class="user-avatar"
fit="cover"
>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
</el-icon>
</div>
</template>
</el-image>
<div class="user-text">
<div class="user-username">用户名{{ item.username }}</div>
<div class="user-date">时间{{ item.created_at }}</div>
</div>
</div>
</li>
<el-divider v-if="scrollLowerUsersByUser" border-style="dashed" content-position="center">到底了~</el-divider>
</ul>
</el-drawer>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, nextTick, reactive} from 'vue'
import {useRouter} from 'vue-router'
import {useUserStore} from '@/stores/user'
import {ElMessage} from 'element-plus'
import {
ArrowLeft,
Loading, Picture
} from '@element-plus/icons-vue'
import QRCode from 'qrcode'
import {distributionAPI} from "@/utils/api.js";
import {getImageUrl} from "@/config/index.js";
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const qrcodeCanvas = ref(null)
const qrcodeGenerated = ref(false)
const generating = ref(false)
const inviteLink = ref('')
// 下级用户
const drawerLowerUsers = ref(false)
// 生成邀请链接
const generateInviteLink = () => {
const userId = userStore.user?.id || 'guest'
console.log(userId)
const baseUrl = `${window.location.origin}/frontend`
return `${baseUrl}/register?inviter=${userId}`// 实施用
// return `http://192.168.1.250:5173/register?inviter=${userId}`// 测试用
}
// 生成二维码
const generateQRCode = async () => {
try {
generating.value = true
qrcodeGenerated.value = false
// 生成邀请链接
const link = generateInviteLink()
inviteLink.value = link
// 等待DOM更新
await nextTick()
if (qrcodeCanvas.value) {
// 生成二维码到canvas
await QRCode.toCanvas(qrcodeCanvas.value, link, {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
qrcodeGenerated.value = true
}
} catch (error) {
console.error('生成二维码失败:', error)
ElMessage.error('生成二维码失败')
} finally {
generating.value = false
}
}
// 复制链接
const copyLink = async () => {
try {
await navigator.clipboard.writeText(inviteLink.value)
ElMessage.success('链接已复制到剪贴板')
} catch (error) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = inviteLink.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
ElMessage.success('链接已复制到剪贴板')
}
}
// 查看下级用户
const canMultiLevel = ref(false) // 是否有权查看更多级用户
const showScrollerLowerUsersByUser = ref(false) // 一二级切换
const scrollLowerUsers = ref(false) // 一级是否继续滚动
const scrollLowerUsersByUser = ref(false) // 二级是否继续滚动
const lowerUsers = ref([]) // 一级下级
const currentUser = ref({}) // 二级当前选中
const lowerUsersByUser = ref([]) // 二级下级
// 一级传参
const params = {
page: 1,
size: 10
}
// 二级传参
const paramsByUser = {
page: 1,
size: 10,
user_id: ''
}
const pages = ref(1)
const pagesByUser = ref(1)
// 打开查看下级用户抽屉
const handleLowerUser = async () => {
// 判断是否为*代理查看下下级用户
var role = JSON.parse(localStorage.getItem('role'))
if (role.distribution || (role.user_type == 'agent' || role.user_type == 'agent_directly')) {
canMultiLevel.value = true
}
lowerUsers.value = []
drawerLowerUsers.value = true
}
// 请求一级用户
const requestLowerUsers = async (params) => {
await distributionAPI.getLowerUsers(params).then((res) => {
lowerUsers.value = lowerUsers.value.concat(res.data.data)
pages.value = res.data.pagination.pages
if (params.page >= pages.value) {
scrollLowerUsers.value = true
}
})
}
// 请求二级用户
const requestLowerUsersByUser = async (params) => {
await distributionAPI.getLowerUsers(params).then((res) => {
lowerUsersByUser.value = lowerUsersByUser.value.concat(res.data.data)
pagesByUser.value = res.data.pagination.pages
if (params.page >= pagesByUser.value) {
scrollLowerUsersByUser.value = true
}
})
}
// 一级分页请求查看
const loadScrollerLowerUsers = async () => {
if (scrollLowerUsers.value) {
return
}
params.page += 1
await requestLowerUsers(params)
}
// 二级分页请求查看
const loadScrollerLowerUsersByUser = async () => {
if (scrollLowerUsersByUser.value) {
return
}
paramsByUser.page += 1
await requestLowerUsersByUser(paramsByUser)
}
// 查看二级用户
const handleLowerUserByUserId = async (item) => {
// 是否有权查看二级
if (canMultiLevel.value) {
paramsByUser.user_id = item.user_id
currentUser.value = item
await requestLowerUsersByUser(paramsByUser)
showScrollerLowerUsersByUser.value = true
}
}
const onBack = () => {
showScrollerLowerUsersByUser.value = false
}
// 关闭查看下级用户抽屉
const closeScrollerLowerUsers = () => {
lowerUsers.value = []
params.page = 0
scrollLowerUsers.value = false
}
// 生命周期
onMounted(() => {
generateQRCode()
})
</script>
<style scoped lang="scss">
.distribution-page {
min-height: 100vh;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
background: white;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-left,
.nav-right {
flex: 1;
}
.nav-right {
display: flex;
justify-content: flex-end;
}
.back-btn {
color: #409eff;
font-size: 14px;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #333;
}
.page-content {
padding: 16px;
}
.intro-section,
.qrcode-section,
.link-section {
margin-bottom: 16px;
}
.intro-card,
.qrcode-card,
.link-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.intro-title,
.qrcode-title,
.link-title {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.intro-desc {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
.qrcode-container {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
min-height: 200px;
}
.qrcode-canvas {
border: 1px solid #eee;
border-radius: 8px;
}
.qrcode-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #666;
}
.loading-icon {
font-size: 24px;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.qrcode-tip {
text-align: center;
font-size: 12px;
color: #999;
margin: 0 0 20px 0;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
.link-container {
margin-top: 12px;
}
.link-input {
width: 100%;
}
:deep(.link-input .el-input-group__append) {
padding: 0;
width: 50px;
}
:deep(.link-input .el-input-group__append .el-button) {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 15px;
}
/* 确保按钮内文字垂直居中 */
:deep(.link-input .el-input-group__append .el-button span) {
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
/* 响应式设计 */
@media (max-width: 480px) {
.action-buttons {
flex-direction: column;
}
}
.lower-users-btn {
width: 100%;
border-radius: 10px;
}
.users-list {
height: 100%;
padding: 0 0 0 10px;;
margin: 0;
list-style: none;
}
.users-list .users-item {
display: flex; /* 使用Flexbox布局 */
align-items: center; /* 垂直居中对齐 */
gap: 1rem; /* 图片与文字之间的间距 */
.user-avatar {
width: 50px;
height: 50px;
object-fit: cover;
}
.user-text {
.user-username {
margin-bottom: 5px;
font-size: 14px;
}
.user-date {
color: #666;
font-size: 12px;
}
}
}
.users-list .users-item + .list-item {
margin-top: 10px;
}
</style>

View File

@@ -287,6 +287,12 @@ const saveProfile = async () => {
await profileFormRef.value.validate()
saving.value = true
form.wechatQr = form.wechatQr.replace('https://minio.zrbjr.com', '')
form.alipayQr = form.alipayQr.replace('https://minio.zrbjr.com', '')
form.unionpayQr = form.unionpayQr.replace('https://minio.zrbjr.com', '')
form.businessLicense = form.businessLicense.replace('https://minio.zrbjr.com', '')
form.idCardFront = form.idCardFront.replace('https://minio.zrbjr.com', '')
form.idCardBack = form.idCardBack.replace('https://minio.zrbjr.com', '')
const response = await api.put('/users/profile', form)
// 更新本地数据

View File

@@ -1,381 +0,0 @@
<template>
<div class="home">
<!-- 导航栏 -->
<el-header class="header">
<div class="header-content">
<div class="logo">
<h2>前端H5系统</h2>
</div>
<div class="nav-menu">
<el-menu
mode="horizontal"
:default-active="activeIndex"
class="nav-menu-items"
@select="handleMenuSelect"
>
<el-menu-item index="home">首页</el-menu-item>
<el-menu-item index="shop">积分商城</el-menu-item>
<el-menu-item index="about">关于</el-menu-item>
</el-menu>
</div>
<div class="user-actions">
<template v-if="userStore.isAuthenticated">
<el-dropdown @command="handleUserCommand">
<span class="user-info">
<div class="user-avatar">
{{ userStore.user?.username?.charAt(0)?.toUpperCase() }}
</div>
<span class="username">{{ userStore.user?.username }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item command="orders">我的订单</el-dropdown-item>
<el-dropdown-item command="points-history">积分记录</el-dropdown-item>
<el-dropdown-item command="transfers">货款管理</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<!-- 移除登录注册按钮 -->
</template>
</div>
</div>
</el-header>
<!-- 主要内容 -->
<el-main class="main-content">
<!-- 主要内容区域 -->
<div class="main-section">
<div class="container">
<div class="welcome-content">
<h2 class="welcome-title">欢迎使用前端H5系统</h2>
<p class="welcome-description">您的智能管理助手</p>
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-section" v-if="userStore.isAuthenticated">
<div class="container">
<h2 class="section-title">系统概览</h2>
<el-row :gutter="20">
<el-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.key">
<div class="stat-card">
<div class="stat-icon">
<el-icon :size="32"><component :is="stat.icon" /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</el-main>
<!-- 页脚 -->
<el-footer class="footer">
<div class="container">
<p>&copy; 2024 前端H5系统. All rights reserved.</p>
</div>
</el-footer>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ArrowDown, User, View, Clock, Document, Edit, Setting, DataAnalysis, Star } from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const activeIndex = ref('home')
const stats = ref([
{ key: 'users', label: '用户数量', value: 0, icon: 'User' },
{ key: 'orders', label: '订单总数', value: 0, icon: 'Document' },
{ key: 'products', label: '商品数量', value: 0, icon: 'Star' },
{ key: 'transfers', label: '转账记录', value: 0, icon: 'Clock' }
])
// 方法
const handleMenuSelect = (index) => {
activeIndex.value = index
switch (index) {
case 'home':
router.push('/transfers')
break
case 'shop':
router.push('/shop')
break
case 'about':
router.push('/about')
break
}
}
const handleUserCommand = (command) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'orders':
router.push('/orders')
break
case 'points-history':
router.push('/points-history')
break
case 'transfers':
router.push('/transfers')
break
case 'logout':
userStore.logout()
break
}
}
// 获取统计数据
const fetchStats = async () => {
try {
// 这里可以调用相关API获取统计数据
// 暂时使用模拟数据
stats.value = [
{ key: 'users', label: '用户数量', value: 156, icon: 'User' },
{ key: 'orders', label: '订单总数', value: 89, icon: 'Document' },
{ key: 'products', label: '商品数量', value: 45, icon: 'Star' },
{ key: 'transfers', label: '转账记录', value: 23, icon: 'Clock' }
]
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
// 生命周期
onMounted(() => {
if (userStore.isAuthenticated) {
fetchStats()
}
})
</script>
<style scoped>
.home {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.logo h2 {
color: white;
margin: 0;
}
.nav-menu-items {
background: transparent;
border: none;
}
.nav-menu-items .el-menu-item {
color: white;
border: none;
}
.nav-menu-items .el-menu-item:hover,
.nav-menu-items .el-menu-item.is-active {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.user-actions {
display: flex;
align-items: center;
gap: 10px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: white;
cursor: pointer;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.main-content {
flex: 1;
padding: 0 0 80px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.main-section,
.stats-section {
padding: 60px 0;
}
.main-section {
background-color: #f8f9fa;
min-height: 50vh;
}
.section-title {
text-align: center;
font-size: 32px;
margin-bottom: 40px;
color: #303133;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
.welcome-content {
text-align: center;
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
}
.welcome-title {
font-size: 32px;
margin-bottom: 16px;
color: #303133;
font-weight: 600;
}
.welcome-description {
font-size: 16px;
color: #606266;
line-height: 1.6;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.stat-icon {
color: #409eff;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.footer {
background-color: #303133;
color: white;
text-align: center;
}
.footer .container {
padding: 20px;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 15px;
padding: 15px 20px;
}
.nav-menu {
order: 3;
width: 100%;
}
.user-actions {
order: 2;
}
.section-title {
font-size: 24px;
}
.section-header {
flex-direction: column;
gap: 20px;
text-align: center;
}
.welcome-title {
font-size: 24px;
}
.main-content {
padding-bottom: 80px;
}
}
</style>

View File

@@ -1,474 +0,0 @@
<template>
<div class="login-page">
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2>用户登录</h2>
<p>欢迎回到炬融圈</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名或邮箱"
size="large"
:prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
:prefix-icon="Lock"
show-password
clearable
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item prop="captcha">
<Captcha
ref="captchaRef"
v-model="loginForm.captcha"
placeholder="请输入验证码"
size="large"
/>
</el-form-item>
<el-form-item>
<div class="form-options">
<el-checkbox v-model="rememberMe">记住我</el-checkbox>
<el-link type="primary" @click="showForgotPassword">
忘记密码
</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
class="login-button"
:loading="userStore.loading"
@click="handleLogin"
>
{{ userStore.loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<!-- <div class="login-footer">
<p>
还没有账号
<el-link type="primary" @click="$router.push('/register')">
立即注册
</el-link>
</p>
</div> -->
<!-- 备案信息 -->
<div class="icp-info">
<div class="icp-item">
<svg class="icp-icon" viewBox="0 0 1024 1024" width="16" height="16">
<path d="M512 85.333333c-23.466667 0-42.666667 19.2-42.666667 42.666667v85.333333c0 23.466667 19.2 42.666667 42.666667 42.666667s42.666667-19.2 42.666667-42.666667V128c0-23.466667-19.2-42.666667-42.666667-42.666667z" fill="#909399"/>
<path d="M512 256c-141.226667 0-256 114.773333-256 256 0 70.613333 28.586667 134.4 74.666667 180.48L512 874.666667l181.333333-182.186667C739.413333 646.4 768 582.613333 768 512c0-141.226667-114.773333-256-256-256z m0 341.333333c-47.146667 0-85.333333-38.186667-85.333333-85.333333s38.186667-85.333333 85.333333-85.333333 85.333333 38.186667 85.333333 85.333333-38.186667 85.333333-85.333333 85.333333z" fill="#909399"/>
<path d="M170.666667 298.666667c-11.733333-20.48-37.546667-27.306667-58.026667-15.573334-20.48 11.733333-27.306667 37.546667-15.573333 58.026667l42.666666 74.24c11.733333 20.48 37.546667 27.306667 58.026667 15.573333 20.48-11.733333 27.306667-37.546667 15.573333-58.026666l-42.666666-74.24z" fill="#909399"/>
<path d="M853.333333 298.666667l-42.666666 74.24c-11.733333 20.48-4.906667 46.293333 15.573333 58.026666 20.48 11.733333 46.293333 4.906667 58.026667-15.573333l42.666666-74.24c11.733333-20.48 4.906667-46.293333-15.573333-58.026667-20.48-11.733333-46.293333-4.906667-58.026667 15.573334z" fill="#909399"/>
</svg>
<a href="https://beian.miit.gov.cn/" target="_blank" class="icp-link">
浙ICP备2025186895号
</a>
</div>
</div>
</div>
</div>
<!-- 背景装饰 -->
<div class="background-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 表单引用
const loginFormRef = ref()
const captchaRef = ref()
// 表单数据
const loginForm = reactive({
username: '',
password: '',
captcha: ''
})
// 其他状态
const rememberMe = ref(false)
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' }
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 4, max: 4, message: '验证码为4位字符', trigger: 'blur' }
]
}
// 处理登录
const handleLogin = async () => {
if (!loginFormRef.value || !captchaRef.value) return
try {
// 先验证表单
const valid = await loginFormRef.value.validate()
if (!valid) return
// 验证验证码
// const captchaValid = await captchaRef.value.verifyCaptcha(loginForm.captcha)
// if (!captchaValid) {
// loginForm.captcha = ''
// return
// }
// 获取验证码信息
const captchaInfo = captchaRef.value.getCaptchaInfo()
// 提交登录请求(包含验证码信息)
const loginData = {
username: loginForm.username,
password: loginForm.password,
captchaId: captchaInfo.captchaId,
captchaText: captchaInfo.captchaText
}
const result = await userStore.login(loginData)
if (result.success) {
// 登录成功,跳转到目标页面或转账管理
const redirectPath = route.query.redirect || '/transfers'
router.push(redirectPath)
}
} catch (error) {
console.error('登录失败:', error)
// 登录失败后刷新验证码
if (captchaRef.value) {
await captchaRef.value.refreshCaptcha()
}
loginForm.captcha = ''
}
}
// 忘记密码
const showForgotPassword = () => {
ElMessageBox.alert(
'请联系管理员重置密码,或使用演示账号进行体验。',
'忘记密码',
{
confirmButtonText: '确定',
type: 'info'
}
)
}
// 组件挂载时的处理
onMounted(() => {
// 如果已经登录,直接跳转
if (userStore.isAuthenticated) {
const redirectPath = route.query.redirect || '/transfers'
router.push(redirectPath)
}
// 从localStorage恢复记住我状态
const savedUsername = localStorage.getItem('rememberedUsername')
if (savedUsername) {
loginForm.username = savedUsername
rememberMe.value = true
}
})
// 监听记住我状态变化
const handleRememberMe = () => {
if (rememberMe.value && loginForm.username) {
localStorage.setItem('rememberedUsername', loginForm.username)
} else {
localStorage.removeItem('rememberedUsername')
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 20px;
position: relative;
z-index: 10;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #303133;
margin-bottom: 8px;
font-weight: 600;
}
.login-header p {
color: #909399;
font-size: 14px;
}
.login-form {
margin-bottom: 20px;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.login-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 600;
}
.login-footer {
text-align: center;
margin-bottom: 20px;
}
.login-footer p {
color: #606266;
font-size: 14px;
}
.icp-info {
text-align: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid rgba(224, 224, 230, 0.3);
}
.icp-item {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.icp-icon {
flex-shrink: 0;
}
.icp-link {
color: #909399;
font-size: 12px;
text-decoration: none;
transition: color 0.3s ease;
}
.icp-link:hover {
color: #409eff;
text-decoration: underline;
}
.quick-login {
margin-top: 20px;
}
.demo-accounts {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 15px;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 6s ease-in-out infinite;
}
.circle-1 {
width: 200px;
height: 200px;
top: 10%;
left: 10%;
animation-delay: 0s;
}
.circle-2 {
width: 150px;
height: 150px;
top: 60%;
right: 10%;
animation-delay: 2s;
}
.circle-3 {
width: 100px;
height: 100px;
bottom: 20%;
left: 20%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-container {
padding: 15px;
}
.login-card {
padding: 30px 20px;
}
.demo-accounts {
flex-direction: column;
}
.form-options {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.icp-info {
margin-top: 15px;
padding-top: 10px;
}
.icp-link {
font-size: 11px;
}
}
/* Element Plus 组件样式覆盖 */
:deep(.el-input__wrapper) {
border-radius: 8px;
}
:deep(.el-button) {
border-radius: 8px;
}
:deep(.el-divider__text) {
background-color: rgba(255, 255, 255, 0.95);
color: #909399;
}
/* 输入框聚焦效果 */
:deep(.el-input__wrapper:hover),
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409eff inset;
}
/* 加载状态样式 */
.login-button.is-loading {
pointer-events: none;
}
/* 动画效果 */
.login-card {
animation: slideInUp 0.6s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 错误状态样式 */
:deep(.el-form-item.is-error .el-input__wrapper) {
box-shadow: 0 0 0 1px #f56c6c inset;
}
/* 成功状态样式 */
:deep(.el-form-item.is-success .el-input__wrapper) {
box-shadow: 0 0 0 1px #67c23a inset;
}
</style>

View File

@@ -3,6 +3,8 @@
<div class="spacer"></div>
<div class="main-content">
<p class="welcome">{{userName}}欢迎来到炬融圈</p>
<!-- 轮播图 -->
<div class="carousel">
<swiper
@@ -13,7 +15,7 @@
:pagination="{ clickable: true }"
:loop="true"
>
<swiper-slide v-for="(item, index) in carouselItems" :key="index">
<swiper-slide v-for="(item, index) in carouselItems" :key="index" @click="handleCarouselClick(item)" class="carousel-slide">
<img :src="item.image" :alt="item.title" class="carousel-img" />
<div class="carousel-title">{{ item.title }}</div>
</swiper-slide>
@@ -22,43 +24,35 @@
<!-- 头部导航 -->
<div class="header">
<router-link
<div
v-for="(item, index) in headerItems"
:key="index"
:to="item.path"
class="header-item"
custom
v-slot="{ navigate }"
>
<div @click="navigate" class="header-item-content">
<div @click="item.text === '系统公告' ? handleSystemAnnouncementClick() : $router.push(item.path)" class="header-item-content">
<img :src="item.image" :alt="item.text" class="header-image" />
<div class="header-text">{{ item.text }}</div>
</div>
</router-link>
</div>
<!-- 修改后的操作区域 - 三个部分等宽 -->
<div class="action-area">
<div class="action-grid">
<router-link to="/mymatching?autoStart=true" class="action-main">
<div class="matching-text">
<div>获取</div>
<div>融豆</div>
</div>
</router-link>
<div class="action-stack">
<div class="action-sub-item top">
<div class="action-icon">💎</div>
<div class="action-text">当前积分: {{ userPoints }}</div>
</div>
<router-link to="/myshop" class="action-sub-item bottom">
<div class="action-icon">🛒</div>
<div class="action-text">商城</div>
</router-link>
</div>
</div>
</div>
<!-- 修改后的操作区域 - 三个图片并排 -->
<div class="action-area">
<div class="action-grid-horizontal">
<router-link to="/matching?quantitative=true" class="action-item">
<img src="/imgs/mainpage/dinglianghuoqu1.png" alt="定量获取" class="action-image1" />
</router-link>
<router-link to="/matching" class="action-item">
<img src="/imgs/mainpage/huoqurongdou1.png" alt="获取融豆" class="action-image2" />
</router-link>
<router-link to="/myshop" class="action-item">
<img src="/imgs/mainpage/jifenyue1.png" alt="商城" class="action-image3" />
</router-link>
</div>
</div>
<div class="tips">点击获得融豆可兑换商城好物</div>
<!-- 热门资讯 -->
<div class="hot-news">
<div class="news-title">热门资讯</div>
@@ -73,18 +67,67 @@
</div>
</div>
</div>
<!-- 欢迎弹窗 -->
<el-dialog
v-model="showWelcomeDialog"
width="90%"
:style="{ height: '515px' }"
:show-close="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
@close="closeWelcomeDialog"
center
class="welcome-dialog"
:lock-scroll="false"
>
<div class="welcome-content">
<div class="welcome-icon">🎉</div>
<h3>融汇通更新</h3>
<div class="welcome-features">
<div class="announcements-container" v-if="displayAnnouncements.length > 0">
<div
class="announcement-item"
v-for="announcement in displayAnnouncements"
:key="announcement.id"
>
<div class="announcement-header">
<h4 class="announcement-title">{{ announcement.title }}</h4>
<span class="announcement-priority" :class="announcement.priority">{{ announcement.priority }}</span>
</div>
<div class="announcement-content">{{ announcement.content }}</div>
<div class="announcement-meta">
<span class="announcement-time">{{ formatDate(announcement.created_at) }}</span>
<span class="announcement-author">发布者: {{ announcement.creator_name }}</span>
</div>
</div>
</div>
<div v-else class="no-announcements">
<span>{{ isSystemAnnouncementClick ? '暂无公告信息' : '暂无未读更新信息' }}</span>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeWelcomeDialog">已读</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'swiper/vue';
import { Autoplay, Pagination } from 'swiper';
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ElMessage, ElDialog, ElButton } from 'element-plus';
import { transferAPI } from '../utils/api';
import { useUserStore } from '../stores/user';
import { useRouter } from 'vue-router';
import 'swiper/css';
import 'swiper/css/autoplay';
import 'swiper/css/pagination';
import api from '@/utils/api'
export default {
components: {
@@ -92,40 +135,51 @@ export default {
SwiperSlide,
},
setup() {
// 用户store
const userStore = useUserStore();
const router = useRouter();
// 响应式数据
const userPoints = ref(0);
const showWelcomeDialog = ref(false);
const updateNotice = ref('');
const announcements = ref([]);
// 计算属性 - 获取用户名
const userName = computed(() => {
return userStore.user?.username || '用户';
});
const carouselItems = ref([
{ image: '/imgs/top/1.jpg', title: '限时优惠活动' },
{ image: '/imgs/top/2.jpg', title: '新用户专享' },
{ image: '/imgs/top/3.jpg', title: '积分兑换' },
{ image: '/imgs/mainpage/lunbotu/1.png', title: '招商中', path: '/' },
{ image: '/imgs/mainpage/lunbotu/2.png', title: '代理合作', path: '/agent/login' },
{ image: '/imgs/mainpage/lunbotu/3.png', title: '积分兑换', path: '/myshop' },
]);
const headerItems = ref([
{
image: '/imgs/mainpage/交易记录.png',
image: '/imgs/mainpage/jiaoyijilu.png',
text: '交易记录',
path: '/transfers',
},
{
image: '/imgs/mainpage/订单查询.png',
image: '/imgs/mainpage/dingdanchaxun.png',
text: '订单查询',
path: '/points-history',
path: '/orders',
},
{
image: '/imgs/mainpage/客服中心.png',
image: '/imgs/mainpage/kefuzhongxin.png',
text: '客服中心',
path: '/customerservice',
},
{
image: '/imgs/mainpage/系统公告.png',
image: '/imgs/mainpage/xitonggonggao.png',
text: '系统公告',
path: '#',
},
]);
const newsItems = ref([
'最新活动:双十一特惠',
'最新活动:商城特惠',
'商城新品上架',
'积分兑换规则更新',
'VIP会员特权升级',
]);
// 获取用户积分
@@ -144,26 +198,160 @@ export default {
}
};
// 计算未读公告
const unreadAnnouncements = computed(() => {
return announcements.value.filter(announcement => !announcement.is_read);
});
// 标记是否通过系统公告按钮打开对话框
const isSystemAnnouncementClick = ref(false);
// 计算要显示的公告列表
const displayAnnouncements = computed(() => {
if (isSystemAnnouncementClick.value) {
// 如果是通过系统公告按钮打开,显示所有公告并按创建时间倒序排列(最新的在前)
return [...announcements.value].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
} else {
// 否则只显示未读公告
return unreadAnnouncements.value;
}
});
// 计算是否应该显示弹窗
const shouldShowDialog = computed(() => {
return unreadAnnouncements.value.length > 0;
});
const getUpdateNotice = async () => {
try {
const response = await api.get('/announcements');
console.log('获取更新信息',response);
if (response.data.success && response.data.data.announcements) {
announcements.value = response.data.data.announcements;
// 设置第一个公告的标题作为默认显示
if (announcements.value.length > 0) {
updateNotice.value = announcements.value[0].title;
}
}
} catch (error) {
console.error('获取更新信息失败', error);
ElMessage.error('获取更新信息失败,请稍后重试');
}
}
// 定时刷新积分
let refreshInterval;
onMounted(() => {
onMounted(async () => {
getUserPoints();
await getUpdateNotice();
// 每5分钟刷新一次积分可根据需求调整时间
refreshInterval = setInterval(getUserPoints, 5 * 60 * 1000);
// 根据是否有未读公告决定是否显示欢迎弹窗
setTimeout(() => {
if (shouldShowDialog.value) {
showWelcomeDialog.value = true;
}
}, 500); // 延迟500ms显示让页面先加载完成
});
onUnmounted(() => {
clearInterval(refreshInterval);
});
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// 标记公告为已读
const markAnnouncementAsRead = async (announcementId) => {
try {
await api.post(`/announcements/${announcementId}/read`);
console.log(`公告 ${announcementId} 已标记为已读`);
} catch (error) {
console.error('标记公告已读失败:', error);
ElMessage.error('标记公告已读失败,请稍后重试');
}
};
// 标记所有未读公告为已读的标志
const isMarkingAsRead = ref(false);
// 标记所有未读公告为已读
const markAllAnnouncementsAsRead = async () => {
if (isMarkingAsRead.value) return; // 防止重复调用
isMarkingAsRead.value = true;
try {
for (const announcement of unreadAnnouncements.value) {
await markAnnouncementAsRead(announcement.id);
}
} finally {
isMarkingAsRead.value = false;
}
};
// 关闭欢迎弹窗并标记所有未读公告为已读
const closeWelcomeDialog = async () => {
// 只有在非系统公告点击时才标记为已读
if (!isSystemAnnouncementClick.value) {
await markAllAnnouncementsAsRead();
}
showWelcomeDialog.value = false;
isSystemAnnouncementClick.value = false; // 重置标志
};
// 点击已读按钮
const handleReadClick = () => {
showWelcomeDialog.value = false;
};
// 处理系统公告点击事件
const handleSystemAnnouncementClick = () => {
// console.log('触发')
isSystemAnnouncementClick.value = true;
showWelcomeDialog.value = true; // 无条件显示对话框
};
// 处理轮播图点击事件
const handleCarouselClick = (item) => {
if (item.path && item.path !== '#') {
// 使用Vue Router进行页面跳转
router.push(item.path);
}
};
return {
modules: [Autoplay, Pagination],
userPoints,
userName,
carouselItems,
headerItems,
newsItems,
showWelcomeDialog,
updateNotice,
announcements,
unreadAnnouncements,
shouldShowDialog,
formatDate,
getUserPoints, // 如果需要外部调用可以暴露
closeWelcomeDialog,
handleReadClick,
handleSystemAnnouncementClick,
handleCarouselClick,
displayAnnouncements,
isSystemAnnouncementClick,
};
},
};
@@ -193,9 +381,12 @@ export default {
width: 100%;
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
background-image: url('/imgs/mainpage/jingganglan-1.png'), url('/imgs/mainpage/jingganglan-2.png');
background-size: 100% 10%, 100% 100%;
background-position: center 28%, center 100%;
background-repeat: no-repeat, no-repeat;
}
.main-content {
@@ -204,9 +395,16 @@ export default {
flex-direction: column;
padding: 16px;
gap: 16px;
max-width: 375px;
margin: 0 auto;
width: 100%;
max-width: 100%;
margin: 0 auto;
}
/* 欢迎文字 */
.welcome {
font-size: 15px;
font-weight: 500;
color: white;
}
.spacer {
@@ -215,7 +413,8 @@ export default {
/* 轮播图样式 */
.carousel {
width: 343px;
width: 100%;
max-width: 343px;
height: 148px;
border-radius: 12px;
overflow: hidden;
@@ -223,6 +422,16 @@ export default {
position: relative;
margin: 0 auto;
opacity: 1;
z-index: 1; /* 设置较低的z-index让背景图显示在上面 */
}
.carousel-slide {
cursor: pointer;
transition: var(--transition);
}
.carousel-slide:hover {
transform: scale(1.02);
}
.carousel-img {
@@ -233,7 +442,7 @@ export default {
.carousel-title {
position: absolute;
bottom: 0;
bottom: 12px;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
@@ -246,7 +455,8 @@ export default {
.header {
display: flex;
justify-content: space-between; /* 保持元素均匀分布 */
width: 343px;
width: 100%;
max-width: 343px;
height: 50px;
padding: 0; /* 移除内边距 */
background-color: transparent;
@@ -286,95 +496,86 @@ export default {
/* 操作区域样式 */
.action-area {
width: 343px;
width: 100%;
max-width: 343px;
margin: 0 auto;
}
.action-grid {
.action-grid-horizontal {
display: flex;
gap: 20px; /* 修改为20px间距 */
height: 204px;
justify-content: space-between;
align-items: center;
gap: 10px;
width: 100%;
overflow:hidden;
}
.action-main {
width: 159px;
height: 204px;
.action-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
color: white;
font-weight: 600;
font-size: 18px;
border-radius: 12px;
box-shadow: var(--box-shadow);
align-items: center;
text-decoration: none;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
padding: 63px 47px;
opacity: 1;
}
.action-main:hover {
transform: scale(1.02);
box-shadow: 0 8px 15px rgba(67, 97, 238, 0.3);
.action-item:hover {
transform: scale(1.05);
}
.action-stack {
display: flex;
flex-direction: column;
gap: 20px; /* 修改为20px间距 */
width: 165px;
.action-image1 {
width: 100%;
height: auto;
max-width: 180px;
object-fit: contain;
border-radius: 8px;
}
.action-sub-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
border-radius: 12px;
box-shadow: var(--box-shadow);
cursor: pointer;
transition: var(--transition);
text-decoration: none;
opacity: 1;
.action-image2 {
width: 120%;
height: auto;
max-width: 180px;
object-fit: contain;
border-radius: 8px;
z-index: 1;
margin-left: -5px;
}
.action-sub-item.top {
width: 165px;
height: 92px;
color: var(--primary-color);
.action-image3 {
width: 100%;
height: auto;
max-width: 180px;
object-fit: contain;
}
.action-sub-item.bottom {
width: 165px;
height: 92px;
color: var(--success-color);
}
.action-sub-item:hover {
transform: translateY(-2px);
}
.action-icon {
font-size: 20px;
margin-bottom: 4px;
}
.action-text {
font-size: 12px;
font-weight: 500;
.tips {
width: 100%;
max-width: 343px;
margin: 0 auto;
padding: 12px 20px;
text-align: center;
font-size: 14px;
font-weight: 500;
color: var(--primary-color);
background-color: rgba(67, 97, 238, 0.1);
border: 1px solid var(--primary-color);
border-radius: 25px;
box-shadow: 0 2px 8px rgba(67, 97, 238, 0.15);
color: #22a7ff;
}
/* 热门资讯 */
.hot-news {
width: 343px;
width: 100%;
max-width: 343px;
padding: 16px;
background-color: white;
border-radius: 12px;
box-shadow: var(--box-shadow);
border: 1px solid #e0e0e0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin: 0 auto;
opacity: 1;
}
@@ -412,4 +613,271 @@ export default {
:deep(.swiper-pagination-bullet-active) {
background: var(--primary-color) !important;
}
/* 欢迎弹窗样式 */
:deep(.welcome-dialog) {
background: transparent !important;
}
:deep(.welcome-dialog .el-overlay) {
background: transparent !important;
}
:deep(.welcome-dialog .el-dialog) {
background: transparent !important;
margin-top: 5vh !important;
box-shadow: none !important;
}
:deep(.welcome-dialog .el-dialog__header) {
background: transparent !important;
}
:deep(.welcome-dialog .el-dialog__headerbtn) {
top: 25px !important;
}
:deep(.welcome-dialog .el-dialog__body) {
background-image: url('/imgs/mainpage/gengxintishi.png');
background-size: 100% 100%;
background-position: center bottom;
background-repeat: no-repeat;
background-color: transparent !important;
position: relative;
/* 调整背景图透明度值范围0-10为完全透明1为完全不透明 */
opacity: 1;
padding-bottom: 60px;
}
:deep(.welcome-dialog .el-dialog__body::before) {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* 完全透明的遮罩层 */
background: transparent;
z-index: 1;
}
.welcome-content {
text-align: center;
padding: 20px 20px 0 20px;
position: relative;
z-index: 2;
height: calc(100% - 60px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.welcome-icon {
font-size: 48px;
margin-bottom: 16px;
animation: bounce 2s infinite;
}
.welcome-content h3 {
color: #ff7b00;
font-size: 26px;
font-weight: 600;
margin: 0 0 20px 0;
}
.welcome-content p {
color: #666;
font-size: 16px;
margin: 0 0 24px 0;
line-height: 1.5;
}
.welcome-features {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 30px;
}
.announcements-container {
max-height: 200px;
overflow-y: auto;
padding-right: 8px;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) #f1f1f1;
}
.announcements-container::-webkit-scrollbar {
width: 6px;
}
.announcements-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.announcements-container::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 3px;
}
.announcements-container::-webkit-scrollbar-thumb:hover {
background: var(--secondary-color);
}
.announcement-item {
padding: 12px 16px;
margin-bottom: 12px;
border-radius: 8px;
transition: var(--transition);
}
.announcement-item:hover {
background: rgba(255, 255, 255, 0.95);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.announcement-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.announcement-title {
font-size: 14px;
font-weight: 600;
color: var(--dark-color);
margin: 0;
}
.announcement-priority {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
text-transform: uppercase;
}
.announcement-priority.high {
background: #ffebee;
color: #c62828;
}
.announcement-priority.medium {
background: #fff3e0;
color: #ef6c00;
}
.announcement-priority.low {
background: #e8f5e8;
color: #2e7d32;
}
.announcement-content {
font-size: 13px;
color: #555;
line-height: 1.4;
margin-bottom: 8px;
}
.announcement-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: #888;
}
.announcement-time,
.announcement-author {
font-size: 11px;
color: #888;
}
.no-announcements {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
font-style: italic;
}
:deep(.welcome-dialog .el-dialog__footer) {
padding: 15px 20px;
border-top: none;
background: transparent;
position: absolute;
bottom: 10;
left: 0;
right: 0;
z-index: 3;
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 12px;
padding: 0;
position: absolute;
bottom: 10;
left: 0;
right: 0;
background: transparent;
z-index: 3;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
/* 弹窗响应式样式 */
@media (max-width: 480px) {
.welcome-content {
padding: 30px 15px 0 15px;
height: calc(100% - 70px);
}
.welcome-icon {
font-size: 40px;
}
.welcome-content h3 {
font-size: 22px;
margin: 0 0 15px 0;
}
.welcome-content p {
font-size: 14px;
}
.welcome-features {
gap: 15px;
margin-top: 25px;
}
.feature-item {
padding: 10px 12px;
}
:deep(.welcome-dialog .el-dialog__footer) {
padding: 10px 15px;
}
:deep(.welcome-dialog .el-dialog__body) {
padding-bottom: 50px;
}
}
</style>

View File

@@ -11,7 +11,7 @@
<div class="card-header">
<h3>融豆匹配</h3>
<div class="toggle-container">
<span class="toggle-label">开启大额匹配</span>
<span class="toggle-label">开启定量获取</span>
<label class="apple-switch">
<input
type="checkbox"
@@ -33,7 +33,7 @@
</div>
<div class="info-item">
<span class="label">分配笔数:</span>
<span class="value">3</span>
<span class="value">3-4</span>
</div>
<div class="info-item">
<span class="label">单笔范围:</span>
@@ -41,7 +41,7 @@
</div>
</div>
<!-- 大额匹配信息 -->
<!-- 定量获取信息 -->
<div v-if="matchingType === 'large'" class="matching-info">
<div class="info-item">
<span class="label">购买数量:</span>
@@ -49,12 +49,12 @@
<el-input
v-model="customAmount"
type="number"
:min="5000"
:min="3000"
:max="50000"
step="100"
placeholder="请输入5000-50000之间的金额"
placeholder="请输入3000-50000之间的融豆"
>
<template #prepend><img src="/imgs/profile/融豆.png" alt="融豆" class="bean-image"></template>
<template #prepend><img src='/imgs/profile/rongdou.png' alt="融豆" class="bean-image"></template>
</el-input>
</div>
</div>
@@ -62,10 +62,6 @@
<span class="label">分配规则:</span>
<span class="value">{{ getLargeMatchingRule() }}</span>
</div>
<div class="info-item">
<span class="label">预计笔数:</span>
<span class="value">{{ getLargeMatchingCount() }}</span>
</div>
</div>
<button
@@ -78,14 +74,14 @@
<!-- 小额匹配提示 -->
<div v-if="matchingType === 'small'" class="tips">
<p> 系统将为您匹配3笔订单总获取5000融豆</p>
<p> 系统将为您匹配3-4笔订单总获取5000融豆</p>
<p> 优先匹配拥有融豆并自愿出售的用户</p>
<!-- <p> 每笔金额随机分配确保货款循环</p> -->
<!-- <p> 每笔融豆随机分配确保货款循环</p> -->
</div>
<!-- 大额匹配提示 -->
<!-- 定量获取提示 -->
<div v-if="matchingType === 'large'" class="tips">
<p> 融豆数量5000-50000</p>
<p> 融豆数量3000-50000</p>
<p> 15000以下分成多笔随机融豆</p>
<p> 15000以上随机分拆每笔100-10000</p>
<p> 优先匹配拥有融豆并自愿出售的用户</p>
@@ -111,8 +107,8 @@
</div>
<div class="allocation-details">
<p>转账给: <strong>{{ allocation.to_user_real_name }}</strong></p>
<p>金额: <strong class="amount">¥{{ allocation.amount }}</strong></p>
<p>金额: ¥{{ allocation.total_amount }}</p>
<p>融豆: <strong class="amount">¥{{ allocation.amount }}</strong></p>
<p>融豆: ¥{{ allocation.total_amount }}</p>
<p class="deadline-info">
转账时效:
<span :class="['time-left', allocation.time_status]">
@@ -156,7 +152,7 @@
<span :class="['status', order.status]">{{ getStatusText(order.status) }}</span>
</div>
<div class="order-info">
<p>金额: ¥{{ order.amount }}</p>
<p>融豆: ¥{{ order.amount }}</p>
<p>发起人: {{ order.initiator_real_name }}</p>
<p v-if="!order.is_system_reverse">轮次: {{ order.cycle_count + 1 }}/{{ order.max_cycles }}</p>
<p v-if="order.is_system_reverse" class="system-note">系统自动发起向负余额用户补充货款</p>
@@ -180,7 +176,7 @@
<div class="modal-body" v-if="selectedOrder">
<div class="order-summary">
<p><strong>状态:</strong> {{ getStatusText(selectedOrder.order.status) }}</p>
<p><strong>金额:</strong> ¥{{ selectedOrder.order.amount }}</p>
<p><strong>融豆:</strong> ¥{{ selectedOrder.order.amount }}</p>
<p><strong>发起人:</strong> {{ selectedOrder.order.initiator_real_name }}</p>
<p><strong>轮次:</strong> {{ selectedOrder.order.cycle_count + 1 }}/{{ selectedOrder.order.max_cycles }}</p>
</div>
@@ -240,7 +236,7 @@
<div class="transfer-info">
<h4>转账信息</h4>
<p><strong>收款人:</strong> {{ transferDialog.toUser.to_user_real_name }}</p>
<p><strong>转账金额:</strong> ¥{{ transferDialog.amount }}</p>
<p><strong>转账融豆:</strong> ¥{{ transferDialog.amount }}</p>
</div>
<!-- 收款码展示 -->
@@ -306,7 +302,7 @@
<div class="transfer-form">
<h4>转账确认</h4>
<el-form label-width="100px">
<el-form-item label="转账金额">
<el-form-item label="转账融豆">
<el-input
v-model="transferDialog.actualAmount"
readonly
@@ -349,12 +345,12 @@
:src="getImageUrl(transferDialog.voucher)"
:preview-src-list="[getImageUrl(transferDialog.voucher)]"
alt="转账凭证"
fit="cover"
fit="contain"
:lazy="true"
>
<template #placeholder>
<div class="image-slot">
<el-icon class="is-loading"><Loading /></el-icon>
<el-icon class="image-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</template>
@@ -393,15 +389,15 @@
</template>
</el-dialog>
<!-- 大额匹配确认弹窗 -->
<!-- 定量获取确认弹窗 -->
<el-dialog
v-model="showLargeMatchingConfirm"
title="开启大额匹配"
title="开启定量获取"
width="90%"
:style="{ maxWidth: '500px' }"
>
<div class="confirm-dialog-content">
<p>确认要开启大额匹配</p>
<p>确认要开启定量获取</p>
</div>
<template #footer>
@@ -439,7 +435,7 @@ export default {
showOrderDetail: false,
selectedOrder: null,
matchingType: 'small', // 匹配类型small(小额) 或 large(大额)
customAmount: '', // 大额匹配自定义金额
customAmount: '', // 定量获取自定义融豆
transferDialog: {
visible: false,
allocationId: null,
@@ -462,13 +458,26 @@ export default {
tempMatchingType: 'small' // 临时存储切换前的类型
}
},
created() {
// 处理定量获取参数
if (this.$route.query.quantitative === 'true') {
this.matchingType = 'large';
this.tempMatchingType = 'large'; // 同步临时状态
}
// 在处理完所有参数后清除query参数
if (this.$route.query.quantitative) {
this.$router.replace({ ...this.$route, query: {} });
}
},
async mounted() {
await this.loadData()
},
methods: {
handleMatchingTypeChange() {
if (this.tempMatchingType === 'large') {
// 如果要切换到大额匹配,显示确认对话框
// 如果要切换到定量获取,显示确认对话框
this.showLargeMatchingConfirm = true
} else {
// 直接切换到小额匹配
@@ -479,7 +488,7 @@ export default {
confirmLargeMatching() {
this.matchingType = 'large'
this.showLargeMatchingConfirm = false
this.$message.success('已开启大额匹配模式')
this.$message.success('已开启定量获取模式')
},
cancelLargeMatching() {
@@ -537,15 +546,13 @@ export default {
let confirmMessage = ''
if (this.matchingType === 'small') {
confirmMessage = '确定要开始小额匹配吗?\n\n匹配成功后将生成3笔转账分配。'
confirmMessage = '确定要开始小额匹配吗?\n\n匹配成功后将生成3-4笔转账分配。'
} else {
if (!this.isValidCustomAmount) {
this.$message.error('请输入有效的匹配金额5000-50000元')
this.$message.error('请输入有效的匹配融豆3000-50000元')
return
}
const amount = parseFloat(this.customAmount)
const count = this.getLargeMatchingCount()
confirmMessage = `确定要开始大额匹配吗?\n\n匹配金额${amount}\n将生成${count}笔转账分配`
}
// 二次确认对话框
@@ -563,7 +570,7 @@ export default {
matchingType: this.matchingType
}
// 如果是大额匹配,添加自定义金额
// 如果是定量获取,添加自定义融豆
if (this.matchingType === 'large') {
requestData.customAmount = parseFloat(this.customAmount)
}
@@ -572,7 +579,7 @@ export default {
const successMessage = this.matchingType === 'small'
? '小额匹配成功已为您生成3笔转账分配'
: `大额匹配成功!已为您生成${this.getLargeMatchingCount()}笔转账分配`
: `定量获取成功!已为您生成笔转账分配`
this.$message.success(successMessage)
await this.loadData()
@@ -595,8 +602,6 @@ export default {
}).then(() => {
this.$router.push('/myprofile')
}).catch(() => {})
} else {
this.$message.error(errorMessage)
}
} finally {
this.creating = false
@@ -608,7 +613,7 @@ export default {
/**
* 确认分配并创建转账记录
* @param {number} allocationId - 分配ID
* @param {number} expectedAmount - 预期转账金额
* @param {number} expectedAmount - 预期转账融豆
*/
async confirmAllocation(allocationId, expectedAmount) {
try {
@@ -709,9 +714,9 @@ export default {
},
/**
* 格式化金额显示,确保数字安全
* @param {number|string|null|undefined} amount - 金额
* @returns {string} 格式化后的金额字符串
* 格式化融豆显示,确保数字安全
* @param {number|string|null|undefined} amount - 融豆
* @returns {string} 格式化后的融豆字符串
*/
formatAmount(amount) {
const num = parseFloat(amount)
@@ -719,41 +724,28 @@ export default {
},
/**
* 获取大额匹配的分配规则描述
* 获取定量获取的分配规则描述
* @returns {string} 规则描述
*/
getLargeMatchingRule() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0) {
return '请输入金额'
} else if (amount < 5000) {
return '金额不能少于5000元'
return '请输入融豆'
} else if (amount < 3000) {
return '融豆不能少于3000元'
} else if (amount > 50000) {
return '金额不能超过50000元'
return '融豆不能超过50000元'
} else if (amount <= 15000) {
return '分成多笔随机金额'
return '分成多笔随机融豆'
} else {
return '随机分拆每笔100-10000元'
}
},
/**
* 获取大额匹配的预计笔数
* 获取定量获取的预计笔数
* @returns {string} 预计笔数描述
*/
getLargeMatchingCount() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0 || amount < 5000 || amount > 50000) {
return '0'
} else if (amount <= 15000) {
return '3'
} else {
// 15000以上随机分拆估算笔数范围
const minCount = Math.ceil(amount / 10000) // 按最大单笔10000计算最少笔数
const maxCount = Math.floor(amount / 100) // 按最小单笔100计算最多笔数
return `${minCount}-${Math.min(maxCount, 10)}` // 限制最大显示笔数为10
}
},
/**
* 关闭转账弹窗
@@ -903,12 +895,18 @@ export default {
const actualAmount = parseFloat(this.transferDialog.actualAmount)
// 处理voucher去掉开头的'https://minio.zrbjr.com'
let processedVoucher = this.transferDialog.voucher
if (processedVoucher.startsWith('https://minio.zrbjr.com')) {
processedVoucher = processedVoucher.replace('https://minio.zrbjr.com', '')
}
this.processing = true
try {
await api.post(`/matching/confirm-allocation/${this.transferDialog.allocationId}`, {
transferAmount: actualAmount,
description: this.transferDialog.description,
voucher: this.transferDialog.voucher
voucher: processedVoucher
})
this.$message.success('转账凭证已提交,转账记录已创建')
this.closeTransferDialog()
@@ -925,12 +923,12 @@ export default {
computed: {
/**
* 验证自定义金额是否有效
* @returns {boolean} 金额是否有效
* 验证自定义融豆是否有效
* @returns {boolean} 融豆是否有效
*/
isValidCustomAmount() {
const amount = parseFloat(this.customAmount)
return !isNaN(amount) && amount >= 5000 && amount <= 50000
return !isNaN(amount) && amount >= 3000 && amount <= 50000
},
/**
@@ -946,6 +944,12 @@ export default {
uploadHeaders() {
return getUploadConfig().headers
}
},
watch: {
matchingType(newVal, oldVal) {
console.log('更改匹配模式', oldVal, 'to', newVal);
}
}
}
</script>
@@ -954,7 +958,7 @@ export default {
.matching-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
padding: 20px 10px;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
@@ -1449,7 +1453,7 @@ export default {
transform: translateY(0);
}
/* 自定义金额输入框样式 */
/* 自定义融豆输入框样式 */
.custom-amount-input {
flex: 1;
margin-left: 10px;
@@ -1841,7 +1845,7 @@ export default {
text-align: center;
}
.is-loading {
.image-loading {
animation: rotating 2s linear infinite;
}

View File

@@ -6,10 +6,6 @@
<h2>用户登录</h2>
<p>欢迎来到炬融圈</p>
</div>
<div class="image">
<img src="/imgs/login.png" alt="炬融圈">
</div>
<el-form
ref="loginFormRef"
@@ -21,7 +17,7 @@
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名或邮箱"
placeholder="请输入手机号"
size="large"
:prefix-icon="User"
clearable
@@ -159,7 +155,7 @@ const rememberMe = ref(false)
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' }
],
password: [
@@ -181,7 +177,7 @@ const handleLogin = async () => {
const valid = await loginFormRef.value.validate()
if (!valid) return
// // 验证验证码
// 验证验证码
// const captchaValid = await captchaRef.value.verifyCaptcha(loginForm.captcha)
// if (!captchaValid) {
// loginForm.captcha = ''
@@ -199,12 +195,40 @@ const handleLogin = async () => {
captchaText: captchaInfo.captchaText
}
const result = await userStore.login(loginData)
if (result.success) {
// 登录成功,跳转到目标页面或转账管理
const redirectPath = route.query.redirect || '/mainpage'
router.push(redirectPath)
console.log('开始调用登录接口');
try {
const result = await userStore.login(loginData)
console.log('登录接口调用完成');
console.log(result,'result');
if (result.success) {
// 登录成功,跳转到目标页面或转账管理
const redirectPath = route.query.redirect || '/mainpage'
router.push(redirectPath)
} else if (result.needPayment) {
// 用户需要支付激活,直接跳转到支付页面
ElMessage.info('账户尚未激活,正在跳转到支付页面...')
router.push({
path: '/payment',
query: {
userId: result.userId,
from: 'login'
}
})
}
} catch (loginError) {
console.error('登录调用异常:', loginError)
// 如果是支付相关的错误,也要处理
if (loginError.needPayment) {
ElMessage.info('账户尚未激活,正在跳转到支付页面...')
router.push({
path: '/payment',
query: {
userId: loginError.userId,
from: 'login'
}
})
}
}
} catch (error) {
console.error('登录失败:', error)
@@ -216,21 +240,6 @@ const handleLogin = async () => {
}
}
// 快速登录(演示用)
const quickLogin = async (type) => {
if (type === 'admin') {
loginForm.username = 'admin'
loginForm.password = 'admin123'
} else {
loginForm.username = 'user'
loginForm.password = 'user123'
}
// 清空验证码,让用户手动输入
loginForm.captcha = ''
ElMessage.info('请输入验证码后登录')
}
// 忘记密码
const showForgotPassword = () => {
ElMessageBox.alert(
@@ -333,33 +342,6 @@ const handleRememberMe = () => {
font-size: 14px;
}
/* 图片容器样式 */
.image {
width: 375px; /* 固定宽度 */
height: 287px; /* 固定高度 */
margin: 0 auto 20px; /* 水平居中,底部留出间距 */
overflow: hidden; /* 隐藏超出容器的部分 */
border-radius: 8px; /* 可选:添加圆角增强视觉效果 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 可选:添加轻微阴影 */
}
/* 图片填充样式 */
.image img {
width: 100%; /* 宽度充满容器 */
height: 100%; /* 高度充满容器 */
object-fit: contain;
display: block; /* 去除图片底部默认空白 */
}
/* 响应式适配(小屏设备自动缩放) */
@media (max-width: 375px) {
.image {
width: 100%; /* 在小于375px的屏幕上宽度自适应 */
height: auto; /* 高度按比例自动计算 */
aspect-ratio: 375 / 287; /* 保持原比例 */
}
}
.login-form {
margin-bottom: 20px;
}

File diff suppressed because it is too large Load Diff

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