Compare commits
290 Commits
5508c72442
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07b49d10c7 | ||
|
|
b79acb6471 | ||
|
|
0cb49410a3 | ||
|
|
e3abff037b | ||
|
|
4a59e428a6 | ||
|
|
5ad9007fea | ||
|
|
58e48e3ebf | ||
|
|
0810a2b46a | ||
|
|
2f296b5fb5 | ||
|
|
1b41508013 | ||
|
|
e63ab42c41 | ||
|
|
1ddf792076 | ||
|
|
da906d52c0 | ||
|
|
bc33717921 | ||
|
|
2b9e1aef3f | ||
|
|
60c921ea28 | ||
|
|
9f18f13607 | ||
|
|
27c70ac567 | ||
|
|
4fba24438c | ||
|
|
bf092e5f69 | ||
|
|
d680da2d83 | ||
|
|
85ec563242 | ||
|
|
9794bec2ae | ||
|
|
7a39dd32cf | ||
|
|
b01c71fdb1 | ||
|
|
829aa0cd4e | ||
|
|
9d089497ce | ||
|
|
4a8618f8e5 | ||
|
|
e334b0fb51 | ||
|
|
edfe0ca0a1 | ||
|
|
444c7954fd | ||
|
|
9fb2d6a914 | ||
|
|
6f3b0a2467 | ||
|
|
df0bcf9d27 | ||
|
|
4c2803d911 | ||
|
|
41078c63e6 | ||
|
|
b4d18c6cfb | ||
|
|
6af927d8be | ||
|
|
9cea8b24b8 | ||
|
|
6ff8f18604 | ||
|
|
53edda8a02 | ||
|
|
5118598d83 | ||
|
|
d8e71d9bf2 | ||
|
|
300293b68b | ||
|
|
ada7aaf1f5 | ||
|
|
fc237c9bfd | ||
|
|
ad58ef9c33 | ||
|
|
ff3537ca35 | ||
|
|
f194311d2a | ||
|
|
1639a650cf | ||
|
|
dc66a9cf53 | ||
|
|
1a585d8469 | ||
|
|
4b3d14f699 | ||
|
|
c3cc665948 | ||
|
|
37b30f4ddb | ||
|
|
0053741c05 | ||
|
|
2ed4625bfd | ||
|
|
f2867bfed4 | ||
|
|
2cc538120f | ||
|
|
f03e82acb5 | ||
|
|
c7bad0e5e5 | ||
|
|
3cec899df2 | ||
|
|
1f4a6b394f | ||
|
|
04dd5396ac | ||
|
|
09cb3c2862 | ||
|
|
27f40074e3 | ||
|
|
5ff08414bc | ||
|
|
c3d13db31b | ||
|
|
b215f34aa8 | ||
|
|
dc01036abf | ||
|
|
cc09f016d2 | ||
|
|
73b7bad859 | ||
|
|
979a7021b1 | ||
|
|
f6d681a698 | ||
|
|
9dc148400c | ||
|
|
3779523047 | ||
|
|
acfc5e60f4 | ||
|
|
7871acf214 | ||
|
|
70ce063aba | ||
|
|
f79535622a | ||
|
|
b38633ff49 | ||
|
|
8d2433e432 | ||
|
|
0d03604888 | ||
|
|
986cdd6fd9 | ||
|
|
af0000107f | ||
|
|
5367eb7834 | ||
|
|
9b2473334b | ||
|
|
5dc38831eb | ||
|
|
a3291f7a31 | ||
|
|
1a4e56bfed | ||
|
|
5d037eaeee | ||
|
|
584a55094e | ||
|
|
fa45ace9a4 | ||
|
|
742bb9d063 | ||
|
|
ebb3497992 | ||
|
|
ae21b33b87 | ||
|
|
429b62a561 | ||
|
|
4417085d75 | ||
|
|
75329ffb84 | ||
|
|
13ae226379 | ||
|
|
1853fb55ac | ||
|
|
a12a17df21 | ||
|
|
86e8fefb97 | ||
|
|
1234ad42a4 | ||
|
|
ec921d313c | ||
|
|
a21f6f77a3 | ||
|
|
485e306082 | ||
|
|
061029fb0c | ||
|
|
d3a9f5039a | ||
|
|
bea1e46deb | ||
|
|
6ef5d644a1 | ||
|
|
73ce628a43 | ||
|
|
00a02866e2 | ||
|
|
1aa6d4ad3a | ||
|
|
38f4664272 | ||
|
|
d25f41d147 | ||
|
|
57d6095555 | ||
|
|
787dc33256 | ||
|
|
a04ba55b7e | ||
|
|
91e48855f4 | ||
|
|
3a40d5f872 | ||
|
|
0b4d241012 | ||
|
|
77685eca9d | ||
|
|
a0b672c969 | ||
|
|
2d342a8ee0 | ||
|
|
74c42ac250 | ||
|
|
a2a9f01e2c | ||
|
|
c3f342dfba | ||
|
|
cc215aec29 | ||
|
|
b71e946bd0 | ||
|
|
1cd54adb06 | ||
|
|
f67002ecfb | ||
|
|
c595b4df0a | ||
|
|
101b3dae54 | ||
|
|
47951ab5ea | ||
|
|
a07452ea8b | ||
|
|
752b3ff1ca | ||
|
|
e99ce93bc1 | ||
|
|
c858ab5ac7 | ||
|
|
9c8048ce7b | ||
|
|
5bc1fcd83d | ||
|
|
c77e95802c | ||
|
|
aa8050543e | ||
|
|
4bc6cbfcc5 | ||
|
|
c9defd4a67 | ||
|
|
e819383722 | ||
|
|
85b3972aa4 | ||
|
|
0d92865041 | ||
|
|
13ec358145 | ||
| 42077dbfd1 | |||
| 083fbba4e8 | |||
| b79d074705 | |||
| 5b607c8031 | |||
| 4f6403d08c | |||
| 0c4937816f | |||
| d02c9ac4cf | |||
| 80def4201c | |||
| 9672e191e1 | |||
| a411e42094 | |||
| 0dde8db6fd | |||
| 92d29fe73f | |||
| 059b5e05fb | |||
| a284047b48 | |||
| a897cdcae9 | |||
| ee831e5931 | |||
| 264bd81307 | |||
| 79b32c887d | |||
| ab9ec7e530 | |||
| d4d4cc614b | |||
| 41ed4f3f34 | |||
| 96cbb5d78f | |||
| a989a000fb | |||
| d852f03e62 | |||
| 9ed12b9248 | |||
| c6fa3d0018 | |||
| e9e5b7ee52 | |||
| 4959b2f34f | |||
| 364276d85b | |||
| df9085baa4 | |||
| 3ea26320cc | |||
| 283cfbbfc8 | |||
| 855d22f448 | |||
| 2fab612906 | |||
| d7a71931a9 | |||
| 1a9edf7e1b | |||
| 6065f3f865 | |||
| 1cbab3f248 | |||
| a68eba7b5f | |||
| e5c7af48a2 | |||
| a16d127512 | |||
| 75f75cb875 | |||
| 78a74a9787 | |||
| ecf8285856 | |||
| fa2e00f9bc | |||
| 93bf30338a | |||
| 2095fc78e6 | |||
| 8b5aa28b8f | |||
| cb1cea512a | |||
| e62a2b3635 | |||
| 94cb24041a | |||
| 83ffdc1f2d | |||
| 5a32bdf544 | |||
| 06edf3d165 | |||
| 60214c1acb | |||
| 7026d1fe1d | |||
| 40d66ae230 | |||
| 10a6ee9e3a | |||
| 5e44b8ccd8 | |||
| 2c2c451c96 | |||
| 136a64d8cb | |||
| 1acc3f7a9a | |||
| edec1cbc08 | |||
| fd8400afe4 | |||
| a5b15e2069 | |||
| aae92dc9d0 | |||
| 98ae13db7a | |||
| 9387722860 | |||
| 153d599373 | |||
| 2096302eca | |||
| 8b6dd7d8a8 | |||
| 2c46da50e3 | |||
| 8c86983ace | |||
| 399b31e8e0 | |||
| 27f92fb3dd | |||
| 8f6f1c9557 | |||
| 12b6a51bcd | |||
|
|
f28f0ad4ab | ||
|
|
9cc0ee358c | ||
|
|
51f77af121 | ||
|
|
114ae26c8f | ||
| f302c9ea69 | |||
|
|
13cf9865dd | ||
|
|
73e132a648 | ||
|
|
9f70082ed6 | ||
| d41cf24764 | |||
| f36dc4f3d3 | |||
| 57f1a7f121 | |||
| 950744adca | |||
|
|
8e32bd2463 | ||
|
|
a14b64c14b | ||
|
|
6d6d460dfc | ||
|
|
d68681faa0 | ||
|
|
391509b766 | ||
|
|
f33e6be27d | ||
|
|
983b773a9c | ||
|
|
e060b235dc | ||
|
|
29df75d58d | ||
|
|
fab8df2f1c | ||
|
|
0c1201baee | ||
|
|
645eac253c | ||
|
|
63d523566d | ||
| 207c68c121 | |||
| 91e0764b09 | |||
| edb3dce4ef | |||
| 653f963a57 | |||
| b514c9004f | |||
| 956c83afe2 | |||
|
|
c873558369 | ||
|
|
3d258dc6aa | ||
|
|
4a30efaa56 | ||
|
|
97514d74cb | ||
|
|
2a87cfbf11 | ||
| 24105e0972 | |||
| dea68663ef | |||
| 2fb0f26b6d | |||
| 7e50e55a30 | |||
| 0255e5d99d | |||
| 0a45d62278 | |||
|
|
acb59b64bd | ||
|
|
ef1a0430d4 | ||
| 6d257dc840 | |||
| 483a7c019c | |||
| 4a7d948061 | |||
| 8a1effedfc | |||
| 3effbc342c | |||
| 2d4f31a116 | |||
|
|
4b4d0cb755 | ||
| eb1c66b3b6 | |||
|
|
c8fdaf9e35 | ||
|
|
ac48ccfc26 | ||
|
|
03f69d8361 | ||
| fd53ce5c98 | |||
| fe9eff8131 | |||
| 459539349f | |||
|
|
af966956a2 | ||
| 3a441bddc2 | |||
| d09f988d7c | |||
| 07c1a23a89 | |||
|
|
b78d53df5d | ||
| fd220a4038 |
@@ -4,11 +4,9 @@ VUE_APP_TITLE = Jarvis
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
ENV = 'development'
|
ENV = 'development'
|
||||||
|
|
||||||
# Jarvis/开发环境
|
|
||||||
VUE_APP_BASE_API = ''
|
|
||||||
|
|
||||||
# 路由懒加载
|
# 路由懒加载
|
||||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||||
|
VUE_APP_BASE_API=/jarvis-api
|
||||||
VUE_APP_BASE_API = 'http://134.175.126.60:30313'
|
# VUE_APP_BASE_API = 'http://127.0.0.1:30313'
|
||||||
port = 8888
|
port = 8888
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ VUE_APP_TITLE = Jarvis
|
|||||||
ENV = 'production'
|
ENV = 'production'
|
||||||
|
|
||||||
# Jarvis/生产环境
|
# Jarvis/生产环境
|
||||||
VUE_APP_BASE_API = ''
|
|
||||||
|
|
||||||
VUE_APP_BASE_API = 'http://134.175.126.60:30313'
|
VUE_APP_BASE_API=/jarvis-api
|
||||||
port = 8888
|
port = 8888
|
||||||
|
|||||||
156
HTTPS部署说明.md
Normal file
156
HTTPS部署说明.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# HTTPS部署配置说明
|
||||||
|
|
||||||
|
## 问题说明
|
||||||
|
|
||||||
|
在HTTPS访问情况下,**不能直接使用 `http://127.0.0.1:30313` 请求后端接口**,因为会出现**混合内容(Mixed Content)**问题:
|
||||||
|
- 浏览器会阻止从HTTPS页面请求HTTP资源
|
||||||
|
- 控制台会报错:`Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...'`
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案一:通过Nginx代理(推荐)
|
||||||
|
|
||||||
|
#### 1. 修改前端环境变量配置
|
||||||
|
|
||||||
|
在生产环境打包时,需要创建 `.env.production` 文件,设置API路径为相对路径:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.production
|
||||||
|
VUE_APP_BASE_API=/dev-api
|
||||||
|
```
|
||||||
|
|
||||||
|
这样前端打包后,所有API请求都会使用 `/dev-api` 作为前缀,例如:
|
||||||
|
- 原请求:`http://127.0.0.1:30313/system/user/list`
|
||||||
|
- 打包后:`/dev-api/system/user/list`
|
||||||
|
- 实际请求:`https://jarvis.van333.cn/dev-api/system/user/list`
|
||||||
|
|
||||||
|
#### 2. 使用提供的nginx配置
|
||||||
|
|
||||||
|
已创建 `nginx-https.conf` 配置文件,包含以下关键配置:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# API代理配置
|
||||||
|
location /dev-api/ {
|
||||||
|
proxy_pass http://127.0.0.1:30313/; # 后端服务地址
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理:**
|
||||||
|
- 用户访问:`https://jarvis.van333.cn/dev-api/system/user/list`
|
||||||
|
- Nginx代理转发到:`http://127.0.0.1:30313/system/user/list`
|
||||||
|
- 后端处理请求,返回结果
|
||||||
|
- Nginx将结果返回给用户(通过HTTPS)
|
||||||
|
|
||||||
|
#### 3. 部署步骤
|
||||||
|
|
||||||
|
1. **创建生产环境配置文件**(如果不存在):
|
||||||
|
```bash
|
||||||
|
# 在项目根目录创建 .env.production
|
||||||
|
echo "VUE_APP_BASE_API=/dev-api" > .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **打包前端项目**:
|
||||||
|
```bash
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **部署到服务器**:
|
||||||
|
```bash
|
||||||
|
# 将 dist 目录内容复制到 /www/sites/jarvis.van333.cn/index/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **更新nginx配置**:
|
||||||
|
```bash
|
||||||
|
# 将 nginx-https.conf 内容替换到你的nginx配置
|
||||||
|
# 或直接使用:sudo cp nginx-https.conf /etc/nginx/sites-available/jarvis.van333.cn
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **重启nginx**:
|
||||||
|
```bash
|
||||||
|
sudo nginx -t # 测试配置
|
||||||
|
sudo nginx -s reload # 重新加载配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:后端也配置HTTPS(不推荐)
|
||||||
|
|
||||||
|
如果后端也配置HTTPS,可以直接使用 `https://127.0.0.1:30313`,但这样会增加配置复杂度。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
### 1. API路径匹配
|
||||||
|
|
||||||
|
确保nginx的 `location` 路径与前端 `VUE_APP_BASE_API` 配置一致:
|
||||||
|
- 如果 `VUE_APP_BASE_API=/dev-api`,则nginx配置 `location /dev-api/`
|
||||||
|
- 如果 `VUE_APP_BASE_API=/api`,则nginx配置 `location /api/`
|
||||||
|
|
||||||
|
### 2. 路径重写
|
||||||
|
|
||||||
|
当前配置中,nginx会将 `/dev-api/xxx` 转发到 `http://127.0.0.1:30313/xxx`(去掉 `/dev-api` 前缀)。
|
||||||
|
|
||||||
|
如果后端需要保留前缀,可以修改为:
|
||||||
|
```nginx
|
||||||
|
location /dev-api/ {
|
||||||
|
proxy_pass http://127.0.0.1:30313/dev-api/; # 保留前缀
|
||||||
|
# ... 其他配置
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. WebSocket支持
|
||||||
|
|
||||||
|
如果后端使用了WebSocket,nginx配置中已包含相关设置:
|
||||||
|
```nginx
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 超时设置
|
||||||
|
|
||||||
|
已配置600秒超时,适合长时间运行的接口。可根据实际需求调整。
|
||||||
|
|
||||||
|
## 验证配置
|
||||||
|
|
||||||
|
部署后,可以通过以下方式验证:
|
||||||
|
|
||||||
|
1. **浏览器开发者工具**:
|
||||||
|
- 打开 `https://jarvis.van333.cn`
|
||||||
|
- F12 打开开发者工具
|
||||||
|
- Network 标签查看API请求
|
||||||
|
- 确认请求URL为 `https://jarvis.van333.cn/dev-api/...`
|
||||||
|
- 确认没有混合内容错误
|
||||||
|
|
||||||
|
2. **测试API请求**:
|
||||||
|
```bash
|
||||||
|
curl -k https://jarvis.van333.cn/dev-api/system/user/list
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **检查nginx日志**:
|
||||||
|
```bash
|
||||||
|
tail -f /www/sites/jarvis.van333.cn/log/access.log
|
||||||
|
tail -f /www/sites/jarvis.van333.cn/log/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么前端请求还是显示 `http://127.0.0.1:30313`?
|
||||||
|
|
||||||
|
A: 检查是否创建了 `.env.production` 文件,并且重新打包了项目。
|
||||||
|
|
||||||
|
### Q: 出现 502 Bad Gateway 错误?
|
||||||
|
|
||||||
|
A:
|
||||||
|
1. 检查后端服务是否运行在 `127.0.0.1:30313`
|
||||||
|
2. 检查nginx配置中的 `proxy_pass` 地址是否正确
|
||||||
|
3. 检查防火墙是否允许nginx访问后端端口
|
||||||
|
|
||||||
|
### Q: 出现 404 Not Found 错误?
|
||||||
|
|
||||||
|
A:
|
||||||
|
1. 检查nginx的 `location /dev-api/` 配置是否正确
|
||||||
|
2. 检查后端接口路径是否正确
|
||||||
|
3. 查看nginx错误日志:`tail -f /www/sites/jarvis.van333.cn/log/error.log`
|
||||||
|
|
||||||
301
doc/操作日志功能-快速上手.md
Normal file
301
doc/操作日志功能-快速上手.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# 操作日志查看功能 - 快速上手指南
|
||||||
|
|
||||||
|
## 🚀 快速部署
|
||||||
|
|
||||||
|
### 1️⃣ 重新编译前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd d:\code\ruoyi-vue
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
或者开发模式:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 重新编译后端(如果还没编译)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd d:\code\RuoYi-Vue-master\ruoyi-java
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ 重启服务
|
||||||
|
|
||||||
|
重启前端和后端服务。
|
||||||
|
|
||||||
|
### 4️⃣ 清除浏览器缓存
|
||||||
|
|
||||||
|
按 `Ctrl + F5` 强制刷新页面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 如何打开日志页面
|
||||||
|
|
||||||
|
### 方法:从配置对话框打开
|
||||||
|
|
||||||
|
1. 打开**订单列表**页面
|
||||||
|
|
||||||
|
2. 点击顶部的 **"H-TF自动写入配置"** 按钮(绿色)
|
||||||
|
|
||||||
|
3. 在弹出的配置对话框底部,找到 **"查看操作日志"** 按钮(蓝色,带文档图标)
|
||||||
|
|
||||||
|
4. 点击后即可查看操作日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 功能演示
|
||||||
|
|
||||||
|
### 界面布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 腾讯文档操作日志 [X] │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 搜索框: [订单号] [操作类型▼] [操作状态▼] [搜索] [重置] │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
|
||||||
|
│ │ 成功 │ │ 跳过 │ │ 失败 │ │ 总计 │ │
|
||||||
|
│ │ 150 │ │ 500 │ │ 10 │ │ 660 │ │
|
||||||
|
│ └───────┘ └───────┘ └───────┘ └───────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 序号 | 操作类型 | 订单号 | 行号 | 物流链接 | 状态 | 时间 │
|
||||||
|
│ ────┼──────────┼────────┼──────┼──────────┼──────┼──────── │
|
||||||
|
│ 1 | 批量同步 | JY123 | 2575 | https... | 成功 | 22:03:30 │
|
||||||
|
│ 2 | 批量同步 | JY124 | 2576 | https... | 跳过 | 22:03:31 │
|
||||||
|
│ 3 | 批量同步 | JY125 | 2577 | https... | 失败 | 22:03:32 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 总计 660 条 [10▼] [<] [1] [2] [3] ... [66] [>] │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [关闭] [刷新] │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 使用场景
|
||||||
|
|
||||||
|
### 场景1:查看今天的同步情况
|
||||||
|
|
||||||
|
1. 打开日志页面
|
||||||
|
2. 查看顶部统计卡片
|
||||||
|
3. 成功数 = 今天成功同步的订单数
|
||||||
|
4. 跳过数 = 今天跳过的订单数(如已有数据)
|
||||||
|
5. 失败数 = 今天失败的订单数
|
||||||
|
|
||||||
|
### 场景2:查找为什么某个订单没有同步
|
||||||
|
|
||||||
|
1. 在"订单号"输入框输入订单号,例如:`JY202511061595`
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看该订单的操作记录:
|
||||||
|
- **成功** ✅ - 已经同步
|
||||||
|
- **跳过** ⚠️ - 被跳过(查看原因)
|
||||||
|
- **失败** ❌ - 同步失败(查看错误信息)
|
||||||
|
- **无记录** - 没有尝试同步该订单
|
||||||
|
|
||||||
|
### 场景3:查看所有失败的订单
|
||||||
|
|
||||||
|
1. 在"操作状态"下拉框选择"失败"
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看所有失败记录的"错误信息"列
|
||||||
|
4. 根据错误信息进行处理:
|
||||||
|
- "未找到订单" → 检查订单是否存在
|
||||||
|
- "订单物流链接为空" → 补充物流信息
|
||||||
|
- "API调用失败" → 检查网络或API
|
||||||
|
|
||||||
|
### 场景4:检查某个订单是否重复推送
|
||||||
|
|
||||||
|
1. 在"订单号"输入框输入订单号
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看记录数量:
|
||||||
|
- 只有1条"成功"记录 ✅ - 正常
|
||||||
|
- 有多条"成功"记录 ❌ - 可能重复推送
|
||||||
|
- 有"跳过"记录 ✅ - 防重机制生效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 快速技巧
|
||||||
|
|
||||||
|
### 技巧1:快速刷新
|
||||||
|
|
||||||
|
点击底部的 **"刷新"** 按钮即可重新加载最新数据。
|
||||||
|
|
||||||
|
### 技巧2:查看物流链接
|
||||||
|
|
||||||
|
点击"物流链接"列的链接,会在新标签页打开物流链接。
|
||||||
|
|
||||||
|
### 技巧3:分页查看
|
||||||
|
|
||||||
|
- 默认每页显示20条
|
||||||
|
- 可以选择10/20/50/100条每页
|
||||||
|
- 使用页码快速跳转
|
||||||
|
|
||||||
|
### 技巧4:重置筛选
|
||||||
|
|
||||||
|
点击 **"重置"** 按钮清除所有搜索条件,显示所有日志。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 数据解读
|
||||||
|
|
||||||
|
### 操作类型
|
||||||
|
|
||||||
|
- **批量同步**:通过"批量同步物流"按钮触发的同步
|
||||||
|
- **单个写入**:单个订单的写入操作(如果有)
|
||||||
|
|
||||||
|
### 操作状态
|
||||||
|
|
||||||
|
- **成功** (绿色):
|
||||||
|
- 物流链接已写入腾讯文档
|
||||||
|
- 订单推送状态已更新为"已推送"
|
||||||
|
- 操作完全成功
|
||||||
|
|
||||||
|
- **跳过** (橙色):
|
||||||
|
- 订单已推送(数据库标记为已推送)
|
||||||
|
- 腾讯文档中该行已有物流链接
|
||||||
|
- 分布式锁获取失败(其他请求正在处理)
|
||||||
|
- 这是**正常现象**,防止重复推送
|
||||||
|
|
||||||
|
- **失败** (红色):
|
||||||
|
- 未找到订单
|
||||||
|
- 订单物流链接为空
|
||||||
|
- 腾讯文档API调用失败
|
||||||
|
- 其他异常
|
||||||
|
- 需要**人工处理**
|
||||||
|
|
||||||
|
### 统计数字含义
|
||||||
|
|
||||||
|
假设统计卡片显示:
|
||||||
|
```
|
||||||
|
成功: 150 跳过: 500 失败: 10 总计: 660
|
||||||
|
```
|
||||||
|
|
||||||
|
解读:
|
||||||
|
- 今天尝试同步了660个订单
|
||||||
|
- 成功同步150个(22.7%)
|
||||||
|
- 跳过500个(75.8%)- 这些订单可能已经同步过了
|
||||||
|
- 失败10个(1.5%)- 需要检查这些订单
|
||||||
|
|
||||||
|
**正常情况下:**
|
||||||
|
- 首次同步:成功比例较高(60-80%)
|
||||||
|
- 二次同步:跳过比例较高(80-95%)
|
||||||
|
- 失败比例:应该很低(<5%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 故障排查
|
||||||
|
|
||||||
|
### 问题1:看不到日志
|
||||||
|
|
||||||
|
**检查步骤:**
|
||||||
|
1. 是否已经执行过批量同步?
|
||||||
|
2. 后端是否正常运行?
|
||||||
|
3. 数据库表 `tencent_doc_operation_log` 是否有数据?
|
||||||
|
|
||||||
|
**验证SQL:**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) FROM tencent_doc_operation_log;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题2:日志不完整
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
- 后端日志记录失败
|
||||||
|
- 数据库连接异常
|
||||||
|
|
||||||
|
**检查方法:**
|
||||||
|
查看后端日志中是否有 "记录操作日志失败" 的错误。
|
||||||
|
|
||||||
|
### 问题3:"查看操作日志"按钮点击无反应
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
1. 按 F12 打开浏览器控制台
|
||||||
|
2. 查看是否有JavaScript错误
|
||||||
|
3. 清除浏览器缓存后重试
|
||||||
|
4. 确保前端已重新编译
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 数据库直接查询(备用方案)
|
||||||
|
|
||||||
|
如果前端页面有问题,可以直接查询数据库:
|
||||||
|
|
||||||
|
### 查看最近50条日志
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
operation_type AS 操作类型,
|
||||||
|
order_no AS 订单号,
|
||||||
|
target_row AS 目标行,
|
||||||
|
operation_status AS 状态,
|
||||||
|
error_message AS 错误信息,
|
||||||
|
operator AS 操作人,
|
||||||
|
create_time AS 时间
|
||||||
|
FROM tencent_doc_operation_log
|
||||||
|
WHERE file_id = 'DTUFydU9FTkRLbEN6' -- 替换为您的fileId
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
LIMIT 50;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看今天的统计
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
operation_status AS 状态,
|
||||||
|
COUNT(*) AS 数量
|
||||||
|
FROM tencent_doc_operation_log
|
||||||
|
WHERE file_id = 'DTUFydU9FTkRLbEN6'
|
||||||
|
AND DATE(create_time) = CURDATE()
|
||||||
|
GROUP BY operation_status;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查找某个订单的记录
|
||||||
|
```sql
|
||||||
|
SELECT *
|
||||||
|
FROM tencent_doc_operation_log
|
||||||
|
WHERE order_no = 'JY202511061595'
|
||||||
|
ORDER BY create_time DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 使用效果
|
||||||
|
|
||||||
|
使用日志查看功能后,您可以:
|
||||||
|
|
||||||
|
✅ **实时监控**:随时查看同步状态
|
||||||
|
✅ **快速定位**:找出问题订单
|
||||||
|
✅ **追溯历史**:查看操作记录
|
||||||
|
✅ **错误诊断**:分析失败原因
|
||||||
|
✅ **效率统计**:评估同步效率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 界面按钮位置
|
||||||
|
|
||||||
|
```
|
||||||
|
订单列表页面
|
||||||
|
↓
|
||||||
|
顶部操作栏
|
||||||
|
↓
|
||||||
|
[搜索] [重置] [导出] [H-TF自动写入配置] [批量同步物流]
|
||||||
|
↓
|
||||||
|
配置对话框
|
||||||
|
↓
|
||||||
|
底部操作按钮
|
||||||
|
↓
|
||||||
|
[查看操作日志] [测试配置] [清除配置] [取消] [保存配置]
|
||||||
|
↓
|
||||||
|
日志查看页面 ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**完成!** 🎊
|
||||||
|
|
||||||
|
如有问题,请查看:
|
||||||
|
- `操作日志查看功能说明.md` - 完整功能文档
|
||||||
|
- `如何查看同步进度和操作日志.md` - 技术细节
|
||||||
|
|
||||||
|
祝使用愉快! 😊
|
||||||
|
|
||||||
251
doc/操作日志查看功能说明.md
Normal file
251
doc/操作日志查看功能说明.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# 腾讯文档操作日志查看功能说明
|
||||||
|
|
||||||
|
## 📊 功能概览
|
||||||
|
|
||||||
|
新增了一个**操作日志查看页面**,可以方便地查看所有腾讯文档的同步操作记录,包括成功、失败、跳过的记录。
|
||||||
|
|
||||||
|
## 🎯 功能特性
|
||||||
|
|
||||||
|
### 1. **可视化统计卡片**
|
||||||
|
- ✅ 成功数量(绿色)
|
||||||
|
- ⚠️ 跳过数量(橙色)
|
||||||
|
- ❌ 失败数量(红色)
|
||||||
|
- 📊 总计数量(蓝色)
|
||||||
|
|
||||||
|
### 2. **强大的搜索功能**
|
||||||
|
- 按订单号搜索
|
||||||
|
- 按操作类型筛选(批量同步/单个写入)
|
||||||
|
- 按操作状态筛选(成功/失败/跳过)
|
||||||
|
|
||||||
|
### 3. **详细的日志展示**
|
||||||
|
- 操作类型(带标签)
|
||||||
|
- 订单号
|
||||||
|
- 目标行号
|
||||||
|
- 物流链接(可点击)
|
||||||
|
- 操作状态(带标签)
|
||||||
|
- 错误信息
|
||||||
|
- 操作人
|
||||||
|
- 操作时间
|
||||||
|
|
||||||
|
### 4. **分页功能**
|
||||||
|
- 支持10/20/50/100条每页
|
||||||
|
- 总计显示
|
||||||
|
- 页码跳转
|
||||||
|
|
||||||
|
## 📍 如何使用
|
||||||
|
|
||||||
|
### 方法1:从配置页面打开(推荐)
|
||||||
|
|
||||||
|
1. 打开订单列表页面
|
||||||
|
2. 点击 **"H-TF自动写入配置"** 按钮
|
||||||
|
3. 在配置对话框底部,点击 **"查看操作日志"** 按钮(蓝色)
|
||||||
|
4. 即可查看当前文档的所有操作日志
|
||||||
|
|
||||||
|
### 方法2:直接在列表页面添加按钮(可选)
|
||||||
|
|
||||||
|
如果需要,也可以在订单列表页面添加一个独立的"查看日志"按钮。
|
||||||
|
|
||||||
|
## 🔍 日志搜索示例
|
||||||
|
|
||||||
|
### 示例1:查看某个订单的操作记录
|
||||||
|
|
||||||
|
1. 在"订单号"输入框输入:`JY202511061595`
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看该订单的所有操作历史
|
||||||
|
|
||||||
|
### 示例2:查看今天失败的操作
|
||||||
|
|
||||||
|
1. 在"操作状态"下拉框选择:`失败`
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看所有失败的记录和错误信息
|
||||||
|
|
||||||
|
### 示例3:查看批量同步的记录
|
||||||
|
|
||||||
|
1. 在"操作类型"下拉框选择:`批量同步`
|
||||||
|
2. 点击"搜索"
|
||||||
|
3. 查看所有批量同步的操作
|
||||||
|
|
||||||
|
## 📊 统计卡片说明
|
||||||
|
|
||||||
|
页面顶部的统计卡片会**实时计算**当前筛选条件下的数据:
|
||||||
|
|
||||||
|
- **成功**:操作成功完成的数量
|
||||||
|
- **跳过**:因为各种原因跳过的数量(如已有数据、已推送等)
|
||||||
|
- **失败**:操作失败的数量
|
||||||
|
- **总计**:所有记录的总数
|
||||||
|
|
||||||
|
## 🎨 界面截图说明
|
||||||
|
|
||||||
|
### 统计卡片区域
|
||||||
|
```
|
||||||
|
┌─────────────┬─────────────┬─────────────┬─────────────┐
|
||||||
|
│ 成功 │ 跳过 │ 失败 │ 总计 │
|
||||||
|
│ 150 │ 500 │ 10 │ 660 │
|
||||||
|
│ (绿色) │ (橙色) │ (红色) │ (蓝色) │
|
||||||
|
└─────────────┴─────────────┴─────────────┴─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志表格
|
||||||
|
```
|
||||||
|
序号 | 操作类型 | 订单号 | 目标行 | 物流链接 | 状态 | 错误信息 | 操作人 | 操作时间
|
||||||
|
-----|----------|--------|--------|----------|------|----------|--------|----------
|
||||||
|
1 | 批量同步 | JY123 | 2575 | https... | 成功 | - | admin | 22:03:30
|
||||||
|
2 | 批量同步 | JY124 | 2576 | https... | 跳过 | 已有数据 | admin | 22:03:30
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 后端接口
|
||||||
|
|
||||||
|
**1. 查询操作日志列表**
|
||||||
|
```
|
||||||
|
GET /jarvis-api/jarvis/tendoc/operationLogs
|
||||||
|
参数:
|
||||||
|
- fileId: 文件ID(可选)
|
||||||
|
- sheetId: 工作表ID(可选)
|
||||||
|
- orderNo: 订单号(可选)
|
||||||
|
- operationType: 操作类型(可选)
|
||||||
|
- operationStatus: 操作状态(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 查询最近的操作日志**
|
||||||
|
```
|
||||||
|
GET /jarvis-api/jarvis/tendoc/recentLogs
|
||||||
|
参数:
|
||||||
|
- fileId: 文件ID(可选)
|
||||||
|
- limit: 限制数量(默认50)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端组件
|
||||||
|
|
||||||
|
- **组件位置**:`src/views/system/jdorder/components/TencentDocOperationLogs.vue`
|
||||||
|
- **组件名称**:`TencentDocOperationLogs`
|
||||||
|
- **依赖API**:`@/api/jarvis/tendoc.js`
|
||||||
|
|
||||||
|
## 📝 操作状态说明
|
||||||
|
|
||||||
|
### SUCCESS(成功)
|
||||||
|
- 物流链接成功写入腾讯文档
|
||||||
|
- 订单状态已更新
|
||||||
|
- 操作日志已记录
|
||||||
|
|
||||||
|
### FAILED(失败)
|
||||||
|
可能的原因:
|
||||||
|
- 未找到订单
|
||||||
|
- 订单物流链接为空
|
||||||
|
- API调用失败
|
||||||
|
- 写入异常
|
||||||
|
|
||||||
|
### SKIPPED(跳过)
|
||||||
|
可能的原因:
|
||||||
|
- 订单已推送(`tencent_doc_pushed = 1`)
|
||||||
|
- 腾讯文档中该行已有物流链接
|
||||||
|
- 分布式锁获取失败
|
||||||
|
|
||||||
|
## 🚀 性能优化建议
|
||||||
|
|
||||||
|
1. **定期清理历史日志**
|
||||||
|
```sql
|
||||||
|
-- 清理30天前的日志
|
||||||
|
DELETE FROM tencent_doc_operation_log
|
||||||
|
WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **添加索引优化查询**
|
||||||
|
```sql
|
||||||
|
-- 已创建的索引
|
||||||
|
CREATE INDEX idx_file_id ON tencent_doc_operation_log(file_id);
|
||||||
|
CREATE INDEX idx_order_no ON tencent_doc_operation_log(order_no);
|
||||||
|
CREATE INDEX idx_create_time ON tencent_doc_operation_log(create_time);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 权限控制
|
||||||
|
|
||||||
|
目前日志查看功能没有单独的权限控制,与腾讯文档配置功能共用权限。
|
||||||
|
|
||||||
|
如需单独控制,可以:
|
||||||
|
1. 在后端Controller添加权限注解
|
||||||
|
2. 在前端路由配置中添加权限判断
|
||||||
|
|
||||||
|
## 📱 响应式设计
|
||||||
|
|
||||||
|
日志查看对话框支持响应式设计:
|
||||||
|
- 宽度:90%(自适应屏幕)
|
||||||
|
- 表格:最大高度500px,自动滚动
|
||||||
|
- 分页:右对齐,自适应
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么看不到日志?
|
||||||
|
|
||||||
|
**A:** 可能原因:
|
||||||
|
1. 还没有执行过批量同步
|
||||||
|
2. fileId或sheetId参数不正确
|
||||||
|
3. 后端接口异常
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 先执行一次批量同步
|
||||||
|
- 检查后端日志是否有错误
|
||||||
|
- 检查数据库表 `tencent_doc_operation_log` 是否有数据
|
||||||
|
|
||||||
|
### Q2: 统计数字不准确?
|
||||||
|
|
||||||
|
**A:** 统计是基于**当前筛选条件**计算的,不是所有数据的统计。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- 如果筛选了"失败"状态,统计卡片只会统计失败的记录
|
||||||
|
- 重置筛选条件后,统计会更新
|
||||||
|
|
||||||
|
### Q3: 能否导出日志?
|
||||||
|
|
||||||
|
**A:** 当前版本不支持导出,但可以直接从数据库导出:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 导出CSV格式
|
||||||
|
SELECT
|
||||||
|
operation_type,
|
||||||
|
order_no,
|
||||||
|
target_row,
|
||||||
|
logistics_link,
|
||||||
|
operation_status,
|
||||||
|
error_message,
|
||||||
|
operator,
|
||||||
|
create_time
|
||||||
|
INTO OUTFILE '/tmp/tendoc_logs.csv'
|
||||||
|
FIELDS TERMINATED BY ','
|
||||||
|
ENCLOSED BY '"'
|
||||||
|
LINES TERMINATED BY '\n'
|
||||||
|
FROM tencent_doc_operation_log
|
||||||
|
WHERE file_id = 'DTUFydU9FTkRLbEN6'
|
||||||
|
ORDER BY create_time DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4: 如何清空所有日志?
|
||||||
|
|
||||||
|
**A:** 谨慎操作!执行以下SQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 清空指定文档的日志
|
||||||
|
DELETE FROM tencent_doc_operation_log
|
||||||
|
WHERE file_id = 'DTUFydU9FTkRLbEN6';
|
||||||
|
|
||||||
|
-- 清空所有日志(慎用!)
|
||||||
|
TRUNCATE TABLE tencent_doc_operation_log;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 使用效果
|
||||||
|
|
||||||
|
使用操作日志功能后,您可以:
|
||||||
|
|
||||||
|
1. ✅ **实时监控**同步状态
|
||||||
|
2. ✅ **快速定位**问题订单
|
||||||
|
3. ✅ **追溯历史**操作记录
|
||||||
|
4. ✅ **统计分析**同步效率
|
||||||
|
5. ✅ **错误诊断**失败原因
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-06 22:50
|
||||||
|
**版本**: v1.0
|
||||||
|
**作者**: AI Assistant
|
||||||
|
|
||||||
178
nginx-https.conf
Normal file
178
nginx-https.conf
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# WebSocket连接升级映射(必须在server块之前定义)
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 80端口:仅处理HTTP请求,自动重定向到HTTPS
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name jarvis.van333.cn; # 匹配域名
|
||||||
|
|
||||||
|
# 核心:HTTP请求永久重定向到HTTPS(301表示永久重定向)
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
# 可选:记录重定向日志(便于排查)
|
||||||
|
access_log /www/sites/jarvis.van333.cn/log/redirect.log main;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 443端口:处理HTTPS请求,包含SSL配置和业务逻辑
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name jarvis.van333.cn; # 与80端口保持一致的域名
|
||||||
|
|
||||||
|
# 网站根目录和默认首页(保留你的业务配置)
|
||||||
|
root /www/sites/jarvis.van333.cn/index;
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
# SSL证书配置(仅在443端口生效)
|
||||||
|
ssl_certificate /www/common/ssl/jarvis.van333.cn/fullchain.cer;
|
||||||
|
ssl_certificate_key /www/common/ssl/jarvis.van333.cn.key;
|
||||||
|
|
||||||
|
# SSL安全配置(复用你的原有配置)
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
access_log /www/sites/jarvis.van333.cn/log/access.log main;
|
||||||
|
error_log /www/sites/jarvis.van333.cn/log/error.log;
|
||||||
|
|
||||||
|
# 静态资源缓存配置
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========== 重要:后端API代理配置 ==========
|
||||||
|
# 将所有API请求代理到后端服务器(解决混合内容问题)
|
||||||
|
# 注意:这里的路径需要与前端 VUE_APP_BASE_API 配置一致
|
||||||
|
# ^~ 表示命中前缀后不再尝试正则 location,避免被其它规则干扰
|
||||||
|
location ^~ /jarvis-api/ {
|
||||||
|
proxy_pass http://127.0.0.1:30313/; # 后端服务地址
|
||||||
|
|
||||||
|
# 请求头设置
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
|
||||||
|
# 勿手动设置 Content-Type / Content-Length:multipart 上传必须原样转发(含 boundary),
|
||||||
|
# 且分块请求时 $content_length 可能为空,会导致后端报「not a multipart request」。
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_pass_request_body on;
|
||||||
|
|
||||||
|
# HTTP版本和WebSocket支持
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
|
||||||
|
# 请求缓冲设置(对大文件上传有用)
|
||||||
|
proxy_request_buffering on;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# OAuth 回调:须放在 /jarvis-api/ 之后、location / 之前。
|
||||||
|
# 切勿重复定义同名 location(例如两个 /wps365-callback,仅第一个生效)。
|
||||||
|
|
||||||
|
# 腾讯文档 OAuth 回调
|
||||||
|
location /tendoc-callback {
|
||||||
|
proxy_pass http://127.0.0.1:30313/tendoc-callback;
|
||||||
|
|
||||||
|
# 请求头设置
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_pass_request_body on;
|
||||||
|
|
||||||
|
# HTTP版本
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
|
||||||
|
# 请求体大小限制
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 金山文档:旧回调路径(后端 302 → /kdocs-callback,仅过渡用;后台建议改为 /kdocs-callback)
|
||||||
|
location /wps365-callback {
|
||||||
|
proxy_pass http://127.0.0.1:30313/wps365-callback;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_pass_request_body on;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 金山文档 OAuth 回调(与 kdocs.redirect-uri、开放平台登记一致)
|
||||||
|
location /kdocs-callback {
|
||||||
|
proxy_pass http://127.0.0.1:30313/kdocs-callback;
|
||||||
|
|
||||||
|
# 请求头设置
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_pass_request_body on;
|
||||||
|
|
||||||
|
# HTTP版本
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
|
||||||
|
# 请求体大小限制
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注意:jarvis相关API已通过 /jarvis-api/ 代理,不再需要单独的 /jarvis/ location
|
||||||
|
|
||||||
|
# Druid监控代理(如果需要)
|
||||||
|
location /druid/ {
|
||||||
|
proxy_pass http://127.0.0.1:30313/druid/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vue Router History模式支持(必须放在最后)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 404错误页面
|
||||||
|
error_page 404 /404.html;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
"jsencrypt": "3.0.0-rc.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
|
"pinyin-pro": "^3.27.0",
|
||||||
"quill": "2.0.2",
|
"quill": "2.0.2",
|
||||||
"screenfull": "5.0.2",
|
"screenfull": "5.0.2",
|
||||||
"sortablejs": "1.10.2",
|
"sortablejs": "1.10.2",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 27 KiB |
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta name="renderer" content="webkit">
|
<meta name="renderer" content="webkit">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, minimum-scale=1, user-scalable=yes, viewport-fit=cover">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title><%= webpackConfig.name %></title>
|
<title><%= webpackConfig.name %></title>
|
||||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
<theme-picker />
|
<!-- <theme-picker /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ThemePicker from "@/components/ThemePicker"
|
// import ThemePicker from "@/components/ThemePicker"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App"
|
||||||
components: { ThemePicker }
|
// components: { ThemePicker }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
53
src/api/jarvis/batchPublish.js
Normal file
53
src/api/jarvis/batchPublish.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 解析线报消息
|
||||||
|
export function parseLineReport(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/parse',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量发品
|
||||||
|
export function batchPublish(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/publish',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询批量发品任务列表
|
||||||
|
export function listTasks(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/task/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询批量发品任务详情
|
||||||
|
export function getTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/task/' + taskId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询批量发品明细列表
|
||||||
|
export function listItems(taskId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/item/list/' + taskId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动重试任务
|
||||||
|
export function retryTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/batchPublish/task/retry/' + taskId,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
154
src/api/jarvis/comment.js
Normal file
154
src/api/jarvis/comment.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询京东评论列表
|
||||||
|
export function listJdComment(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/jd/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询京东评论详细
|
||||||
|
export function getJdComment(id) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/jd/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改京东评论
|
||||||
|
export function updateJdComment(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/jd',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除京东评论
|
||||||
|
export function delJdComment(ids) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/jd/' + ids,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置京东评论使用状态
|
||||||
|
export function resetJdCommentByProductId(productId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/jd/reset/' + productId,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询淘宝评论列表
|
||||||
|
export function listTbComment(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/taobaoComment/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询淘宝评论详细
|
||||||
|
export function getTbComment(id) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/taobaoComment/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改淘宝评论
|
||||||
|
export function updateTbComment(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/taobaoComment',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除淘宝评论
|
||||||
|
export function delTbComment(ids) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/taobaoComment/' + ids,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置淘宝评论使用状态
|
||||||
|
export function resetTbCommentByProductId(productId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/taobaoComment/reset/' + productId,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取评论统计信息
|
||||||
|
export function getCommentStatistics(source) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params: { source }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取接口调用统计
|
||||||
|
export function getApiStatistics(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/api/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Redis产品类型映射(京东)
|
||||||
|
export function getJdProductTypeMap() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/redis/jd/map',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Redis产品类型映射(淘宝)
|
||||||
|
export function getTbProductTypeMap() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/redis/tb/map',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取评论(由后端转发到外部服务,避免前端跨域)
|
||||||
|
export function fetchComments(productId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/comment/fetch-comments',
|
||||||
|
method: 'get',
|
||||||
|
params: { product_id: productId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前IP地址(公开接口)
|
||||||
|
export function getCurrentIP() {
|
||||||
|
return request({
|
||||||
|
url: '/public/comment/ip',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取评论生成历史记录(公开接口)
|
||||||
|
export function getCommentHistory(query) {
|
||||||
|
return request({
|
||||||
|
url: '/public/comment/history',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取评论生成使用统计(公开接口)
|
||||||
|
export function getCommentUsageStatistics() {
|
||||||
|
return request({
|
||||||
|
url: '/public/comment/usage-statistics',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
97
src/api/jarvis/kdocs.js
Normal file
97
src/api/jarvis/kdocs.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getKdocsAuthUrl(state) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/authUrl',
|
||||||
|
method: 'get',
|
||||||
|
params: state ? { state } : {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKdocsTokenStatus(userId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/tokenStatus',
|
||||||
|
method: 'get',
|
||||||
|
params: { userId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshKdocsToken(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/refreshToken',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setKdocsToken(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/setToken',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKdocsUserInfo(userId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/userInfo',
|
||||||
|
method: 'get',
|
||||||
|
params: { userId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKdocsFileList(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/files',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKdocsFileInfo(userId, fileToken) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/fileInfo',
|
||||||
|
method: 'get',
|
||||||
|
params: { userId, fileToken }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKdocsSheetList(userId, fileToken) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/sheets',
|
||||||
|
method: 'get',
|
||||||
|
params: { userId, fileToken }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createKdocsSheet(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/createSheet',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readKdocsCells(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/readCells',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateKdocsCells(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/updateCells',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchUpdateKdocsCells(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/kdocs/batchUpdateCells',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
20
src/api/jarvis/marketingImage.js
Normal file
20
src/api/jarvis/marketingImage.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 生成单张营销图片
|
||||||
|
export function generateMarketingImage(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/marketing-image/generate',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量生成营销图片
|
||||||
|
export function batchGenerateMarketingImages(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/marketing-image/batch-generate',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
43
src/api/jarvis/phoneReplaceConfig.js
Normal file
43
src/api/jarvis/phoneReplaceConfig.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取指定类型的手机号列表
|
||||||
|
export function getPhoneList(type) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/phoneReplaceConfig/' + type,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置指定类型的手机号列表
|
||||||
|
export function setPhoneList(type, phoneList) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/phoneReplaceConfig/' + type,
|
||||||
|
method: 'put',
|
||||||
|
data: phoneList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加手机号到指定类型
|
||||||
|
export function addPhone(type, phone) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/phoneReplaceConfig/' + type + '/add',
|
||||||
|
method: 'post',
|
||||||
|
data: phone,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从指定类型删除手机号
|
||||||
|
export function removePhone(type, phone) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/phoneReplaceConfig/' + type + '/remove',
|
||||||
|
method: 'post',
|
||||||
|
data: phone,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
53
src/api/jarvis/productJdConfig.js
Normal file
53
src/api/jarvis/productJdConfig.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询产品京东配置列表
|
||||||
|
export function listProductJdConfig(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询产品京东配置详细
|
||||||
|
export function getProductJdConfig(productModel) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig/' + productModel,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增产品京东配置
|
||||||
|
export function addProductJdConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改产品京东配置
|
||||||
|
export function updateProductJdConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除产品京东配置
|
||||||
|
export function delProductJdConfig(productModels) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig/' + productModels,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化默认数据
|
||||||
|
export function initDefaultData() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/productJdConfig/initData',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
38
src/api/jarvis/socialMedia.js
Normal file
38
src/api/jarvis/socialMedia.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 提取关键词
|
||||||
|
export function extractKeywords(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/extract-keywords',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文案
|
||||||
|
export function generateContent(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/generate-content',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一键生成完整内容(关键词 + 文案 + 图片)
|
||||||
|
export function generateComplete(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/generate-complete',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 闲鱼文案(手动):根据标题+可选型号生成代下单、教你下单文案
|
||||||
|
export function generateXianyuWenan(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/xianyu-wenan/generate',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
35
src/api/jarvis/socialMediaPrompt.js
Normal file
35
src/api/jarvis/socialMediaPrompt.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取提示词模板列表
|
||||||
|
export function listPromptTemplates() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/prompt/list',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个提示词模板
|
||||||
|
export function getPromptTemplate(key) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/prompt/' + key,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存提示词模板
|
||||||
|
export function savePromptTemplate(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/prompt/save',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除提示词模板(恢复默认)
|
||||||
|
export function deletePromptTemplate(key) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/prompt/' + key,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
243
src/api/jarvis/tendoc.js
Normal file
243
src/api/jarvis/tendoc.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取腾讯文档授权URL
|
||||||
|
export function getTencentDocAuthUrl() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/authUrl',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth回调获取访问令牌
|
||||||
|
export function getTencentDocAccessToken(code) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/oauth/callback',
|
||||||
|
method: 'get',
|
||||||
|
params: { code }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新访问令牌
|
||||||
|
export function refreshTencentDocToken(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/refreshToken',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充单个订单的物流链接(直接传单号和物流链接)
|
||||||
|
export function fillSingleLogistics(thirdPartyOrderNo, logisticsLink) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/fillSingleLogistics',
|
||||||
|
method: 'post',
|
||||||
|
data: { thirdPartyOrderNo, logisticsLink }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量同步物流链接(从数据库读取订单物流信息并填充到表格)
|
||||||
|
export function fillLogisticsByOrderNo(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/fillLogisticsByOrderNo',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取token状态
|
||||||
|
export function getTokenStatus() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/tokenStatus',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置token(用于首次授权)
|
||||||
|
export function setToken(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/setToken',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加单个订单物流信息
|
||||||
|
export function appendLogistics(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/appendLogistics',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动发货
|
||||||
|
export function autoShip(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/autoShip',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取表格数据
|
||||||
|
export function readSheetData(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/readSheet',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
export function getFileInfo(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/fileInfo',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作表列表
|
||||||
|
export function getSheetList(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/sheetList',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试获取用户信息
|
||||||
|
export function testUserInfo() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/testUserInfo',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== H-TF订单自动写入配置接口 ====================
|
||||||
|
|
||||||
|
// 获取自动写入配置
|
||||||
|
export function getAutoWriteConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tencentDoc/config',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新自动写入配置
|
||||||
|
export function updateAutoWriteConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tencentDoc/config',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试配置是否有效
|
||||||
|
export function testAutoWriteConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tencentDoc/config/test',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除自动写入配置
|
||||||
|
export function clearAutoWriteConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tencentDoc/config',
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档的工作表列表
|
||||||
|
export function getDocSheetList(fileId) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tencentDoc/config/sheets',
|
||||||
|
method: 'get',
|
||||||
|
params: { fileId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询操作日志列表
|
||||||
|
export function getOperationLogs(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/operationLogs',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询最近的操作日志
|
||||||
|
export function getRecentLogs(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/recentLogs',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 批量推送记录相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批量推送记录列表
|
||||||
|
*/
|
||||||
|
export function getBatchPushRecords(params) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/batchPushRecords',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批量推送记录详情
|
||||||
|
*/
|
||||||
|
export function getBatchPushRecordDetail(batchId) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/tendoc/batchPushRecord/${batchId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推送状态和倒计时信息
|
||||||
|
*/
|
||||||
|
export function getPushStatus() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/pushStatus',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发立即推送
|
||||||
|
*/
|
||||||
|
export function triggerPushNow() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/triggerPushNow',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消待推送任务
|
||||||
|
*/
|
||||||
|
export function cancelPendingPush() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/cancelPendingPush',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反向同步第三方单号
|
||||||
|
* 从腾讯文档的物流单号列读取链接,通过链接匹配本地订单,将腾讯文档的单号列值写入到订单的第三方单号字段
|
||||||
|
*/
|
||||||
|
export function reverseSyncThirdPartyOrderNo(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/tendoc/reverseSyncThirdPartyOrderNo',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
32
src/api/jarvis/wecomInboundTrace.js
Normal file
32
src/api/jarvis/wecomInboundTrace.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listWecomInboundTrace(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/inboundTrace/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWecomInboundTrace(id) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/inboundTrace/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delWecomInboundTrace(ids) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/inboundTrace/' + (Array.isArray(ids) ? ids.join(',') : ids),
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理测试数据(追踪表 + 可选 Redis) */
|
||||||
|
export function cleanWecomInboundTraceTestData(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/inboundTrace/cleanTestData',
|
||||||
|
method: 'post',
|
||||||
|
data: data || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
40
src/api/jarvis/wecomShareLinkLogistics.js
Normal file
40
src/api/jarvis/wecomShareLinkLogistics.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listWecomShareLinkLogisticsJob(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/shareLinkLogisticsJob/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWecomShareLinkLogisticsJob(jobKey) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/shareLinkLogisticsJob/' + encodeURIComponent(jobKey),
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function backfillShareLinkLogisticsFromTrace() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/shareLinkLogisticsJob/backfillFromInboundTrace',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与订单列表「获取物流」一致:立即查物流并推送 */
|
||||||
|
export function fetchShareLinkManually(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/shareLinkLogisticsJob/fetchShareLinkManually',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动执行一轮 Redis 待扫描队列(同定时任务) */
|
||||||
|
export function drainShareLinkPendingQueueOnce() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/wecom/shareLinkLogisticsJob/drainPendingQueueOnce',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
22
src/api/monitor/logfile.js
Normal file
22
src/api/monitor/logfile.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/** 获取可选的日志文件列表 */
|
||||||
|
export function listLogfiles() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/logfile/list',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取日志文件末尾 N 行(HTTPS 轮询,无需 WebSocket)
|
||||||
|
* @param {string} file - 文件名,如 sys-info.log
|
||||||
|
* @param {number} lines - 行数,默认 500,最大 5000
|
||||||
|
*/
|
||||||
|
export function tailLogfile(file, lines = 500) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/logfile/tail',
|
||||||
|
method: 'get',
|
||||||
|
params: { file, lines }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,4 +6,12 @@ export function getServer() {
|
|||||||
url: '/monitor/server',
|
url: '/monitor/server',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取服务健康度检测
|
||||||
|
export function getHealth() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/server/health',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
23
src/api/public/order.js
Normal file
23
src/api/public/order.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 提交公开订单
|
||||||
|
export function submitPublicOrder(data) {
|
||||||
|
return request({
|
||||||
|
url: '/public/order/submit',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制提交公开订单(带forceGenerate参数)
|
||||||
|
export function submitPublicOrderWithForce(data) {
|
||||||
|
return request({
|
||||||
|
url: '/public/order/submit',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
forceGenerate: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
98
src/api/system/erpProduct.js
Normal file
98
src/api/system/erpProduct.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
export function listErpProduct(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/erpProduct/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情
|
||||||
|
export function getErpProduct(id) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/erpProduct/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
export function addErpProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/erpProduct',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改
|
||||||
|
export function updateErpProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/erpProduct',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
export function delErpProduct(ids) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/erpProduct/${ids}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取商品列表(单页,兼容)
|
||||||
|
export function pullProductList(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/erpProduct/pull',
|
||||||
|
method: 'post',
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全量同步商品(自动遍历所有页码)
|
||||||
|
export function syncAllProducts(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/erpProduct/syncAll',
|
||||||
|
method: 'post',
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量上架
|
||||||
|
export function batchPublish(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/batchPublish',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量下架
|
||||||
|
export function batchDownShelf(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/batchDownShelf',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取ERP账号列表
|
||||||
|
export function getERPAccounts() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/ERPAccount',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取授权的闲鱼会员名下拉
|
||||||
|
export function getUsernames(query) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/usernames',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
95
src/api/system/favoriteProduct.js
Normal file
95
src/api/system/favoriteProduct.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
export function listFavoriteProduct(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情
|
||||||
|
export function getFavoriteProduct(id) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/favoriteProduct/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
export function addFavoriteProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改
|
||||||
|
export function updateFavoriteProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
export function delFavoriteProduct(ids) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/favoriteProduct/${ids}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新置顶状态
|
||||||
|
export function updateTopStatus(id, isTop) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/favoriteProduct/updateTopStatus/${id}/${isTop}`,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到常用商品
|
||||||
|
export function addToFavorites(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct/addToFavorites',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据skuid查询是否已存在
|
||||||
|
export function getBySkuid(skuid) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/favoriteProduct/getBySkuid/${encodeURIComponent(skuid)}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户的常用商品列表
|
||||||
|
export function getUserFavorites() {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct/userFavorites',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从常用商品快速发品
|
||||||
|
export function quickPublishFromFavorite(id, appid) {
|
||||||
|
return request({
|
||||||
|
url: `/jarvis/favoriteProduct/quickPublish/${id}`,
|
||||||
|
method: 'post',
|
||||||
|
data: appid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新发品信息(商品ID、状态等)
|
||||||
|
export function updateProductInfo(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/favoriteProduct/updateProductInfo',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
37
src/api/system/giftcoupon.js
Normal file
37
src/api/system/giftcoupon.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 礼金列表
|
||||||
|
export function listGiftCoupons(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/giftcoupon/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 礼金详情(包含关联订单)
|
||||||
|
export function getGiftCoupon(giftCouponKey) {
|
||||||
|
return request({
|
||||||
|
url: `/system/giftcoupon/${giftCouponKey}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 礼金统计
|
||||||
|
export function getGiftCouponStatistics(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/giftcoupon/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出礼金列表
|
||||||
|
export function exportGiftCoupons(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/giftcoupon/export',
|
||||||
|
method: 'post',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
40
src/api/system/instruction.js
Normal file
40
src/api/system/instruction.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function executeInstruction(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/instruction/execute',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeInstructionWithForce(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/instruction/execute',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
forceGenerate: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史消息记录
|
||||||
|
* @param type request | response
|
||||||
|
* @param limit 条数上限
|
||||||
|
* @param keyword 可选,搜索关键词;传则后端在全部数据中过滤后返回
|
||||||
|
*/
|
||||||
|
export function getHistory(type, limit, keyword) {
|
||||||
|
const params = { type, limit }
|
||||||
|
if (keyword != null && String(keyword).trim() !== '') {
|
||||||
|
params.keyword = String(keyword).trim()
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/instruction/history',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,32 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
|
// JD订单列表
|
||||||
|
export function listJDOrders(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// JD订单详情
|
||||||
|
export function getJDOrder(id) {
|
||||||
|
return request({
|
||||||
|
url: `/system/jdorder/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新JD订单
|
||||||
|
export function updateJDOrder(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 一键转链
|
// 一键转链
|
||||||
export function generatePromotionContent(data) {
|
export function generatePromotionContent(data) {
|
||||||
@@ -8,3 +36,223 @@ export function generatePromotionContent(data) {
|
|||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建商品(基于转链生成的文案与图片)
|
||||||
|
export function createProductByPromotion(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/createByPromotion',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上架商品
|
||||||
|
export function publishProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/publish',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地区下拉
|
||||||
|
export function getProvinces() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/region/provinces',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCities(provId) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/region/cities',
|
||||||
|
method: 'get',
|
||||||
|
params: { provId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAreas(provId, cityId) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/region/areas',
|
||||||
|
method: 'get',
|
||||||
|
params: { provId, cityId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类目下拉
|
||||||
|
export function getCategories(params) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/categories',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会员名下拉
|
||||||
|
export function getUsernames(params) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/usernames',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERP 账号下拉(备用)
|
||||||
|
export function getERPAccounts() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/ERPAccount',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 属性下拉
|
||||||
|
export function getProperties(params) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/product/pv',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开礼金
|
||||||
|
export function createGiftCoupon(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/jdorder/createGiftCoupon',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转链(支持礼金)
|
||||||
|
export function transferWithGift(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/jdorder/transfer',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量创建礼金券
|
||||||
|
export function batchCreateGiftCoupons(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/jdorder/batchCreateGiftCoupons',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本URL替换(批量创建礼金并替换)
|
||||||
|
export function replaceUrlsWithGiftCoupons(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/jdorder/replaceUrlsWithGiftCoupons',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出JD订单列表
|
||||||
|
export function exportJDOrders(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/export',
|
||||||
|
method: 'post',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除JD订单(支持批量,ids为逗号分隔或数组)
|
||||||
|
export function delJDOrder(ids) {
|
||||||
|
// 兼容数组或字符串
|
||||||
|
const idPath = Array.isArray(ids) ? ids.join(',') : ids
|
||||||
|
return request({
|
||||||
|
url: `/system/jdorder/${idPath}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后返表上传记录列表
|
||||||
|
export function listGroupRebateExcelUploads(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/groupRebateUpload/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除后返表上传记录,并回滚对应订单后返备注(新导入数据) */
|
||||||
|
export function deleteGroupRebateUpload(id) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/groupRebateUpload/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function postGroupRebateMultipart(url, formData) {
|
||||||
|
return axios
|
||||||
|
.post(process.env.VUE_APP_BASE_API + url, formData, {
|
||||||
|
headers: { Authorization: 'Bearer ' + getToken() },
|
||||||
|
transformRequest: [
|
||||||
|
(data, headers) => {
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
if (headers && typeof headers.delete === 'function') {
|
||||||
|
headers.delete('Content-Type')
|
||||||
|
} else if (headers) {
|
||||||
|
delete headers['Content-Type']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const d = res.data
|
||||||
|
if (!d || d.code !== 200) {
|
||||||
|
return Promise.reject(new Error((d && d.msg) || '请求失败'))
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导入跟团返现 Excel(multipart,单文件) */
|
||||||
|
export function importGroupRebateExcel(formData) {
|
||||||
|
return postGroupRebateMultipart('/system/jdorder/importGroupRebateExcel', formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量导入后返表(multipart,多个 files 字段) */
|
||||||
|
export function importGroupRebateExcelBatch(formData) {
|
||||||
|
return postGroupRebateMultipart('/system/jdorder/importGroupRebateExcelBatch', formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动获取物流信息(用于调试)
|
||||||
|
export function fetchLogisticsManually(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/jdorder/fetchLogisticsManually',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单搜索工具接口(返回简易字段)
|
||||||
|
export function searchOrders(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/tools/search',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量标记后返到账(赔付金额>0的订单)
|
||||||
|
export function batchMarkRebateReceived() {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/tools/batch-mark-rebate-received',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成录单格式文本(Excel可粘贴格式)
|
||||||
|
export function generateExcelText(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/jdorder/generateExcelText',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
60
src/api/system/prdErrorTip.js
Normal file
60
src/api/system/prdErrorTip.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询商品错误提示列表
|
||||||
|
export function listPrdErrorTip(query) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询商品错误提示详细
|
||||||
|
export function getPrdErrorTip(id) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增商品错误提示
|
||||||
|
export function addPrdErrorTip(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改商品错误提示
|
||||||
|
export function updatePrdErrorTip(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除商品错误提示
|
||||||
|
export function delPrdErrorTip(id) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除商品错误提示
|
||||||
|
export function delPrdErrorTips(ids) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip/' + ids,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据错误代码和子代码查询错误提示
|
||||||
|
export function getPrdErrorTipByCode(errCode, errSubCode) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/prdErrorTip/getByCode/' + errCode + '/' + errSubCode,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 257 KiB |
@@ -89,4 +89,448 @@
|
|||||||
> .el-submenu__title
|
> .el-submenu__title
|
||||||
.el-submenu__icon-arrow {
|
.el-submenu__icon-arrow {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端 Element UI 组件优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
// 表格优化
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.el-table__header-wrapper,
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 8px 5px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__cell {
|
||||||
|
padding: 8px 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作列按钮优化
|
||||||
|
.el-button {
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 2px;
|
||||||
|
|
||||||
|
&.el-button--mini {
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格工具栏优化
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
.el-table__header {
|
||||||
|
th {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单优化
|
||||||
|
.el-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单项内联优化
|
||||||
|
.el-form-item--mini,
|
||||||
|
.el-form-item--small {
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框优化
|
||||||
|
.el-input {
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px; // 防止iOS自动缩放
|
||||||
|
height: 44px; // 增大触摸目标
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择器优化
|
||||||
|
.el-select {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期选择器优化
|
||||||
|
.el-date-editor {
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮优化
|
||||||
|
.el-button {
|
||||||
|
min-height: 44px; // 增大触摸目标
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&.el-button--mini {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.el-button--small {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框优化
|
||||||
|
.el-dialog {
|
||||||
|
width: 95% !important;
|
||||||
|
margin: 5vh auto !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 15px;
|
||||||
|
max-height: calc(90vh - 120px);
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 10px 15px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息框优化
|
||||||
|
.el-message-box {
|
||||||
|
width: 90% !important;
|
||||||
|
|
||||||
|
.el-message-box__content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-message-box__btns {
|
||||||
|
.el-button {
|
||||||
|
width: 48%;
|
||||||
|
margin: 0 1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽屉优化
|
||||||
|
.el-drawer {
|
||||||
|
width: 85% !important;
|
||||||
|
|
||||||
|
.el-drawer__header {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页优化
|
||||||
|
.el-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.el-pagination__sizes,
|
||||||
|
.el-pagination__total,
|
||||||
|
.el-pagination__jump {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-prev,
|
||||||
|
.btn-next,
|
||||||
|
.el-pager li {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签页优化
|
||||||
|
.el-tabs {
|
||||||
|
.el-tabs__header {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-wrap {
|
||||||
|
&::after {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片优化
|
||||||
|
.el-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-card__header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤条优化
|
||||||
|
.el-steps {
|
||||||
|
.el-step__title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-step__description {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传组件优化
|
||||||
|
.el-upload {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签优化
|
||||||
|
.el-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关优化
|
||||||
|
.el-switch {
|
||||||
|
.el-switch__core {
|
||||||
|
min-width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单选框组优化
|
||||||
|
.el-radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-radio {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复选框组优化
|
||||||
|
.el-checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-checkbox {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 级联选择器优化
|
||||||
|
.el-cascader {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间选择器优化
|
||||||
|
.el-time-picker {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字输入框优化
|
||||||
|
.el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑块优化
|
||||||
|
.el-slider {
|
||||||
|
margin: 15px 0;
|
||||||
|
|
||||||
|
.el-slider__button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评分优化
|
||||||
|
.el-rate {
|
||||||
|
.el-rate__item {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 颜色选择器优化
|
||||||
|
.el-color-picker {
|
||||||
|
.el-color-picker__trigger {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 穿梭框优化
|
||||||
|
.el-transfer {
|
||||||
|
.el-transfer-panel {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树形控件优化
|
||||||
|
.el-tree {
|
||||||
|
.el-tree-node__content {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠面板优化
|
||||||
|
.el-collapse {
|
||||||
|
.el-collapse-item__header {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse-item__content {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间线优化
|
||||||
|
.el-timeline {
|
||||||
|
.el-timeline-item__content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 描述列表优化
|
||||||
|
.el-descriptions {
|
||||||
|
.el-descriptions__label {
|
||||||
|
font-size: 13px;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态优化
|
||||||
|
.el-empty {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.el-empty__description {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 骨架屏优化
|
||||||
|
.el-skeleton {
|
||||||
|
.el-skeleton__item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结果页优化
|
||||||
|
.el-result {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.el-result__title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-result__subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
@import './element-ui.scss';
|
@import './element-ui.scss';
|
||||||
@import './sidebar.scss';
|
@import './sidebar.scss';
|
||||||
@import './btn.scss';
|
@import './btn.scss';
|
||||||
|
@import './mobile.scss';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -122,6 +123,10 @@ aside {
|
|||||||
//main-container全局样式
|
//main-container全局样式
|
||||||
.app-container {
|
.app-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.components-container {
|
.components-container {
|
||||||
@@ -176,3 +181,214 @@ aside {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移动端响应式优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
// 全局容器优化
|
||||||
|
.app-container {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-container {
|
||||||
|
margin: 15px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单优化
|
||||||
|
.el-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input,
|
||||||
|
.el-select,
|
||||||
|
.el-date-picker {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮组优化
|
||||||
|
.el-button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框优化
|
||||||
|
.el-dialog {
|
||||||
|
width: 95% !important;
|
||||||
|
margin: 5vh auto !important;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 15px;
|
||||||
|
max-height: calc(90vh - 120px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽屉优化
|
||||||
|
.el-drawer {
|
||||||
|
width: 85% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页优化
|
||||||
|
.pagination-container {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.el-pagination__sizes,
|
||||||
|
.el-pagination__total,
|
||||||
|
.el-pagination__jump {
|
||||||
|
display: none; // 移动端隐藏部分分页信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片优化
|
||||||
|
.el-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.el-card__header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签页优化
|
||||||
|
.el-tabs {
|
||||||
|
.el-tabs__header {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格容器优化
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
min-width: 600px; // 保持最小宽度,允许横向滚动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索表单优化
|
||||||
|
.search-form {
|
||||||
|
.el-form-item {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮区域优化
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本溢出处理
|
||||||
|
.text-ellipsis {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸优化
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端卡片列表优化
|
||||||
|
.mobile-card-list {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端搜索表单优化
|
||||||
|
.mobile-search-form {
|
||||||
|
.search-bar {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端按钮组优化
|
||||||
|
.mobile-button-group {
|
||||||
|
.mobile-buttons {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端表格容器优化
|
||||||
|
.table-container {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.el-table {
|
||||||
|
display: none; // 移动端隐藏表格,使用卡片视图
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框优化(防止iOS自动缩放)
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="tel"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用文本选择(移动端长按优化)
|
||||||
|
.no-select {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平板设备优化
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-container {
|
||||||
|
margin: 20px 30px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
394
src/assets/styles/mobile.scss
Normal file
394
src/assets/styles/mobile.scss
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
/**
|
||||||
|
* 移动端专用样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 移动端全局优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
// 页面容器
|
||||||
|
.app-container {
|
||||||
|
padding: 12px !important;
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 48px - 60px); // 减去头部和底部导航
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片样式
|
||||||
|
.el-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.el-card__header {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表项优化
|
||||||
|
.el-list-item {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割线优化
|
||||||
|
.el-divider {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签优化
|
||||||
|
.el-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 徽章优化
|
||||||
|
.el-badge {
|
||||||
|
.el-badge__content {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤条优化
|
||||||
|
.el-steps {
|
||||||
|
.el-step__title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-step__description {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间线优化
|
||||||
|
.el-timeline {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
.el-timeline-item__node {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-timeline-item__content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 描述列表优化
|
||||||
|
.el-descriptions {
|
||||||
|
.el-descriptions__label {
|
||||||
|
width: 35%;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态优化
|
||||||
|
.el-empty {
|
||||||
|
padding: 40px 20px;
|
||||||
|
|
||||||
|
.el-empty__image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-empty__description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 骨架屏优化
|
||||||
|
.el-skeleton {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.el-skeleton__item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结果页优化
|
||||||
|
.el-result {
|
||||||
|
padding: 30px 20px;
|
||||||
|
|
||||||
|
.el-result__icon {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-result__title {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-result__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载优化
|
||||||
|
.el-loading-mask {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息提示优化
|
||||||
|
.el-message {
|
||||||
|
min-width: auto;
|
||||||
|
width: 90%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
.el-message__content {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知优化
|
||||||
|
.el-notification {
|
||||||
|
width: 90%;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.el-notification__title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-notification__content {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹出层优化
|
||||||
|
.el-popover {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具提示优化
|
||||||
|
.el-tooltip__popper {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉菜单优化
|
||||||
|
.el-dropdown-menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择器下拉优化
|
||||||
|
.el-select-dropdown {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-select-dropdown__item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期选择器优化
|
||||||
|
.el-picker-panel {
|
||||||
|
width: 95%;
|
||||||
|
left: 2.5% !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-date-picker__header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-picker-panel__content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 级联选择器优化
|
||||||
|
.el-cascader-menus {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-cascader-menu {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 穿梭框优化
|
||||||
|
.el-transfer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.el-transfer-panel {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树形控件优化
|
||||||
|
.el-tree {
|
||||||
|
.el-tree-node__content {
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tree-node__label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠面板优化
|
||||||
|
.el-collapse {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.el-collapse-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.el-collapse-item__header {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse-item__content {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条优化
|
||||||
|
.el-progress {
|
||||||
|
.el-progress__text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑块优化
|
||||||
|
.el-slider {
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.el-slider__button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评分优化
|
||||||
|
.el-rate {
|
||||||
|
.el-rate__item {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 颜色选择器优化
|
||||||
|
.el-color-picker {
|
||||||
|
.el-color-picker__trigger {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传组件优化
|
||||||
|
.el-upload {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览优化
|
||||||
|
.el-image-viewer__wrapper {
|
||||||
|
.el-image-viewer__canvas {
|
||||||
|
img {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽屉优化
|
||||||
|
.el-drawer {
|
||||||
|
.el-drawer__header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
|
||||||
|
.el-drawer__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 固定定位元素优化(适配安全区域)
|
||||||
|
.fixed-bottom {
|
||||||
|
bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-top {
|
||||||
|
top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平板设备优化
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -137,6 +137,68 @@
|
|||||||
.pagination-container .el-pagination > .el-pagination__sizes {
|
.pagination-container .el-pagination > .el-pagination__sizes {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表格移动端优化
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.el-table__header-wrapper,
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px 5px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__cell {
|
||||||
|
padding: 8px 5px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单移动端优化
|
||||||
|
.form-header {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 5px 5px 15px 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片移动端优化
|
||||||
|
.el-card__header {
|
||||||
|
padding: 10px 12px 5px !important;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 12px 15px 15px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮组移动端优化
|
||||||
|
.top-right-btn {
|
||||||
|
float: none;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具类移动端优化
|
||||||
|
.mb20, .mt20, .mr20, .ml20 {
|
||||||
|
margin: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb10, .mt10, .mr10, .ml10 {
|
||||||
|
margin: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb5, .mt5, .mr5, .ml5 {
|
||||||
|
margin: 5px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table .fixed-width .el-button--mini {
|
.el-table .fixed-width .el-button--mini {
|
||||||
|
|||||||
@@ -12,20 +12,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
-webkit-transition: width .28s;
|
-webkit-transition: width .28s ease-in-out;
|
||||||
transition: width 0.28s;
|
transition: width 0.28s ease-in-out, box-shadow 0.3s ease;
|
||||||
width: $base-sidebar-width !important;
|
width: $base-sidebar-width !important;
|
||||||
background-color: $base-menu-background;
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 0px;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
border-radius: 0 20px 20px 0;
|
||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
-webkit-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
// 添加悬停效果
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加进入动画
|
||||||
|
animation: slideInLeft 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset element-ui css
|
// reset element-ui css
|
||||||
.horizontal-collapse-transition {
|
.horizontal-collapse-transition {
|
||||||
@@ -46,7 +68,7 @@
|
|||||||
|
|
||||||
&.has-logo {
|
&.has-logo {
|
||||||
.el-scrollbar {
|
.el-scrollbar {
|
||||||
height: calc(100% - 50px);
|
height: calc(100% - 60px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,41 +90,132 @@
|
|||||||
border: none;
|
border: none;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
font-size: 14px;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item, .el-submenu__title {
|
.el-menu-item, .el-submenu__title {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
}
|
font-size: 14px !important;
|
||||||
|
font-weight: 500;
|
||||||
// menu hover
|
color: rgba(33, 33, 33, 0.85) !important;
|
||||||
.submenu-title-noDropdown,
|
background: transparent !important;
|
||||||
.el-submenu__title {
|
border-radius: 12px;
|
||||||
|
margin: 4px 12px;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
background: rgba(25, 118, 210, 0.1) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: rgba(25, 118, 210, 0.15) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.2);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 20px;
|
||||||
|
background: #1976d2;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .theme-dark .is-active > .el-submenu__title {
|
// 子菜单样式优化
|
||||||
color: $base-menu-color-active !important;
|
.el-submenu {
|
||||||
}
|
.el-submenu__title {
|
||||||
|
border-radius: 12px;
|
||||||
& .nest-menu .el-submenu>.el-submenu__title,
|
margin: 4px 12px;
|
||||||
& .el-submenu .el-menu-item {
|
padding: 0 16px !important;
|
||||||
min-width: $base-sidebar-width !important;
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
&:hover {
|
||||||
|
background: rgba(25, 118, 210, 0.1) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
background: rgba(255, 255, 255, 0.3) !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 8px 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
margin: 2px 8px;
|
||||||
|
padding: 0 24px !important;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 13px !important;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 13px !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(25, 118, 210, 0.08) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: rgba(25, 118, 210, 0.12) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .theme-dark .nest-menu .el-submenu>.el-submenu__title,
|
// 图标样式优化
|
||||||
& .theme-dark .el-submenu .el-menu-item {
|
.svg-icon {
|
||||||
background-color: $base-sub-menu-background !important;
|
color: rgba(33, 33, 33, 0.7) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $base-sub-menu-hover !important;
|
color: #1976d2 !important;
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +240,14 @@
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-submenu {
|
.el-submenu {
|
||||||
@@ -138,7 +259,14 @@
|
|||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +274,24 @@
|
|||||||
.el-submenu {
|
.el-submenu {
|
||||||
&>.el-submenu__title {
|
&>.el-submenu__title {
|
||||||
&>span {
|
&>span {
|
||||||
height: 0;
|
height: 0 !important;
|
||||||
width: 0;
|
width: 0 !important;
|
||||||
overflow: hidden;
|
overflow: hidden !important;
|
||||||
visibility: hidden;
|
visibility: hidden !important;
|
||||||
display: inline-block;
|
display: inline-block !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
span {
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +306,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
transition: transform .28s;
|
transition: transform .28s ease-in-out;
|
||||||
width: $base-sidebar-width !important;
|
width: $base-sidebar-width !important;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
.el-menu-item, .el-submenu__title {
|
||||||
|
margin: 2px 8px;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-submenu .el-menu-item {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hideSidebar {
|
&.hideSidebar {
|
||||||
@@ -177,6 +331,38 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
transform: translate3d(-$base-sidebar-width, 0, 0);
|
transform: translate3d(-$base-sidebar-width, 0, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平板设备优化
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar-container {
|
||||||
|
.el-menu-item, .el-submenu__title {
|
||||||
|
font-size: 13px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小屏幕优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-container {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item, .el-submenu__title {
|
||||||
|
margin: 2px 6px;
|
||||||
|
padding: 0 10px !important;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,9 +374,55 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 弹出菜单样式优化
|
||||||
|
.el-menu--popup {
|
||||||
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
color: rgba(33, 33, 33, 0.85) !important;
|
||||||
|
margin: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(25, 118, 210, 0.1) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: rgba(25, 118, 210, 0.15) !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义滚动条
|
||||||
|
&::-webkit-scrollbar-track-piece {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when menu collapsed
|
// 收起状态下的样式
|
||||||
.el-menu--vertical {
|
.el-menu--vertical {
|
||||||
&>.el-menu {
|
&>.el-menu {
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
@@ -201,27 +433,7 @@
|
|||||||
.nest-menu .el-submenu>.el-submenu__title,
|
.nest-menu .el-submenu>.el-submenu__title,
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
&:hover {
|
&:hover {
|
||||||
// you can use $subMenuHover
|
background: rgba(255, 255, 255, 0.15) !important;
|
||||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the scroll bar appears when the subMenu is too long
|
|
||||||
>.el-menu--popup {
|
|
||||||
max-height: 100vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track-piece {
|
|
||||||
background: #d3dce6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #99a9bf;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ $tiffany: #4AB7BD;
|
|||||||
$yellow:#FEC171;
|
$yellow:#FEC171;
|
||||||
$panGreen: #30B08F;
|
$panGreen: #30B08F;
|
||||||
|
|
||||||
// 默认菜单主题风格
|
// 默认菜单主题风格 - 现代化渐变主题
|
||||||
$base-menu-color:#bfcbd9;
|
$base-menu-color:rgba(33, 33, 33, 0.85);
|
||||||
$base-menu-color-active:#f4f4f5;
|
$base-menu-color-active:#1976d2;
|
||||||
$base-menu-background:#304156;
|
$base-menu-background:linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
$base-logo-title-color: #ffffff;
|
$base-logo-title-color: #1976d2;
|
||||||
|
|
||||||
$base-menu-light-color:rgba(0,0,0,.70);
|
$base-menu-light-color:rgba(0,0,0,.70);
|
||||||
$base-menu-light-background:#ffffff;
|
$base-menu-light-background:#ffffff;
|
||||||
$base-logo-light-title-color: #001529;
|
$base-logo-light-title-color: #001529;
|
||||||
|
|
||||||
$base-sub-menu-background:#1f2d3d;
|
$base-sub-menu-background:rgba(255,255,255,0.08);
|
||||||
$base-sub-menu-hover:#001528;
|
$base-sub-menu-hover:rgba(255,255,255,0.15);
|
||||||
|
|
||||||
// 自定义暗色菜单风格
|
// 自定义暗色菜单风格
|
||||||
/**
|
/**
|
||||||
|
|||||||
134
src/components/ListLayout/index.vue
Normal file
134
src/components/ListLayout/index.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-layout">
|
||||||
|
<!-- 顶部搜索区域 -->
|
||||||
|
<div class="search-section">
|
||||||
|
<slot name="search"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 - 可滚动 -->
|
||||||
|
<div class="table-section">
|
||||||
|
<slot name="table"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 固定分页区域 -->
|
||||||
|
<div class="pagination-section">
|
||||||
|
<slot name="pagination"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ListLayout',
|
||||||
|
props: {
|
||||||
|
// 可以添加一些配置属性
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: 'calc(100vh - 84px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 主容器布局 */
|
||||||
|
.list-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 84px); /* 减去头部导航高度 */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域 - 固定在顶部 */
|
||||||
|
.search-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格区域 - 可滚动 */
|
||||||
|
.table-section {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 0; /* 确保 flex 子元素可以收缩 */
|
||||||
|
overflow-x: auto; /* 确保横向滚动条显示 */
|
||||||
|
overflow-y: auto; /* 确保纵向滚动条显示 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定分页区域 */
|
||||||
|
.pagination-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格区域滚动条样式 */
|
||||||
|
.table-section::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-thumb {
|
||||||
|
background: #c0c0c0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保表格在容器内正确显示,允许横向滚动 */
|
||||||
|
.table-section .el-table {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页组件样式优化 */
|
||||||
|
.pagination-section .pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.list-layout {
|
||||||
|
height: calc(100vh - 48px - 60px); /* 移动端调整高度:减去头部和底部导航 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0 10px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-section {
|
||||||
|
padding: 8px 10px;
|
||||||
|
/* 移动端隐藏分页,因为很少用 */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
249
src/components/MobileBottomNav/index.vue
Normal file
249
src/components/MobileBottomNav/index.vue
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-bottom-nav" :class="{ 'scrollable': navItems.length > 5 }" v-if="isMobile && show">
|
||||||
|
<div
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.path"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ 'active': isActive(item.path) }"
|
||||||
|
@click="handleNavClick(item)"
|
||||||
|
>
|
||||||
|
<div class="nav-icon">
|
||||||
|
<i :class="item.icon" v-if="item.icon"></i>
|
||||||
|
<svg-icon :icon-class="item.iconClass" v-else-if="item.iconClass" />
|
||||||
|
<el-badge :value="item.badge" :hidden="!item.badge" v-if="item.badge">
|
||||||
|
<div class="icon-placeholder"></div>
|
||||||
|
</el-badge>
|
||||||
|
</div>
|
||||||
|
<div class="nav-label">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileBottomNav',
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device', 'sidebarRouters']),
|
||||||
|
isMobile() {
|
||||||
|
return this.device === 'mobile' || window.innerWidth < 768
|
||||||
|
},
|
||||||
|
navItems() {
|
||||||
|
// 如果提供了自定义 items,直接使用
|
||||||
|
if (this.items && this.items.length > 0) {
|
||||||
|
return this.items
|
||||||
|
}
|
||||||
|
// 始终从 store 的 sidebarRouters 计算,保证与接口返回、路由注册一致,避免移动端菜单/跳转错乱
|
||||||
|
const routes = this.sidebarRouters || []
|
||||||
|
const flatRoutes = this.flattenRoutes(routes)
|
||||||
|
const mainRoutes = flatRoutes.filter(route => {
|
||||||
|
const excludePaths = ['/redirect', '/login', '/register', '/404', '/401', '/user/profile']
|
||||||
|
const path = route.path || ''
|
||||||
|
return path &&
|
||||||
|
path !== '/' &&
|
||||||
|
!excludePaths.some(exclude => path.includes(exclude)) &&
|
||||||
|
!path.startsWith('/user/')
|
||||||
|
})
|
||||||
|
if (mainRoutes.length > 0) {
|
||||||
|
return mainRoutes
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ path: '/sloworder/index', label: '首页', icon: 'el-icon-s-home' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 扁平化路由为叶子节点,路径与 Vue Router 注册的完整 path 一致 */
|
||||||
|
flattenRoutes(routes, parentPath = '') {
|
||||||
|
if (!routes || !Array.isArray(routes)) return []
|
||||||
|
const result = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.hidden) return
|
||||||
|
let fullPath = (route.path || '').trim()
|
||||||
|
if (parentPath) {
|
||||||
|
if (fullPath.startsWith('/')) {
|
||||||
|
// 已是绝对路径,直接使用
|
||||||
|
} else {
|
||||||
|
const base = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath
|
||||||
|
fullPath = `${base}/${fullPath}`.replace(/\/+/g, '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fullPath && !fullPath.startsWith('/')) {
|
||||||
|
fullPath = '/' + fullPath
|
||||||
|
}
|
||||||
|
if (route.children && route.children.length > 0) {
|
||||||
|
result.push(...this.flattenRoutes(route.children, fullPath))
|
||||||
|
} else if (route.meta && route.meta.title && fullPath) {
|
||||||
|
result.push({
|
||||||
|
path: fullPath,
|
||||||
|
label: route.meta.title,
|
||||||
|
icon: route.meta.icon || 'el-icon-menu',
|
||||||
|
iconClass: route.meta.icon,
|
||||||
|
route
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
isActive(path) {
|
||||||
|
if (!path) return false
|
||||||
|
const currentPath = this.$route.path
|
||||||
|
return currentPath === path || currentPath.startsWith(path + '/')
|
||||||
|
},
|
||||||
|
handleNavClick(item) {
|
||||||
|
if (item.handler) {
|
||||||
|
item.handler()
|
||||||
|
this.$emit('nav-click', item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.path) {
|
||||||
|
// 确保路径正确
|
||||||
|
let path = item.path
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
path = `/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除末尾的斜杠(除了根路径)
|
||||||
|
if (path !== '/' && path.endsWith('/')) {
|
||||||
|
path = path.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试导航
|
||||||
|
this.$router.push(path).catch(err => {
|
||||||
|
// 如果push失败,尝试replace
|
||||||
|
if (err.name !== 'NavigationDuplicated') {
|
||||||
|
this.$router.replace(path).catch(() => {
|
||||||
|
console.error('Navigation to', path, 'failed')
|
||||||
|
// 不显示错误消息,避免打扰用户
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('nav-click', item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mobile-bottom-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 60px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
// 隐藏滚动条但保持滚动功能
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 70px;
|
||||||
|
max-width: 90px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
i, .svg-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #909399;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-placeholder {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
transition: all 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.nav-icon {
|
||||||
|
i, .svg-icon {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端隐藏
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-bottom-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为底部导航预留空间
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-main {
|
||||||
|
padding-bottom: 60px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
188
src/components/MobileButtonGroup/index.vue
Normal file
188
src/components/MobileButtonGroup/index.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-button-group" :class="{ 'sticky': sticky }">
|
||||||
|
<!-- 移动端按钮组 -->
|
||||||
|
<div v-if="isMobile" class="mobile-buttons">
|
||||||
|
<!-- 主要操作按钮 -->
|
||||||
|
<div class="primary-actions" v-if="primaryButtons.length > 0">
|
||||||
|
<el-button
|
||||||
|
v-for="btn in primaryButtons"
|
||||||
|
:key="btn.key || btn.label"
|
||||||
|
:type="btn.type || 'primary'"
|
||||||
|
:size="btn.size || 'medium'"
|
||||||
|
:icon="btn.icon"
|
||||||
|
:disabled="btn.disabled"
|
||||||
|
:loading="btn.loading"
|
||||||
|
@click="handleClick(btn)"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 更多操作下拉菜单 -->
|
||||||
|
<el-dropdown
|
||||||
|
v-if="moreButtons.length > 0"
|
||||||
|
trigger="click"
|
||||||
|
placement="top-end"
|
||||||
|
@command="handleCommand"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
type="default"
|
||||||
|
size="medium"
|
||||||
|
icon="el-icon-more"
|
||||||
|
class="more-btn"
|
||||||
|
>
|
||||||
|
更多
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="btn in moreButtons"
|
||||||
|
:key="btn.key || btn.label"
|
||||||
|
:command="btn.key || btn.label"
|
||||||
|
:disabled="btn.disabled"
|
||||||
|
:divided="btn.divided"
|
||||||
|
>
|
||||||
|
<i :class="btn.icon" v-if="btn.icon"></i>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<div v-else class="desktop-buttons">
|
||||||
|
<slot>
|
||||||
|
<el-button
|
||||||
|
v-for="btn in allButtons"
|
||||||
|
:key="btn.key || btn.label"
|
||||||
|
:type="btn.type || 'default'"
|
||||||
|
:size="btn.size || 'mini'"
|
||||||
|
:icon="btn.icon"
|
||||||
|
:disabled="btn.disabled"
|
||||||
|
:loading="btn.loading"
|
||||||
|
:plain="btn.plain"
|
||||||
|
@click="handleClick(btn)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileButtonGroup',
|
||||||
|
props: {
|
||||||
|
buttons: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
primaryCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
sticky: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
isMobile() {
|
||||||
|
return this.device === 'mobile' || window.innerWidth < 768
|
||||||
|
},
|
||||||
|
allButtons() {
|
||||||
|
return this.buttons || []
|
||||||
|
},
|
||||||
|
primaryButtons() {
|
||||||
|
return this.allButtons.slice(0, this.primaryCount).filter(btn => !btn.hide)
|
||||||
|
},
|
||||||
|
moreButtons() {
|
||||||
|
return this.allButtons.slice(this.primaryCount).filter(btn => !btn.hide)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick(btn) {
|
||||||
|
if (btn.handler) {
|
||||||
|
btn.handler()
|
||||||
|
}
|
||||||
|
this.$emit('button-click', btn)
|
||||||
|
},
|
||||||
|
handleCommand(command) {
|
||||||
|
const btn = this.moreButtons.find(b => (b.key || b.label) === command)
|
||||||
|
if (btn) {
|
||||||
|
this.handleClick(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mobile-button-group {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
margin: -12px -12px 12px -12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
|
||||||
|
.primary-actions {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端隐藏移动端组件
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端隐藏桌面端组件
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
259
src/components/MobileSearchForm/index.vue
Normal file
259
src/components/MobileSearchForm/index.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-search-form" :class="{ 'expanded': expanded }">
|
||||||
|
<!-- 移动端折叠搜索 -->
|
||||||
|
<div v-if="isMobile" class="mobile-search-wrapper">
|
||||||
|
<!-- 搜索按钮栏 -->
|
||||||
|
<div class="search-bar" v-if="!expanded">
|
||||||
|
<el-input
|
||||||
|
v-model="quickSearch"
|
||||||
|
placeholder="快速搜索..."
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuickSearch"
|
||||||
|
@clear="handleQuickSearch"
|
||||||
|
>
|
||||||
|
<el-button slot="append" icon="el-icon-search" @click="handleQuickSearch"></el-button>
|
||||||
|
</el-input>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-setting"
|
||||||
|
class="filter-btn"
|
||||||
|
@click="expanded = true"
|
||||||
|
>
|
||||||
|
筛选
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开的搜索表单 -->
|
||||||
|
<div class="expanded-form" v-if="expanded">
|
||||||
|
<div class="form-header">
|
||||||
|
<span class="form-title">筛选条件</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-close"
|
||||||
|
@click="expanded = false"
|
||||||
|
class="close-btn"
|
||||||
|
></el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<slot name="form" :expanded="expanded">
|
||||||
|
<el-form
|
||||||
|
:model="formData"
|
||||||
|
label-width="80px"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</el-form>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-footer">
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端正常显示 -->
|
||||||
|
<div v-else class="desktop-search-wrapper">
|
||||||
|
<slot name="form" :expanded="false">
|
||||||
|
<el-form
|
||||||
|
:model="formData"
|
||||||
|
:inline="inline"
|
||||||
|
:label-width="labelWidth"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</el-form>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileSearchForm',
|
||||||
|
props: {
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
labelWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '68px'
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
quickSearch: '',
|
||||||
|
formData: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
isMobile() {
|
||||||
|
return this.device === 'mobile' || window.innerWidth < 768
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
model: {
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
handler(val) {
|
||||||
|
this.formData = { ...val }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleQuickSearch() {
|
||||||
|
this.$emit('quick-search', this.quickSearch)
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.$emit('search', this.formData)
|
||||||
|
this.expanded = false
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.$emit('reset')
|
||||||
|
this.formData = {}
|
||||||
|
this.quickSearch = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mobile-search-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-wrapper {
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-form {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
::v-deep .el-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input,
|
||||||
|
.el-select,
|
||||||
|
.el-date-picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-search-wrapper {
|
||||||
|
::v-deep .el-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端隐藏移动端组件
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-search-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端隐藏桌面端组件
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-search-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
403
src/components/MobileTable/index.vue
Normal file
403
src/components/MobileTable/index.vue
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-table-container">
|
||||||
|
<!-- 移动端卡片式列表 -->
|
||||||
|
<div v-if="isMobile" class="mobile-card-list">
|
||||||
|
<div
|
||||||
|
v-for="(row, index) in data"
|
||||||
|
:key="getRowKey(row, index)"
|
||||||
|
class="mobile-card-item"
|
||||||
|
:class="{ 'selected': isSelected(row) }"
|
||||||
|
@click="handleCardClick(row, index)"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<div class="card-header" v-if="showHeader">
|
||||||
|
<div class="card-title">
|
||||||
|
<slot name="header" :row="row" :index="index">
|
||||||
|
{{ getHeaderText(row) }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions" v-if="showSelection || showActions">
|
||||||
|
<el-checkbox
|
||||||
|
v-if="showSelection"
|
||||||
|
:value="isSelected(row)"
|
||||||
|
@click.stop="handleSelect(row)"
|
||||||
|
@change="handleSelectChange(row, $event)"
|
||||||
|
/>
|
||||||
|
<el-dropdown
|
||||||
|
v-if="showActions"
|
||||||
|
trigger="click"
|
||||||
|
@command="(cmd) => handleAction(cmd, row)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span class="action-btn" @click.stop>
|
||||||
|
<i class="el-icon-more"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<slot name="actions" :row="row" :index="index">
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.key"
|
||||||
|
:command="action.key"
|
||||||
|
:disabled="action.disabled && action.disabled(row)"
|
||||||
|
>
|
||||||
|
<i :class="action.icon" v-if="action.icon"></i>
|
||||||
|
{{ action.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</slot>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片内容 -->
|
||||||
|
<div class="card-content">
|
||||||
|
<slot name="content" :row="row" :index="index">
|
||||||
|
<div
|
||||||
|
v-for="column in columns"
|
||||||
|
:key="column.prop || column.key"
|
||||||
|
class="card-field"
|
||||||
|
v-if="column.visible !== false"
|
||||||
|
>
|
||||||
|
<div class="field-label">{{ column.label }}</div>
|
||||||
|
<div class="field-value">
|
||||||
|
<slot
|
||||||
|
:name="`column-${column.prop || column.key}`"
|
||||||
|
:row="row"
|
||||||
|
:column="column"
|
||||||
|
:value="getFieldValue(row, column)"
|
||||||
|
>
|
||||||
|
<dict-tag
|
||||||
|
v-if="column.dictType"
|
||||||
|
:options="getDictOptions(column.dictType)"
|
||||||
|
:value="getFieldValue(row, column)"
|
||||||
|
/>
|
||||||
|
<span v-else-if="column.type === 'switch'">
|
||||||
|
<el-switch
|
||||||
|
:value="getFieldValue(row, column)"
|
||||||
|
:active-value="column.activeValue"
|
||||||
|
:inactive-value="column.inactiveValue"
|
||||||
|
@change="(val) => handleSwitchChange(row, column, val)"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ formatValue(getFieldValue(row, column), column) }}</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部操作 -->
|
||||||
|
<div class="card-footer" v-if="showFooter">
|
||||||
|
<slot name="footer" :row="row" :index="index">
|
||||||
|
<el-button
|
||||||
|
v-for="btn in footerButtons"
|
||||||
|
:key="btn.key"
|
||||||
|
:type="btn.type || 'text'"
|
||||||
|
:size="btn.size || 'small'"
|
||||||
|
:icon="btn.icon"
|
||||||
|
:disabled="btn.disabled && btn.disabled(row)"
|
||||||
|
@click.stop="handleButtonClick(btn, row)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<el-empty
|
||||||
|
v-if="!data || data.length === 0"
|
||||||
|
description="暂无数据"
|
||||||
|
:image-size="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端表格 -->
|
||||||
|
<el-table
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:data="data"
|
||||||
|
v-on="$listeners"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileTable',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
rowKey: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
showSelection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showHeader: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
footerButtons: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selectedRows: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
headerField: {
|
||||||
|
type: String,
|
||||||
|
default: 'name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
isMobile() {
|
||||||
|
return this.device === 'mobile' || window.innerWidth < 768
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRowKey(row, index) {
|
||||||
|
if (typeof this.rowKey === 'function') {
|
||||||
|
return this.rowKey(row, index)
|
||||||
|
}
|
||||||
|
return row[this.rowKey] || index
|
||||||
|
},
|
||||||
|
isSelected(row) {
|
||||||
|
if (!this.showSelection || !this.selectedRows) return false
|
||||||
|
const key = this.getRowKey(row)
|
||||||
|
return this.selectedRows.some(r => this.getRowKey(r) === key)
|
||||||
|
},
|
||||||
|
handleSelect(row) {
|
||||||
|
this.$emit('select', row)
|
||||||
|
},
|
||||||
|
handleSelectChange(row, selected) {
|
||||||
|
this.$emit('selection-change', row, selected)
|
||||||
|
},
|
||||||
|
handleCardClick(row, index) {
|
||||||
|
this.$emit('row-click', row, index)
|
||||||
|
},
|
||||||
|
handleAction(command, row) {
|
||||||
|
this.$emit('action', command, row)
|
||||||
|
},
|
||||||
|
handleButtonClick(btn, row) {
|
||||||
|
if (btn.handler) {
|
||||||
|
btn.handler(row)
|
||||||
|
}
|
||||||
|
this.$emit('button-click', btn.key, row)
|
||||||
|
},
|
||||||
|
getFieldValue(row, column) {
|
||||||
|
if (column.formatter) {
|
||||||
|
return column.formatter(row, column, row[column.prop], index)
|
||||||
|
}
|
||||||
|
if (typeof column.prop === 'function') {
|
||||||
|
return column.prop(row)
|
||||||
|
}
|
||||||
|
return column.prop ? this.getNestedValue(row, column.prop) : ''
|
||||||
|
},
|
||||||
|
getNestedValue(obj, path) {
|
||||||
|
return path.split('.').reduce((o, p) => o && o[p], obj)
|
||||||
|
},
|
||||||
|
formatValue(value, column) {
|
||||||
|
if (value === null || value === undefined) return '-'
|
||||||
|
if (column.formatter) {
|
||||||
|
return column.formatter(null, column, value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
getHeaderText(row) {
|
||||||
|
if (this.headerField) {
|
||||||
|
return this.getNestedValue(row, this.headerField) || '未命名'
|
||||||
|
}
|
||||||
|
return row.name || row.title || '未命名'
|
||||||
|
},
|
||||||
|
getDictOptions(dictType) {
|
||||||
|
// 这里需要根据实际的字典获取方式来实现
|
||||||
|
// 可以通过 this.$store 或 this.$parent 获取字典数据
|
||||||
|
try {
|
||||||
|
const dictData = this.$store?.state?.dict?.dict || {}
|
||||||
|
return dictData[dictType] || []
|
||||||
|
} catch (e) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSwitchChange(row, column, value) {
|
||||||
|
this.$emit('switch-change', row, column, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mobile-table-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card-list {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border: 2px solid #409eff;
|
||||||
|
background: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
.card-field {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
min-width: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
::v-deep .el-tag {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
::v-deep .el-button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端隐藏
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-card-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端隐藏表格
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
::v-deep .el-table {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
:current-page.sync="currentPage"
|
:current-page.sync="currentPage"
|
||||||
:page-size.sync="pageSize"
|
:page-size.sync="pageSize"
|
||||||
:layout="layout"
|
:layout="layout"
|
||||||
:page-sizes="[15, 50, 100, 200,1]"
|
:page-sizes="pageSizes"
|
||||||
:pager-count="pagerCount"
|
:pager-count="pagerCount"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@@ -31,18 +31,18 @@ export default {
|
|||||||
},
|
},
|
||||||
limit: {
|
limit: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 15
|
default: 50
|
||||||
},
|
},
|
||||||
pageSizes: {
|
pageSizes: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [15, 50, 100, 200,1]
|
return [10, 50, 100, 200, 500, 1000]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 移动端页码按钮的数量端默认值5
|
// 移动端页码按钮的数量端默认值5
|
||||||
pagerCount: {
|
pagerCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: document.body.clientWidth < 992 ? 5 : 7
|
default: 7
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -63,8 +63,19 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
isMobile: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.isMobile = window.innerWidth < 768
|
||||||
|
if (this.isMobile && this.layout === 'total, sizes, prev, pager, next, jumper') {
|
||||||
|
this.$emit('update:layout', 'prev, pager, next')
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentPage: {
|
currentPage: {
|
||||||
get() {
|
get() {
|
||||||
@@ -84,6 +95,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleResize() {
|
||||||
|
const wasMobile = this.isMobile
|
||||||
|
this.isMobile = window.innerWidth < 768
|
||||||
|
if (wasMobile !== this.isMobile) {
|
||||||
|
this.$forceUpdate()
|
||||||
|
}
|
||||||
|
},
|
||||||
handleSizeChange(val) {
|
handleSizeChange(val) {
|
||||||
if (this.currentPage * val > this.total) {
|
if (this.currentPage * val > this.total) {
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
@@ -106,6 +124,33 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
::v-deep .el-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.el-pagination__sizes,
|
||||||
|
.el-pagination__total,
|
||||||
|
.el-pagination__jump {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-prev,
|
||||||
|
.btn-next,
|
||||||
|
.el-pager li {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.pagination-container.hidden {
|
.pagination-container.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
167
src/components/PublicFooterNav/index.vue
Normal file
167
src/components/PublicFooterNav/index.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="public-footer-nav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<div
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.path"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: isActive(item.path) }"
|
||||||
|
@click="handleNavClick(item.path)"
|
||||||
|
>
|
||||||
|
<i :class="item.icon"></i>
|
||||||
|
<span class="nav-label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'PublicFooterNav',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
navItems: [
|
||||||
|
{
|
||||||
|
label: '首页',
|
||||||
|
path: '/public/home',
|
||||||
|
icon: 'el-icon-s-home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '评论生成',
|
||||||
|
path: '/tools/comment-gen',
|
||||||
|
icon: 'el-icon-edit-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '订单提交',
|
||||||
|
path: '/public/order-submit',
|
||||||
|
icon: 'el-icon-upload2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '订单搜索',
|
||||||
|
path: '/tools/order-search',
|
||||||
|
icon: 'el-icon-search'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isActive(path) {
|
||||||
|
return this.$route.path === path
|
||||||
|
},
|
||||||
|
handleNavClick(path) {
|
||||||
|
if (this.$route.path !== path) {
|
||||||
|
this.$router.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.public-footer-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 0;
|
||||||
|
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover i {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.nav-container {
|
||||||
|
padding: 0 4px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.nav-container {
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 0 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
498
src/components/PublishDialog.vue
Normal file
498
src/components/PublishDialog.vue
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog title="发品" :visible.sync="internalVisible" width="1200px" :close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="false" append-to-body @close="handleClose">
|
||||||
|
<el-form :model="form" :rules="rules" ref="publishForm" label-width="110px">
|
||||||
|
<el-form-item label="文案版本" v-if="wenanOptions && wenanOptions.length > 0">
|
||||||
|
<el-select v-model="form.wenanIndex" placeholder="选择文案版本" @change="onWenanChange" style="width:100%">
|
||||||
|
<el-option v-for="(opt, idx) in wenanOptions" :key="idx" :label="opt.label" :value="idx" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="闲管家账号" v-if="!hideAppid">
|
||||||
|
<el-select v-model="form.appid" filterable placeholder="选择ERP应用" :loading="erpAccountLoading" @change="onAppidChange">
|
||||||
|
<el-option v-for="a in erpAccountsOptions" :key="a.value" :label="a.label" :value="a.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="form.title" maxlength="34" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文案内容" prop="content">
|
||||||
|
<el-input type="textarea" :rows="6" v-model="form.content" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选择图片" v-if="productImages && productImages.length">
|
||||||
|
<div class="img-grid">
|
||||||
|
<div class="img-item" v-for="(img, idx) in productImages" :key="idx">
|
||||||
|
<img :src="img.url" :alt="`图片${idx+1}`" @click="handlePreviewImage(img.url)" />
|
||||||
|
<div class="img-actions">
|
||||||
|
<el-checkbox v-model="img.selected">使用</el-checkbox>
|
||||||
|
<el-button type="text" size="mini" @click="handleCopyImageUrl(img.url)">复制</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<el-button size="mini" @click="selectAllImages(true)">全选</el-button>
|
||||||
|
<el-button size="mini" @click="selectAllImages(false)">全不选</el-button>
|
||||||
|
<el-button size="mini" @click="invertSelection">反选</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="额外图片链接">
|
||||||
|
<el-input type="textarea" :rows="3" v-model="form.extraImagesText" placeholder="每行一条图片URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="白底图">
|
||||||
|
<el-input v-model="form.whiteImages" placeholder="可选,图片URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务项">
|
||||||
|
<el-select v-model="form.serviceSupport" multiple collapse-tags placeholder="可多选">
|
||||||
|
<el-option v-for="opt in serviceSupportOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="会员名" prop="userName">
|
||||||
|
<el-select v-model="form.userName" filterable placeholder="选择会员名" :loading="userNameLoading">
|
||||||
|
<el-option v-for="u in userNameOptions" :key="u.value" :label="u.label" :value="u.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="省/市/区" required>
|
||||||
|
<div style="display:flex; gap:8px; width:100%">
|
||||||
|
<el-select v-model.number="form.province" placeholder="选择省" style="flex:1" filterable @change="onProvinceChange">
|
||||||
|
<el-option v-for="p in regionOptions.provinces" :key="p.value" :label="p.label" :value="p.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model.number="form.city" placeholder="选择市" style="flex:1" filterable :disabled="!form.province" @change="onCityChange">
|
||||||
|
<el-option v-for="c in regionOptions.cities" :key="c.value" :label="c.label" :value="c.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model.number="form.district" placeholder="选择区" style="flex:1" filterable :disabled="!form.city">
|
||||||
|
<el-option v-for="a in regionOptions.areas" :key="a.value" :label="a.label" :value="a.value" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价格(元)" prop="price">
|
||||||
|
<el-input v-model.number="form.price" type="number" min="0.01" step="0.01" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="原价(元)">
|
||||||
|
<el-input v-model.number="form.originalPrice" type="number" min="0" step="0.01" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="运费(元)" prop="expressFee">
|
||||||
|
<el-input v-model.number="form.expressFee" type="number" min="0" step="0.01" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="库存" prop="stock">
|
||||||
|
<el-input v-model.number="form.stock" type="number" min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商家编码">
|
||||||
|
<el-input v-model="form.outerId" maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品类型" prop="itemBizType">
|
||||||
|
<el-select v-model.number="form.itemBizType" filterable @change="onItemBizTypeChange">
|
||||||
|
<el-option v-for="opt in itemBizTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="行业类型" prop="spBizType">
|
||||||
|
<el-select v-model.number="form.spBizType" filterable @change="onSpBizTypeChange">
|
||||||
|
<el-option v-for="opt in spBizTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类目ID" prop="channelCatId">
|
||||||
|
<el-select v-model="form.channelCatId" filterable placeholder="请选择类目" :disabled="!categoryOptions.length" :loading="categoryLoading" @change="loadProperties">
|
||||||
|
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品属性">
|
||||||
|
<div v-if="pvOptions.length" style="display:flex; flex-direction:column; gap:8px;">
|
||||||
|
<div v-for="(p, pi) in pvOptions" :key="p.propertyId" style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<span style="width:90px; text-align:right; color:#666;">{{ p.propertyName }}:</span>
|
||||||
|
<el-select v-model="selectedPv[p.propertyId]" clearable filterable placeholder="请选择" style="flex:1" @change="onPvChange">
|
||||||
|
<el-option v-for="v in p.values" :key="v.valueId" :label="v.valueName" :value="v.valueId" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="color:#999;">无属性或请选择类型和类目后加载</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="成色">
|
||||||
|
<el-select v-model.number="form.stuffStatus" clearable filterable placeholder="可选">
|
||||||
|
<el-option v-for="opt in stuffStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="属性JSON">
|
||||||
|
<el-input type="textarea" :rows="3" v-model="form.channelPvJson" placeholder='示例: [{"property_id":"p","property_name":"颜色","value_id":"v","value_name":"红"}]' />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-alert
|
||||||
|
v-if="createdProduct"
|
||||||
|
:title="`商品ID:${createdProduct.productId || '-'}`"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin: 10px 0;"
|
||||||
|
>
|
||||||
|
<template slot="description">
|
||||||
|
<div style="display:flex; align-items:center; flex-wrap:wrap; gap:12px;">
|
||||||
|
<span>状态:{{ createdProduct.productStatus || '-' }}</span>
|
||||||
|
<span>商家编码:{{ createdProduct.outerId || '-' }}</span>
|
||||||
|
<el-button type="text" size="mini" @click="copyText(String(createdProduct.productId || ''))">复制ID</el-button>
|
||||||
|
<el-button type="text" size="mini" @click="copyText(String(createdProduct.outerId || ''))">复制商家编码</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="closeDialog">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="submitPublish">发 品</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
:disabled="!createdProduct || !createdProduct.productId"
|
||||||
|
:loading="publishLoading"
|
||||||
|
@click="publishNow"
|
||||||
|
>上 架</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { createProductByPromotion, publishProduct, getProvinces, getCities, getAreas, getCategories, getUsernames, getERPAccounts, getProperties } from "@/api/system/jdorder";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PublishDialog',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
initialData: { type: Object, default: () => ({}) },
|
||||||
|
hideAppid: { type: Boolean, default: false }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
internalVisible: false,
|
||||||
|
loading: false,
|
||||||
|
publishLoading: false,
|
||||||
|
createdProduct: null,
|
||||||
|
wenanOptions: [],
|
||||||
|
productImages: [],
|
||||||
|
form: {
|
||||||
|
appid: '',
|
||||||
|
userName: '',
|
||||||
|
province: null,
|
||||||
|
city: null,
|
||||||
|
district: null,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
wenanIndex: 0,
|
||||||
|
extraImagesText: '',
|
||||||
|
whiteImages: '',
|
||||||
|
serviceSupport: ['NFR'],
|
||||||
|
price: null,
|
||||||
|
originalPrice: null,
|
||||||
|
expressFee: 0,
|
||||||
|
stock: 999,
|
||||||
|
outerId: '',
|
||||||
|
itemBizType: 2,
|
||||||
|
spBizType: 3,
|
||||||
|
channelCatId: '',
|
||||||
|
stuffStatus: 100,
|
||||||
|
channelPvJson: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||||
|
content: [{ required: true, message: '请输入文案内容', trigger: 'blur' }],
|
||||||
|
userName: [{ required: true, message: '请输入闲鱼会员名', trigger: 'blur' }],
|
||||||
|
price: [{ required: true, message: '请输入价格(分)', trigger: 'blur' }],
|
||||||
|
expressFee: [{ required: true, message: '请输入运费(分)', trigger: 'blur' }],
|
||||||
|
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
|
||||||
|
itemBizType: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
|
||||||
|
spBizType: [{ required: true, message: '请选择行业类型', trigger: 'change' }],
|
||||||
|
channelCatId: [{ required: true, message: '请输入类目ID', trigger: 'blur' }]
|
||||||
|
},
|
||||||
|
regionOptions: { provinces: [], cities: [], areas: [] },
|
||||||
|
categoryOptions: [],
|
||||||
|
categoryLoading: false,
|
||||||
|
userNameOptions: [],
|
||||||
|
userNameLoading: false,
|
||||||
|
erpAccountsOptions: [],
|
||||||
|
erpAccountLoading: false,
|
||||||
|
pvOptions: [],
|
||||||
|
selectedPv: {},
|
||||||
|
itemBizTypeOptions: [
|
||||||
|
{ label: '普通商品', value: 2 },
|
||||||
|
{ label: '已验货', value: 0 },
|
||||||
|
{ label: '验货宝', value: 10 },
|
||||||
|
{ label: '闲鱼优品', value: 19 },
|
||||||
|
{ label: '闲鱼特卖', value: 24 },
|
||||||
|
{ label: '品牌捡漏', value: 26 }
|
||||||
|
],
|
||||||
|
spBizTypeOptions: [
|
||||||
|
{ label: '手机', value: 1 },
|
||||||
|
{ label: '时尚', value: 2 },
|
||||||
|
{ label: '家电', value: 3 },
|
||||||
|
{ label: '乐器', value: 8 },
|
||||||
|
{ label: '数码3C', value: 9 },
|
||||||
|
{ label: '奢品', value: 16 },
|
||||||
|
{ label: '母婴', value: 17 },
|
||||||
|
{ label: '美妆', value: 18 },
|
||||||
|
{ label: '珠宝', value: 19 },
|
||||||
|
{ label: '游戏', value: 20 },
|
||||||
|
{ label: '家居', value: 21 },
|
||||||
|
{ label: '虚拟', value: 22 },
|
||||||
|
{ label: '图书', value: 24 },
|
||||||
|
{ label: '食品', value: 27 },
|
||||||
|
{ label: '玩具', value: 28 },
|
||||||
|
{ label: '其他', value: 99 }
|
||||||
|
],
|
||||||
|
stuffStatusOptions: [
|
||||||
|
{ label: '不传', value: null },
|
||||||
|
{ label: '全新', value: 100 },
|
||||||
|
{ label: '99新', value: 99 },
|
||||||
|
{ label: '95新', value: 95 },
|
||||||
|
{ label: '9成新', value: 90 },
|
||||||
|
{ label: '8成新', value: 80 },
|
||||||
|
{ label: '7成新', value: 70 },
|
||||||
|
{ label: '6成新', value: 60 },
|
||||||
|
{ label: '5成新', value: 50 }
|
||||||
|
],
|
||||||
|
serviceSupportOptions: [
|
||||||
|
{ label: '七天无理由退货', value: 'SDR' },
|
||||||
|
{ label: '描述不符包邮退', value: 'NFR' },
|
||||||
|
{ label: '描述不符全额退(虚拟)', value: 'VNR' },
|
||||||
|
{ label: '10分钟极速发货(虚拟)', value: 'FD_10MS' },
|
||||||
|
{ label: '24小时极速发货', value: 'FD_24HS' },
|
||||||
|
{ label: '48小时极速发货', value: 'FD_48HS' },
|
||||||
|
{ label: '正品保障', value: 'FD_GPA' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible: {
|
||||||
|
immediate: true,
|
||||||
|
handler(v) { this.internalVisible = v; if (v) { this.bootstrap(); } }
|
||||||
|
},
|
||||||
|
'form.itemBizType'(val) { this.onItemBizTypeChange(); },
|
||||||
|
'form.spBizType'(val) { this.onSpBizTypeChange(); },
|
||||||
|
'form.appid'(val) { this.onAppidChange(); },
|
||||||
|
'form.channelCatId'(val) { this.loadProperties(); }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyText(text) {
|
||||||
|
const val = String(text || '').trim();
|
||||||
|
if (!val) { this.$modal.msgWarning('无可复制的内容'); return; }
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(val).then(() => { this.$modal.msgSuccess('复制成功'); }).catch(() => { this.fallbackCopy(val); });
|
||||||
|
} else {
|
||||||
|
this.fallbackCopy(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
||||||
|
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||||
|
try { document.execCommand('copy'); this.$modal.msgSuccess('复制成功'); } catch(e){ this.$modal.msgError('复制失败'); }
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
},
|
||||||
|
async bootstrap() {
|
||||||
|
// 初始化表单与文案/图片
|
||||||
|
const d = this.initialData || {};
|
||||||
|
this.wenanOptions = Array.isArray(d.wenanOptions) ? d.wenanOptions : [];
|
||||||
|
this.form.title = d.title || '';
|
||||||
|
this.form.content = d.content || '';
|
||||||
|
this.form.wenanIndex = 0;
|
||||||
|
if (Array.isArray(d.images)) {
|
||||||
|
this.productImages = d.images.map(u => ({ url: u, selected: true }));
|
||||||
|
} else { this.productImages = []; }
|
||||||
|
// 预估原价
|
||||||
|
if (typeof d.originalPrice === 'number') {
|
||||||
|
this.form.originalPrice = d.originalPrice;
|
||||||
|
}
|
||||||
|
// 预设:会员名、省市区
|
||||||
|
if (d.userName) this.form.userName = d.userName;
|
||||||
|
if (d.province) this.form.province = d.province;
|
||||||
|
if (d.city) this.form.city = d.city;
|
||||||
|
if (d.district) this.form.district = d.district;
|
||||||
|
await this.loadProvinces();
|
||||||
|
await this.loadERPAccounts();
|
||||||
|
await this.loadUsernames();
|
||||||
|
await this.loadCategories();
|
||||||
|
this.$nextTick(() => this.$refs.publishForm && this.$refs.publishForm.clearValidate());
|
||||||
|
},
|
||||||
|
onWenanChange(val) {
|
||||||
|
if (this.wenanOptions && this.wenanOptions[val]) {
|
||||||
|
this.form.content = this.wenanOptions[val].content || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectAllImages(flag) { (this.productImages || []).forEach(it => { it.selected = !!flag; }); },
|
||||||
|
invertSelection() { (this.productImages || []).forEach(it => { it.selected = !it.selected; }); },
|
||||||
|
handleCopyImageUrl(imageUrl) { if (navigator.clipboard) { navigator.clipboard.writeText(imageUrl).then(() => { this.$modal.msgSuccess('图片链接复制成功'); }).catch(() => { this.$message.error('复制失败'); }); } },
|
||||||
|
handlePreviewImage(imageUrl) { window.open(imageUrl, '_blank'); },
|
||||||
|
async loadProvinces(echo = true) {
|
||||||
|
try {
|
||||||
|
const res = await getProvinces();
|
||||||
|
if (res.code === 200) this.regionOptions.provinces = res.data || []; else this.$modal.msgError(res.msg || '加载省份失败');
|
||||||
|
} catch(e){ this.$modal.msgError('加载省份失败'); }
|
||||||
|
if (!this.form.province && this.regionOptions.provinces.length) {
|
||||||
|
this.form.province = this.regionOptions.provinces[0].value;
|
||||||
|
}
|
||||||
|
if (this.form.province) { await this.loadCities(this.form.province, true); } else { this.regionOptions.cities = []; this.regionOptions.areas = []; this.form.city = null; this.form.district = null; }
|
||||||
|
},
|
||||||
|
async onProvinceChange() { await this.loadCities(this.form.province, false); },
|
||||||
|
async onCityChange() { await this.loadAreas(this.form.province, this.form.city, false); },
|
||||||
|
async loadCities(provId, echo = false) {
|
||||||
|
if (!provId) { this.regionOptions.cities = []; this.regionOptions.areas = []; this.form.city = null; this.form.district = null; return; }
|
||||||
|
try { const res = await getCities(provId); if (res.code === 200) this.regionOptions.cities = res.data || []; else this.$modal.msgError(res.msg || '加载城市失败'); } catch(e){ this.$modal.msgError('加载城市失败'); }
|
||||||
|
if (!this.form.city && this.regionOptions.cities.length) {
|
||||||
|
this.form.city = this.regionOptions.cities[0].value;
|
||||||
|
}
|
||||||
|
if (this.form.city) { await this.loadAreas(provId, this.form.city, true); } else { this.regionOptions.areas = []; this.form.district = null; }
|
||||||
|
},
|
||||||
|
async loadAreas(provId, cityId, echo = false) {
|
||||||
|
if (!provId || !cityId) { this.regionOptions.areas = []; this.form.district = null; return; }
|
||||||
|
try { const res = await getAreas(provId, cityId); if (res.code === 200) this.regionOptions.areas = res.data || []; else this.$modal.msgError(res.msg || '加载区县失败'); } catch(e){ this.$modal.msgError('加载区县失败'); }
|
||||||
|
if (!this.form.district && this.regionOptions.areas.length) {
|
||||||
|
this.form.district = this.regionOptions.areas[0].value;
|
||||||
|
} else if (!echo) {
|
||||||
|
this.form.district = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadERPAccounts() {
|
||||||
|
this.erpAccountLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getERPAccounts();
|
||||||
|
if (res.code === 200) this.erpAccountsOptions = res.data || []; else this.$modal.msgError(res.msg || '加载应用失败');
|
||||||
|
} catch(e){ this.$modal.msgError('加载应用失败'); }
|
||||||
|
this.erpAccountLoading = false;
|
||||||
|
// 如果隐藏appid选择,则不强制赋值给表单,由后端使用默认账号
|
||||||
|
if (!this.hideAppid && !this.form.appid && this.erpAccountsOptions.length) {
|
||||||
|
this.form.appid = this.erpAccountsOptions[0].value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAppidChange() { this.form.userName = ''; this.loadUsernames(); this.loadCategories(); this.loadProperties(); },
|
||||||
|
async loadUsernames() {
|
||||||
|
this.userNameLoading = true;
|
||||||
|
try { const res = await getUsernames({ pageNum: 1, pageSize: 200, appid: this.form.appid }); if (res.code === 200) this.userNameOptions = res.data || []; else this.$modal.msgError(res.msg || '加载会员名失败'); } catch(e){ this.$modal.msgError('加载会员名失败'); }
|
||||||
|
this.userNameLoading = false; if (!this.form.userName && this.userNameOptions.length) { this.form.userName = this.userNameOptions[0].value; }
|
||||||
|
},
|
||||||
|
async onItemBizTypeChange() { this.categoryOptions = []; this.form.channelCatId = ''; await this.loadCategories(); },
|
||||||
|
async onSpBizTypeChange() { this.categoryOptions = []; this.form.channelCatId = ''; await this.loadCategories(); },
|
||||||
|
async loadCategories() {
|
||||||
|
const itemBizType = this.form.itemBizType; const spBizType = this.form.spBizType; const appid = this.form.appid; if (!itemBizType) return;
|
||||||
|
this.categoryLoading = true;
|
||||||
|
try { const res = await getCategories({ itemBizType, spBizType, appid }); if (res.code === 200) this.categoryOptions = res.data || []; else this.$modal.msgError(res.msg || '加载类目失败'); } catch(e){ this.$modal.msgError('加载类目失败'); }
|
||||||
|
this.categoryLoading = false;
|
||||||
|
if (this.form.channelCatId) { this.loadProperties(); } else if (this.categoryOptions.length) { this.form.channelCatId = this.categoryOptions[0].value; this.loadProperties(); }
|
||||||
|
},
|
||||||
|
async loadProperties() {
|
||||||
|
const f = this.form; if (!f.itemBizType || !f.spBizType || !f.channelCatId) { this.pvOptions = []; this.selectedPv = {}; return; }
|
||||||
|
try {
|
||||||
|
const res = await getProperties({ itemBizType: f.itemBizType, spBizType: f.spBizType, channelCatId: f.channelCatId, appid: f.appid });
|
||||||
|
if (res.code === 200) { this.pvOptions = res.data || []; const keep = { ...this.selectedPv }; this.selectedPv = {}; (this.pvOptions || []).forEach(p => { if (keep[p.propertyId]) this.selectedPv[p.propertyId] = keep[p.propertyId]; }); }
|
||||||
|
else { this.$modal.msgError(res.msg || '加载属性失败'); }
|
||||||
|
} catch(e) { this.$modal.msgError('加载属性失败'); }
|
||||||
|
},
|
||||||
|
onPvChange() {
|
||||||
|
// 属性值变更时的回调(可用于调试或联动逻辑)
|
||||||
|
},
|
||||||
|
submitPublish() {
|
||||||
|
this.$refs.publishForm.validate(valid => {
|
||||||
|
if (!valid) return;
|
||||||
|
const f = this.form;
|
||||||
|
const selectedImages = (this.productImages || []).filter(it => it.selected).map(it => it.url).filter(Boolean);
|
||||||
|
const extraImages = String(f.extraImagesText || '').split(/\n+/).map(s => s.trim()).filter(Boolean);
|
||||||
|
const images = [...selectedImages, ...extraImages];
|
||||||
|
if (!images.length) { this.$modal.msgError('请至少选择或填写一张图片'); return; }
|
||||||
|
let channelPv = undefined;
|
||||||
|
if (f.channelPvJson && f.channelPvJson.trim()) {
|
||||||
|
try { channelPv = JSON.parse(f.channelPvJson); }
|
||||||
|
catch(e) { this.$modal.msgError('属性JSON格式不正确'); return; }
|
||||||
|
} else if (this.selectedPv && Object.keys(this.selectedPv).length) {
|
||||||
|
// 从 pvOptions 中获取完整的属性信息(property_id, property_name, value_id, value_name)
|
||||||
|
channelPv = [];
|
||||||
|
Object.keys(this.selectedPv).forEach(pid => {
|
||||||
|
const valueId = this.selectedPv[pid];
|
||||||
|
if (!valueId) return; // 跳过未选择的属性
|
||||||
|
// 从 pvOptions 中找到对应的属性
|
||||||
|
const property = this.pvOptions.find(p => String(p.propertyId) === String(pid));
|
||||||
|
if (!property) return;
|
||||||
|
// 从属性的 values 中找到对应的值
|
||||||
|
const value = property.values && property.values.find(v => String(v.valueId) === String(valueId));
|
||||||
|
if (!value) return;
|
||||||
|
// 构建完整的4个字段
|
||||||
|
channelPv.push({
|
||||||
|
property_id: pid,
|
||||||
|
property_name: property.propertyName,
|
||||||
|
value_id: valueId,
|
||||||
|
value_name: value.valueName
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (channelPv.length === 0) channelPv = undefined;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
appid: f.appid || undefined,
|
||||||
|
title: f.title,
|
||||||
|
content: f.content,
|
||||||
|
images: images,
|
||||||
|
whiteImages: f.whiteImages || undefined,
|
||||||
|
userName: f.userName,
|
||||||
|
province: f.province,
|
||||||
|
city: f.city,
|
||||||
|
district: f.district,
|
||||||
|
serviceSupport: (f.serviceSupport && f.serviceSupport.length) ? f.serviceSupport.join(',') : undefined,
|
||||||
|
price: cents(f.price),
|
||||||
|
originalPrice: f.originalPrice != null ? cents(f.originalPrice) : undefined,
|
||||||
|
expressFee: cents(f.expressFee),
|
||||||
|
stock: f.stock,
|
||||||
|
outerId: f.outerId || undefined,
|
||||||
|
itemBizType: f.itemBizType,
|
||||||
|
spBizType: f.spBizType,
|
||||||
|
channelCatId: f.channelCatId,
|
||||||
|
stuffStatus: f.stuffStatus || undefined,
|
||||||
|
// 后端字段为下划线命名
|
||||||
|
channel_pv: channelPv
|
||||||
|
};
|
||||||
|
function cents(yuan){ const n = Number(yuan); if (Number.isNaN(n)) return undefined; return Math.round(n*100); }
|
||||||
|
this.loading = true;
|
||||||
|
createProductByPromotion(payload).then(async res => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.code === 200) {
|
||||||
|
try {
|
||||||
|
const outerId = res.data && (res.data.outerId || (res.data.data && res.data.data.outerId));
|
||||||
|
if (outerId) this.$modal.msgSuccess(`发品成功,商家编码:${outerId}`); else this.$modal.msgSuccess('发品提交成功');
|
||||||
|
} catch(e){ this.$modal.msgSuccess('发品提交成功'); }
|
||||||
|
// 记录创建成功的商品,保留弹窗供手动“上架”
|
||||||
|
const productId = this.extractProductId(res.data) || (res.data && (res.data.product_id || (res.data.data && res.data.data.product_id)));
|
||||||
|
const productStatus = res.data && (res.data.product_status || (res.data.data && res.data.data.product_status));
|
||||||
|
const outerId2 = res.data && (res.data.outerId || res.data.outer_id || (res.data.data && (res.data.data.outerId || res.data.data.outer_id)));
|
||||||
|
this.createdProduct = { productId, productStatus, outerId: outerId2 };
|
||||||
|
this.$emit('success', res);
|
||||||
|
} else { this.$modal.msgError(res.msg || '发品失败'); }
|
||||||
|
}).catch(err => { this.loading = false; console.error('发品失败', err); this.$modal.msgError('发品失败,请稍后重试'); });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async publishNow() {
|
||||||
|
if (!this.createdProduct || !this.createdProduct.productId) return;
|
||||||
|
this.publishLoading = true;
|
||||||
|
try {
|
||||||
|
const pubRes = await publishProduct({ productId: this.createdProduct.productId, userName: this.form.userName, appid: this.form.appid });
|
||||||
|
if (pubRes && pubRes.code === 200) {
|
||||||
|
const code = (pubRes.data && pubRes.data.code) ?? pubRes.code;
|
||||||
|
if (code === 0 || code === 200) this.$modal.msgSuccess('上架成功'); else this.$modal.msgWarning('上架已提交或状态未知');
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError((pubRes && pubRes.msg) || '上架失败');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.$modal.msgError('上架失败');
|
||||||
|
}
|
||||||
|
this.publishLoading = false;
|
||||||
|
},
|
||||||
|
extractProductId(resp) {
|
||||||
|
try {
|
||||||
|
if (!resp) return null;
|
||||||
|
if (typeof resp === 'object') {
|
||||||
|
if (resp.productId) return Number(resp.productId);
|
||||||
|
if (resp.data && resp.data.productId) return Number(resp.data.productId);
|
||||||
|
if (resp.data && resp.data.id) return Number(resp.data.id);
|
||||||
|
}
|
||||||
|
} catch (e) { return null; }
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
closeDialog() { this.$emit('update:visible', false); this.internalVisible = false; },
|
||||||
|
handleClose() { this.closeDialog(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.img-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
|
||||||
|
.img-item { border: 1px solid #e5e5e5; border-radius: 4px; padding: 6px; text-align: center; }
|
||||||
|
.img-item img { width: 100%; height: 100px; object-fit: cover; border-radius: 4px; }
|
||||||
|
.img-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 6px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
@@ -151,4 +151,39 @@ export default {
|
|||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
margin: 3px auto;
|
margin: 3px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-right-btn {
|
||||||
|
float: none;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.el-button--mini {
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-dialog {
|
||||||
|
width: 95% !important;
|
||||||
|
margin: 5vh auto !important;
|
||||||
|
|
||||||
|
.el-transfer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,24 +51,50 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
min-height: calc(100vh - 48px - 60px); // 减去头部和底部导航
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main:has(.copyright) {
|
.app-main:has(.copyright) {
|
||||||
padding-bottom: 36px;
|
padding-bottom: 36px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-header + .app-main {
|
.fixed-header + .app-main {
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-top: 48px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasTagsView {
|
.hasTagsView {
|
||||||
.app-main {
|
.app-main {
|
||||||
/* 84 = navbar + tags-view = 50 + 34 */
|
/* 84 = navbar + tags-view = 50 + 34 */
|
||||||
min-height: calc(100vh - 84px);
|
min-height: calc(100vh - 84px);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
min-height: calc(100vh - 48px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-header + .app-main {
|
.fixed-header + .app-main {
|
||||||
padding-top: 84px;
|
padding-top: 84px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-top: 48px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
|
||||||
|
|
||||||
<breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
|
<breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
|
||||||
<top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
|
<top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Breadcrumb from '@/components/Breadcrumb'
|
import Breadcrumb from '@/components/Breadcrumb'
|
||||||
import TopNav from '@/components/TopNav'
|
import TopNav from '@/components/TopNav'
|
||||||
import Hamburger from '@/components/Hamburger'
|
|
||||||
import Screenfull from '@/components/Screenfull'
|
import Screenfull from '@/components/Screenfull'
|
||||||
import SizeSelect from '@/components/SizeSelect'
|
import SizeSelect from '@/components/SizeSelect'
|
||||||
import Search from '@/components/HeaderSearch'
|
import Search from '@/components/HeaderSearch'
|
||||||
@@ -55,7 +52,6 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
TopNav,
|
TopNav,
|
||||||
Hamburger,
|
|
||||||
Screenfull,
|
Screenfull,
|
||||||
SizeSelect,
|
SizeSelect,
|
||||||
Search,
|
Search,
|
||||||
@@ -64,7 +60,6 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'sidebar',
|
|
||||||
'avatar',
|
'avatar',
|
||||||
'device',
|
'device',
|
||||||
'nickName'
|
'nickName'
|
||||||
@@ -81,9 +76,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleSideBar() {
|
|
||||||
this.$store.dispatch('app/toggleSideBar')
|
|
||||||
},
|
|
||||||
setLayout(event) {
|
setLayout(event) {
|
||||||
this.$emit('setLayout')
|
this.$emit('setLayout')
|
||||||
},
|
},
|
||||||
@@ -94,7 +86,7 @@ export default {
|
|||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$store.dispatch('LogOut').then(() => {
|
this.$store.dispatch('LogOut').then(() => {
|
||||||
location.href = '/index'
|
this.$router.push('/login')
|
||||||
})
|
})
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
@@ -110,26 +102,27 @@ export default {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||||
|
|
||||||
.hamburger-container {
|
// 移动端优化
|
||||||
line-height: 46px;
|
@media (max-width: 768px) {
|
||||||
height: 100%;
|
height: 48px;
|
||||||
float: left;
|
padding: 0 5px;
|
||||||
cursor: pointer;
|
|
||||||
transition: background .3s;
|
|
||||||
-webkit-tap-highlight-color:transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, .025)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-container {
|
.breadcrumb-container {
|
||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: none; // 移动端隐藏面包屑
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.topmenu-container {
|
.topmenu-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50px;
|
left: 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.errLog-container {
|
.errLog-container {
|
||||||
@@ -141,19 +134,36 @@ export default {
|
|||||||
float: right;
|
float: right;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
line-height: 48px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu-item {
|
.right-menu-item {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: 44px; // 增大触摸目标
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #5a5e66;
|
color: #5a5e66;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
&.hover-effect {
|
&.hover-effect {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .3s;
|
transition: background .3s;
|
||||||
@@ -161,6 +171,10 @@ export default {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, .025)
|
background: rgba(0, 0, 0, .025)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(0, 0, 0, .05)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,33 +182,71 @@ export default {
|
|||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
min-height: 44px; // 增大触摸目标
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-top: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-nickname{
|
.user-nickname{
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 10px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 60px;
|
||||||
|
display: none; // 移动端隐藏昵称
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon-caret-bottom {
|
.el-icon-caret-bottom {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
|
||||||
right: -20px;
|
|
||||||
top: 25px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: none; // 移动端隐藏设置按钮
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,40 +2,7 @@
|
|||||||
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
|
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
|
||||||
<div class="drawer-container">
|
<div class="drawer-container">
|
||||||
<div>
|
<div>
|
||||||
<div class="setting-drawer-content">
|
<!-- 主题风格设置已完全移除,避免与自定义侧边栏样式冲突 -->
|
||||||
<div class="setting-drawer-title">
|
|
||||||
<h3 class="drawer-title">主题风格设置</h3>
|
|
||||||
</div>
|
|
||||||
<div class="setting-drawer-block-checbox">
|
|
||||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
|
||||||
<img src="@/assets/images/dark.svg" alt="dark">
|
|
||||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
|
|
||||||
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
|
||||||
<img src="@/assets/images/light.svg" alt="light">
|
|
||||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
|
|
||||||
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>主题颜色</span>
|
|
||||||
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider/>
|
|
||||||
|
|
||||||
<h3 class="drawer-title">系统布局配置</h3>
|
<h3 class="drawer-title">系统布局配置</h3>
|
||||||
|
|
||||||
@@ -84,15 +51,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ThemePicker from '@/components/ThemePicker'
|
// import ThemePicker from '@/components/ThemePicker'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ThemePicker },
|
// components: { ThemePicker },
|
||||||
expose: ['openSetting'],
|
expose: ['openSetting'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
theme: this.$store.state.settings.theme,
|
|
||||||
sideTheme: this.$store.state.settings.sideTheme,
|
|
||||||
showSettings: false
|
showSettings: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -181,20 +146,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
themeChange(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'theme',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.theme = val
|
|
||||||
},
|
|
||||||
handleTheme(val) {
|
|
||||||
this.$store.dispatch('settings/changeSetting', {
|
|
||||||
key: 'sideTheme',
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
this.sideTheme = val
|
|
||||||
},
|
|
||||||
openSetting() {
|
openSetting() {
|
||||||
this.showSettings = true
|
this.showSettings = true
|
||||||
},
|
},
|
||||||
@@ -212,9 +163,7 @@ export default {
|
|||||||
"fixedHeader":${this.fixedHeader},
|
"fixedHeader":${this.fixedHeader},
|
||||||
"sidebarLogo":${this.sidebarLogo},
|
"sidebarLogo":${this.sidebarLogo},
|
||||||
"dynamicTitle":${this.dynamicTitle},
|
"dynamicTitle":${this.dynamicTitle},
|
||||||
"footerVisible":${this.footerVisible},
|
"footerVisible":${this.footerVisible}
|
||||||
"sideTheme":"${this.sideTheme}",
|
|
||||||
"theme":"${this.theme}"
|
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
setTimeout(this.$modal.closeLoading(), 1000)
|
setTimeout(this.$modal.closeLoading(), 1000)
|
||||||
@@ -229,48 +178,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.setting-drawer-content {
|
/* 主题风格设置相关样式已移除 */
|
||||||
.setting-drawer-title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: rgba(0, 0, 0, .85);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-drawer-block-checbox {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.setting-drawer-block-checbox-item {
|
|
||||||
position: relative;
|
|
||||||
margin-right: 16px;
|
|
||||||
border-radius: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-drawer-block-checbox-selectIcon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-top: 15px;
|
|
||||||
padding-left: 24px;
|
|
||||||
color: #1890ff;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-container {
|
.drawer-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||||
<transition name="sidebarLogoFade">
|
<transition name="sidebarLogoFade">
|
||||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/tools/comment-gen">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<div class="logo-icon">
|
||||||
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
<i class="el-icon-chat-line-round"></i>
|
||||||
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/tools/comment-gen">
|
||||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
<div class="logo-icon">
|
||||||
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
<i class="el-icon-chat-line-round"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="sidebar-title">{{ title }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,38 +58,96 @@ export default {
|
|||||||
.sidebar-logo-container {
|
.sidebar-logo-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 60px;
|
||||||
line-height: 50px;
|
line-height: 60px;
|
||||||
background: #2b2f3a;
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
& .sidebar-logo-link {
|
& .sidebar-logo-link {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
& .sidebar-logo {
|
& .logo-icon {
|
||||||
width: 32px;
|
display: flex;
|
||||||
height: 32px;
|
align-items: center;
|
||||||
vertical-align: middle;
|
justify-content: center;
|
||||||
margin-right: 12px;
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 2px solid rgba(25, 118, 210, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #1976d2;
|
||||||
|
text-shadow: none;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-color: rgba(25, 118, 210, 0.5);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .sidebar-title {
|
& .sidebar-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #fff;
|
color: #1976d2;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 50px;
|
font-size: 16px;
|
||||||
font-size: 14px;
|
|
||||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
text-shadow: none;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.collapse {
|
&.collapse {
|
||||||
.sidebar-logo {
|
.logo-icon {
|
||||||
margin-right: 0px;
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
<logo v-if="showLogo" :collapse="false" />
|
||||||
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
:collapse="isCollapse"
|
:collapse="false"
|
||||||
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
|
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
|
||||||
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
@@ -48,9 +48,6 @@ export default {
|
|||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return variables
|
return variables
|
||||||
},
|
|
||||||
isCollapse() {
|
|
||||||
return !this.sidebar.opened
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ export default {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #d8dce5;
|
border-bottom: 1px solid #d8dce5;
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none; // 移动端隐藏标签页
|
||||||
|
}
|
||||||
|
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -258,11 +266,30 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
max-width: 120px;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #42b983;
|
background-color: #42b983;
|
||||||
@@ -298,13 +325,39 @@ export default {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #333;
|
color: #333;
|
||||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||||
|
min-width: 120px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 140px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 7px 16px;
|
padding: 7px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
|
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
|
||||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||||
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
|
<sidebar v-if="!sidebar.hide && device !== 'mobile'" class="sidebar-container"/>
|
||||||
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
|
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide, 'mobile-layout': device === 'mobile'}" class="main-container">
|
||||||
<div :class="{'fixed-header':fixedHeader}">
|
<div :class="{'fixed-header':fixedHeader}">
|
||||||
<navbar @setLayout="setLayout"/>
|
<navbar @setLayout="setLayout"/>
|
||||||
<tags-view v-if="needTagsView"/>
|
<tags-view v-if="needTagsView && device !== 'mobile'"/>
|
||||||
</div>
|
</div>
|
||||||
<app-main/>
|
<app-main/>
|
||||||
<settings ref="settingRef"/>
|
<settings ref="settingRef"/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 移动端底部导航:key 随接口路由更新,确保菜单与跳转使用最新路由 -->
|
||||||
|
<mobile-bottom-nav
|
||||||
|
v-if="device === 'mobile'"
|
||||||
|
:key="mobileNavKey"
|
||||||
|
:items="mobileNavItems"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
|
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
|
||||||
|
import MobileBottomNav from '@/components/MobileBottomNav'
|
||||||
import ResizeMixin from './mixin/ResizeHandler'
|
import ResizeMixin from './mixin/ResizeHandler'
|
||||||
import { mapState } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import variables from '@/assets/styles/variables.scss'
|
import variables from '@/assets/styles/variables.scss'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -26,7 +33,8 @@ export default {
|
|||||||
Navbar,
|
Navbar,
|
||||||
Settings,
|
Settings,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
TagsView
|
TagsView,
|
||||||
|
MobileBottomNav
|
||||||
},
|
},
|
||||||
mixins: [ResizeMixin],
|
mixins: [ResizeMixin],
|
||||||
computed: {
|
computed: {
|
||||||
@@ -38,10 +46,22 @@ export default {
|
|||||||
needTagsView: state => state.settings.tagsView,
|
needTagsView: state => state.settings.tagsView,
|
||||||
fixedHeader: state => state.settings.fixedHeader
|
fixedHeader: state => state.settings.fixedHeader
|
||||||
}),
|
}),
|
||||||
|
...mapGetters(['sidebarRouters']),
|
||||||
|
/** 接口路由更新后变化,使底部导航重新渲染并拉取最新菜单,避免点击跳错页 */
|
||||||
|
mobileNavKey() {
|
||||||
|
const routes = this.sidebarRouters || []
|
||||||
|
const len = routes.length
|
||||||
|
const firstPath = (len && routes[0]) ? (routes[0].path || '') : ''
|
||||||
|
return `${len}-${firstPath}`
|
||||||
|
},
|
||||||
|
mobileNavItems() {
|
||||||
|
// 如果返回空数组,组件会使用默认逻辑从路由中自动获取所有可用路由
|
||||||
|
return []
|
||||||
|
},
|
||||||
classObj() {
|
classObj() {
|
||||||
return {
|
return {
|
||||||
hideSidebar: !this.sidebar.opened,
|
hideSidebar: false, // 侧边栏始终展开
|
||||||
openSidebar: this.sidebar.opened,
|
openSidebar: true, // 侧边栏始终展开
|
||||||
withoutAnimation: this.sidebar.withoutAnimation,
|
withoutAnimation: this.sidebar.withoutAnimation,
|
||||||
mobile: this.device === 'mobile'
|
mobile: this.device === 'mobile'
|
||||||
}
|
}
|
||||||
@@ -75,6 +95,18 @@ export default {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
&.mobile {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.openSidebar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-bg {
|
.drawer-bg {
|
||||||
@@ -107,4 +139,43 @@ export default {
|
|||||||
.mobile .fixed-header {
|
.mobile .fixed-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-wrapper {
|
||||||
|
&.mobile {
|
||||||
|
.sidebar-container {
|
||||||
|
display: none; // 移动端完全隐藏侧边栏,使用底部导航
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-bg {
|
||||||
|
display: none; // 移动端不需要遮罩
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
&.mobile-layout {
|
||||||
|
padding-bottom: 60px; // 为底部导航预留空间
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header {
|
||||||
|
width: 100% !important;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端隐藏标签页
|
||||||
|
.hasTagsView {
|
||||||
|
.fixed-header + .app-main {
|
||||||
|
padding-top: 48px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
|
||||||
const { body } = document
|
const { body } = document
|
||||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
const WIDTH = 768 // 移动端断点调整为 768px,更符合移动设备标准
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
14
src/main.js
14
src/main.js
@@ -16,6 +16,7 @@ import { download } from '@/utils/request'
|
|||||||
|
|
||||||
import './assets/icons' // icon
|
import './assets/icons' // icon
|
||||||
import './permission' // permission control
|
import './permission' // permission control
|
||||||
|
import { initMobile } from '@/utils/mobile' // 移动端初始化
|
||||||
import { getDicts } from "@/api/system/dict/data"
|
import { getDicts } from "@/api/system/dict/data"
|
||||||
import { getConfigKey } from "@/api/system/config"
|
import { getConfigKey } from "@/api/system/config"
|
||||||
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"
|
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"
|
||||||
@@ -35,6 +36,11 @@ import ImagePreview from "@/components/ImagePreview"
|
|||||||
import DictTag from '@/components/DictTag'
|
import DictTag from '@/components/DictTag'
|
||||||
// 字典数据组件
|
// 字典数据组件
|
||||||
import DictData from '@/components/DictData'
|
import DictData from '@/components/DictData'
|
||||||
|
// 移动端组件
|
||||||
|
import MobileTable from '@/components/MobileTable'
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
import MobileBottomNav from '@/components/MobileBottomNav'
|
||||||
|
|
||||||
// 全局方法挂载
|
// 全局方法挂载
|
||||||
Vue.prototype.getDicts = getDicts
|
Vue.prototype.getDicts = getDicts
|
||||||
@@ -55,6 +61,11 @@ Vue.component('Editor', Editor)
|
|||||||
Vue.component('FileUpload', FileUpload)
|
Vue.component('FileUpload', FileUpload)
|
||||||
Vue.component('ImageUpload', ImageUpload)
|
Vue.component('ImageUpload', ImageUpload)
|
||||||
Vue.component('ImagePreview', ImagePreview)
|
Vue.component('ImagePreview', ImagePreview)
|
||||||
|
// 移动端组件
|
||||||
|
Vue.component('MobileTable', MobileTable)
|
||||||
|
Vue.component('MobileSearchForm', MobileSearchForm)
|
||||||
|
Vue.component('MobileButtonGroup', MobileButtonGroup)
|
||||||
|
Vue.component('MobileBottomNav', MobileBottomNav)
|
||||||
|
|
||||||
Vue.use(directive)
|
Vue.use(directive)
|
||||||
Vue.use(plugins)
|
Vue.use(plugins)
|
||||||
@@ -75,6 +86,9 @@ Vue.use(Element, {
|
|||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// 初始化移动端优化
|
||||||
|
initMobile()
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
router,
|
router,
|
||||||
|
|||||||
91
src/mixins/mobile.js
Normal file
91
src/mixins/mobile.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 移动端混入
|
||||||
|
*/
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { isMobile, isIOS, isAndroid, getDeviceType } from '@/utils/mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
$isMobile() {
|
||||||
|
return this.device === 'mobile' || isMobile()
|
||||||
|
},
|
||||||
|
$isIOS() {
|
||||||
|
return isIOS()
|
||||||
|
},
|
||||||
|
$isAndroid() {
|
||||||
|
return isAndroid()
|
||||||
|
},
|
||||||
|
$deviceType() {
|
||||||
|
return getDeviceType()
|
||||||
|
},
|
||||||
|
$isTablet() {
|
||||||
|
return window.innerWidth >= 768 && window.innerWidth < 1024
|
||||||
|
},
|
||||||
|
$isPhone() {
|
||||||
|
return window.innerWidth < 768
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 移动端提示
|
||||||
|
*/
|
||||||
|
$mobileToast(message, type = 'info') {
|
||||||
|
if (this.$isMobile) {
|
||||||
|
this.$message({
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
duration: 2000,
|
||||||
|
showClose: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 移动端确认对话框
|
||||||
|
*/
|
||||||
|
$mobileConfirm(message, title = '提示', options = {}) {
|
||||||
|
return this.$confirm(message, title, {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 移动端表格列过滤
|
||||||
|
*/
|
||||||
|
$filterMobileColumns(columns) {
|
||||||
|
if (this.$isMobile) {
|
||||||
|
// 移动端只显示重要列
|
||||||
|
return columns.filter(col => {
|
||||||
|
if (col.mobile === false) return false
|
||||||
|
if (col.mobile === true) return true
|
||||||
|
// 默认显示前5列
|
||||||
|
return columns.indexOf(col) < 5
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return columns
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 格式化移动端显示值
|
||||||
|
*/
|
||||||
|
$formatMobileValue(value, column) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
if (column && column.formatter) {
|
||||||
|
return column.formatter(null, column, value)
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { isRelogin } from '@/utils/request'
|
|||||||
|
|
||||||
NProgress.configure({ showSpinner: false })
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
const whiteList = ['/login', '/register']
|
const whiteList = ['/login', '/register', '/public/home', '/tools/comment-gen', '/tools/order-search', '/public/order-submit', '/kdocs-callback', '/tendoc-callback']
|
||||||
|
|
||||||
const isWhiteList = (path) => {
|
const isWhiteList = (path) => {
|
||||||
return whiteList.some(pattern => isPathMatch(pattern, path))
|
return whiteList.some(pattern => isPathMatch(pattern, path))
|
||||||
@@ -21,7 +21,7 @@ router.beforeEach((to, from, next) => {
|
|||||||
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
||||||
/* has token*/
|
/* has token*/
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
next({ path: '/' })
|
next({ path: '/user/profile' })
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
} else if (isWhiteList(to.path)) {
|
} else if (isWhiteList(to.path)) {
|
||||||
next()
|
next()
|
||||||
@@ -39,7 +39,7 @@ router.beforeEach((to, from, next) => {
|
|||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
store.dispatch('LogOut').then(() => {
|
store.dispatch('LogOut').then(() => {
|
||||||
Message.error(err)
|
Message.error(err)
|
||||||
next({ path: '/' })
|
next({ path: '/user/profile' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import auth from './auth'
|
|||||||
import cache from './cache'
|
import cache from './cache'
|
||||||
import modal from './modal'
|
import modal from './modal'
|
||||||
import download from './download'
|
import download from './download'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
@@ -16,5 +17,7 @@ export default {
|
|||||||
Vue.prototype.$modal = modal
|
Vue.prototype.$modal = modal
|
||||||
// 下载文件
|
// 下载文件
|
||||||
Vue.prototype.$download = download
|
Vue.prototype.$download = download
|
||||||
|
// 全局请求实例(供 this.$axios 使用)
|
||||||
|
Vue.prototype.$axios = request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import Layout from '@/layout'
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 公共路由
|
// 公共路由(无需权限即可访问)
|
||||||
export const constantRoutes = [
|
export const constantRoutes = [
|
||||||
{
|
{
|
||||||
path: '/redirect',
|
path: '/redirect',
|
||||||
@@ -62,17 +62,8 @@ export const constantRoutes = [
|
|||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
component: Layout,
|
redirect: '/sloworder/index'
|
||||||
redirect: 'order/list',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'order/list',
|
|
||||||
component: () => import('@/views/system/orderrows/index'),
|
|
||||||
name: 'OrderList',
|
|
||||||
meta: { title: '京粉订单', icon: 'order', affix: true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: '/user',
|
||||||
@@ -88,13 +79,58 @@ export const constantRoutes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// 公共工具首页
|
||||||
|
{
|
||||||
|
path: '/public/home',
|
||||||
|
component: () => import('@/views/public/PublicHome'),
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
// 评论生成工具(内部使用,不易被发现的路径)
|
||||||
|
{
|
||||||
|
path: '/tools/comment-gen',
|
||||||
|
component: () => import('@/views/public/CommentGenerator'),
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
// 订单搜索工具(内部使用,不易被发现的路径)
|
||||||
|
{
|
||||||
|
path: '/tools/order-search',
|
||||||
|
component: () => import('@/views/public/OrderSearch'),
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
// 公开订单提交页(不使用 Layout,无侧边栏)
|
||||||
|
{
|
||||||
|
path: '/public/order-submit',
|
||||||
|
component: () => import('@/views/public/order-submit/index'),
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
// 慢单管理(移到公共路由,无需权限)
|
||||||
|
{
|
||||||
|
path: '/sloworder',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'SlowOrder',
|
||||||
|
meta: { title: '下好的慢单', icon: 'list' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/jdorder/orderList'),
|
||||||
|
name: 'SlowOrderIndex',
|
||||||
|
meta: { title: '下好的慢单', icon: 'list' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 动态路由,基于用户权限动态去加载
|
||||||
|
export const dynamicRoutes = [
|
||||||
|
// 京粉订单管理
|
||||||
{
|
{
|
||||||
path: '/order',
|
path: '/order',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: 'list',
|
redirect: 'list',
|
||||||
name: 'Order',
|
name: 'Order',
|
||||||
meta: { title: '京粉订单', icon: 'shopping' },
|
meta: { title: '京粉订单', icon: 'money' },
|
||||||
|
permissions: ['jdorder:order:list'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'list',
|
path: 'list',
|
||||||
@@ -106,53 +142,26 @@ export const constantRoutes = [
|
|||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
component: () => import('@/views/system/orderrows/statistics'),
|
component: () => import('@/views/system/orderrows/statistics'),
|
||||||
name: 'OrderStatistics',
|
name: 'OrderStatistics',
|
||||||
meta: { title: '订单统计', icon: 'chart' }
|
meta: { title: '订单统计', icon: 'chart' },
|
||||||
|
permissions: ['jdorder:order:statistics']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: () => import('@/views/system/orderrows/settings'),
|
component: () => import('@/views/system/orderrows/settings'),
|
||||||
name: 'OrderSettings',
|
name: 'OrderSettings',
|
||||||
meta: { title: '订单设置', icon: 'setting' }
|
meta: { title: '订单设置', icon: 'setting' },
|
||||||
}
|
permissions: ['jdorder:order:settings']
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: '/message',
|
|
||||||
component: Layout,
|
|
||||||
redirect: 'noredirect',
|
|
||||||
name: 'Message',
|
|
||||||
meta: { title: '线报消息', icon: 'message' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'index',
|
|
||||||
component: () => import('@/views/system/xbmessage/index'),
|
|
||||||
name: 'MessageIndex',
|
|
||||||
meta: { title: '线报消息', icon: 'list' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/xbgroup',
|
|
||||||
component: Layout,
|
|
||||||
redirect: 'noredirect',
|
|
||||||
name: 'Xbgroup',
|
|
||||||
meta: { title: '线报群管理', icon: 'peoples' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'index',
|
|
||||||
component: () => import('@/views/system/xbgroup/index'),
|
|
||||||
name: 'XbgroupIndex',
|
|
||||||
meta: { title: '线报群列表', icon: 'list' }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// 一键转链工具
|
||||||
{
|
{
|
||||||
path: '/jdorder',
|
path: '/jdorder',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: 'noredirect',
|
redirect: 'noredirect',
|
||||||
name: 'Jdorder',
|
name: 'Jdorder',
|
||||||
meta: { title: '一键转链', icon: 'link' },
|
meta: { title: '一键转链', icon: 'link' },
|
||||||
|
permissions: ['jdorder:convert:list'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'index',
|
path: 'index',
|
||||||
@@ -162,25 +171,147 @@ export const constantRoutes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// 京东指令台
|
||||||
|
{
|
||||||
|
path: '/jd-instruction',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'JdInstruction',
|
||||||
|
meta: { title: '京东指令台', icon: 'guide' },
|
||||||
|
permissions: ['jdorder:instruction:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/jd-instruction/index'),
|
||||||
|
name: 'JdInstructionIndex',
|
||||||
|
meta: { title: '指令执行', icon: 'form' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 常用商品管理
|
||||||
|
{
|
||||||
|
path: '/favorite',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'Favorite',
|
||||||
|
meta: { title: '常用商品', icon: 'star' },
|
||||||
|
permissions: ['jdorder:favorite:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/favoriteProduct/index'),
|
||||||
|
name: 'FavoriteIndex',
|
||||||
|
meta: { title: '常用商品', icon: 'shopping' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 线报消息管理
|
||||||
|
{
|
||||||
|
path: '/message',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'Message',
|
||||||
|
meta: { title: '线报消息', icon: 'message' },
|
||||||
|
permissions: ['jdorder:message:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/xbmessage/index'),
|
||||||
|
name: 'MessageIndex',
|
||||||
|
meta: { title: '线报消息', icon: 'wechat' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 批量发品
|
||||||
|
{
|
||||||
|
path: '/batchPublish',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'BatchPublish',
|
||||||
|
meta: { title: '批量发品', icon: 'shopping' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/jarvis/batchPublish/index'),
|
||||||
|
name: 'BatchPublishIndex',
|
||||||
|
meta: { title: '批量发品', icon: 'upload' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 文档同步配置
|
||||||
|
{
|
||||||
|
path: '/docSync',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/docSync/index',
|
||||||
|
name: 'DocSync',
|
||||||
|
meta: { title: '文档同步配置', icon: 'document' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/jarvis/docSync/index'),
|
||||||
|
name: 'DocSyncIndex',
|
||||||
|
meta: { title: '文档同步配置', icon: 'document' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'kdocs',
|
||||||
|
component: () => import('@/views/jarvis/kdocs/index'),
|
||||||
|
name: 'KdocsCloudManage',
|
||||||
|
meta: { title: '金山文档 在线表格', icon: 'document' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 线报群管理
|
||||||
|
{
|
||||||
|
path: '/xbgroup',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'Xbgroup',
|
||||||
|
meta: { title: '线报群管理', icon: 'peoples' },
|
||||||
|
permissions: ['jdorder:xbgroup:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/xbgroup/index'),
|
||||||
|
name: 'XbgroupIndex',
|
||||||
|
meta: { title: '线报群列表', icon: 'peoples' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 礼金管理
|
||||||
|
{
|
||||||
|
path: '/giftcoupon',
|
||||||
|
component: Layout,
|
||||||
|
redirect: 'noredirect',
|
||||||
|
name: 'GiftCoupon',
|
||||||
|
meta: { title: '礼金管理', icon: 'money' },
|
||||||
|
permissions: ['system:giftcoupon:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
component: () => import('@/views/system/giftcoupon/index'),
|
||||||
|
name: 'GiftCouponIndex',
|
||||||
|
meta: { title: '礼金列表', icon: 'gift' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 系统管理
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: 'noredirect',
|
redirect: 'noredirect',
|
||||||
name: 'System',
|
name: 'System',
|
||||||
meta: { title: '系统管理', icon: 'system' },
|
meta: { title: '系统管理', icon: 'system' },
|
||||||
|
permissions: ['system:admin:list'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'superadmin',
|
path: 'superadmin',
|
||||||
component: () => import('@/views/system/superadmin/index'),
|
component: () => import('@/views/system/superadmin/index'),
|
||||||
name: 'Superadmin',
|
name: 'Superadmin',
|
||||||
meta: { title: '超级管理员', icon: 'user' }
|
meta: { title: '超级管理员', icon: 'people' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
// 原有的系统路由
|
||||||
|
|
||||||
// 动态路由,基于用户权限动态去加载
|
|
||||||
export const dynamicRoutes = [
|
|
||||||
{
|
{
|
||||||
path: '/system/user-auth',
|
path: '/system/user-auth',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -237,6 +368,19 @@ export const dynamicRoutes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/monitor/logfile',
|
||||||
|
component: Layout,
|
||||||
|
permissions: ['monitor:server:list'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/views/monitor/logfile/index'),
|
||||||
|
name: 'Logfile',
|
||||||
|
meta: { title: '日志文件', icon: 'documentation' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tool/gen-edit',
|
path: '/tool/gen-edit',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const getters = {
|
|||||||
permission_routes: state => state.permission.routes,
|
permission_routes: state => state.permission.routes,
|
||||||
topbarRouters: state => state.permission.topbarRouters,
|
topbarRouters: state => state.permission.topbarRouters,
|
||||||
defaultRoutes: state => state.permission.defaultRoutes,
|
defaultRoutes: state => state.permission.defaultRoutes,
|
||||||
sidebarRouters: state => state.permission.sidebarRouters
|
sidebarRouters: state => state.permission.sidebarRouters,
|
||||||
|
favoriteProductRefreshKey: state => state.app.favoriteProductRefreshKey
|
||||||
}
|
}
|
||||||
export default getters
|
export default getters
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const state = {
|
|||||||
hide: false
|
hide: false
|
||||||
},
|
},
|
||||||
device: 'desktop',
|
device: 'desktop',
|
||||||
size: Cookies.get('size') || 'medium'
|
size: Cookies.get('size') || 'medium',
|
||||||
|
// 全局刷新标记:常用商品列表
|
||||||
|
favoriteProductRefreshKey: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -37,6 +39,9 @@ const mutations = {
|
|||||||
},
|
},
|
||||||
SET_SIDEBAR_HIDE: (state, status) => {
|
SET_SIDEBAR_HIDE: (state, status) => {
|
||||||
state.sidebar.hide = status
|
state.sidebar.hide = status
|
||||||
|
},
|
||||||
|
INCREMENT_FAVORITE_PRODUCT_REFRESH_KEY: (state) => {
|
||||||
|
state.favoriteProductRefreshKey++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,9 @@ const actions = {
|
|||||||
},
|
},
|
||||||
toggleSideBarHide({ commit }, status) {
|
toggleSideBarHide({ commit }, status) {
|
||||||
commit('SET_SIDEBAR_HIDE', status)
|
commit('SET_SIDEBAR_HIDE', status)
|
||||||
|
},
|
||||||
|
triggerFavoriteProductRefresh({ commit }) {
|
||||||
|
commit('INCREMENT_FAVORITE_PRODUCT_REFRESH_KEY')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
230
src/utils/mobile.js
Normal file
230
src/utils/mobile.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* 移动端工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为移动设备
|
||||||
|
*/
|
||||||
|
export function isMobile() {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||||
|
window.innerWidth < 768
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为iOS设备
|
||||||
|
*/
|
||||||
|
export function isIOS() {
|
||||||
|
return /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为Android设备
|
||||||
|
*/
|
||||||
|
export function isAndroid() {
|
||||||
|
return /Android/.test(navigator.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备类型
|
||||||
|
*/
|
||||||
|
export function getDeviceType() {
|
||||||
|
if (isMobile()) {
|
||||||
|
if (isIOS()) return 'ios'
|
||||||
|
if (isAndroid()) return 'android'
|
||||||
|
return 'mobile'
|
||||||
|
}
|
||||||
|
return 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置移动端视口
|
||||||
|
*/
|
||||||
|
export function setMobileViewport() {
|
||||||
|
if (isMobile()) {
|
||||||
|
const viewport = document.querySelector('meta[name="viewport"]')
|
||||||
|
if (viewport) {
|
||||||
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=5, minimum-scale=1, user-scalable=yes, viewport-fit=cover')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防止iOS双击缩放
|
||||||
|
*/
|
||||||
|
export function preventDoubleTapZoom() {
|
||||||
|
if (isIOS()) {
|
||||||
|
let lastTouchEnd = 0
|
||||||
|
document.addEventListener('touchend', (event) => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastTouchEnd <= 300) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
lastTouchEnd = now
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化移动端滚动
|
||||||
|
*/
|
||||||
|
export function optimizeMobileScroll() {
|
||||||
|
if (isMobile()) {
|
||||||
|
// 添加平滑滚动
|
||||||
|
document.documentElement.style.scrollBehavior = 'smooth'
|
||||||
|
|
||||||
|
// 优化触摸滚动
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = `
|
||||||
|
* {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置安全区域适配(iOS刘海屏)
|
||||||
|
*/
|
||||||
|
export function setSafeArea() {
|
||||||
|
if (isIOS()) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = `
|
||||||
|
.safe-area-top {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
.safe-area-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
.safe-area-left {
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
.safe-area-right {
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端初始化
|
||||||
|
*/
|
||||||
|
export function initMobile() {
|
||||||
|
setMobileViewport()
|
||||||
|
preventDoubleTapZoom()
|
||||||
|
optimizeMobileScroll()
|
||||||
|
setSafeArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化移动端表格数据为卡片数据
|
||||||
|
*/
|
||||||
|
export function formatTableToCards(tableData, columns) {
|
||||||
|
return tableData.map(row => {
|
||||||
|
const card = {}
|
||||||
|
columns.forEach(column => {
|
||||||
|
if (column.visible !== false) {
|
||||||
|
const value = column.prop ? getNestedValue(row, column.prop) : ''
|
||||||
|
card[column.prop || column.key] = {
|
||||||
|
label: column.label,
|
||||||
|
value: column.formatter ? column.formatter(row, column, value) : value,
|
||||||
|
type: column.type || 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
_cardData: card
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取嵌套对象值
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj, path) {
|
||||||
|
return path.split('.').reduce((o, p) => o && o[p], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端表格列配置
|
||||||
|
*/
|
||||||
|
export function getMobileTableColumns(columns) {
|
||||||
|
return columns
|
||||||
|
.filter(col => col.visible !== false)
|
||||||
|
.map(col => ({
|
||||||
|
...col,
|
||||||
|
label: col.label || col.prop,
|
||||||
|
prop: col.prop || col.key
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
*/
|
||||||
|
export function debounce(func, wait) {
|
||||||
|
let timeout
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
func(...args)
|
||||||
|
}
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(later, wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
*/
|
||||||
|
export function throttle(func, limit) {
|
||||||
|
let inThrottle
|
||||||
|
return function(...args) {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func.apply(this, args)
|
||||||
|
inThrottle = true
|
||||||
|
setTimeout(() => inThrottle = false, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端图片懒加载
|
||||||
|
*/
|
||||||
|
export function lazyLoadImages() {
|
||||||
|
if ('IntersectionObserver' in window) {
|
||||||
|
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const img = entry.target
|
||||||
|
img.src = img.dataset.src
|
||||||
|
img.classList.remove('lazy')
|
||||||
|
imageObserver.unobserve(img)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelectorAll('img.lazy').forEach(img => {
|
||||||
|
imageObserver.observe(img)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isMobile,
|
||||||
|
isIOS,
|
||||||
|
isAndroid,
|
||||||
|
getDeviceType,
|
||||||
|
setMobileViewport,
|
||||||
|
preventDoubleTapZoom,
|
||||||
|
optimizeMobileScroll,
|
||||||
|
setSafeArea,
|
||||||
|
initMobile,
|
||||||
|
formatTableToCards,
|
||||||
|
getMobileTableColumns,
|
||||||
|
debounce,
|
||||||
|
throttle,
|
||||||
|
lazyLoadImages
|
||||||
|
}
|
||||||
|
|
||||||
97
src/utils/publishHelper.js
Normal file
97
src/utils/publishHelper.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { addToFavorites, getBySkuid } from "@/api/system/favoriteProduct";
|
||||||
|
|
||||||
|
function parseMaybeJson(str) {
|
||||||
|
if (!str || typeof str !== 'string') return null;
|
||||||
|
try { return JSON.parse(str); } catch(e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNested(obj, path) {
|
||||||
|
try {
|
||||||
|
if (!obj) return null;
|
||||||
|
const segs = path.split('.');
|
||||||
|
let cur = obj;
|
||||||
|
for (const k of segs) {
|
||||||
|
if (cur && typeof cur === 'object' && k in cur) cur = cur[k]; else return null;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
} catch(e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePublishResponse(res) {
|
||||||
|
try {
|
||||||
|
const d = res && res.data ? res.data : res;
|
||||||
|
const productId = d?.product_id ?? d?.productId ?? d?.data?.product_id ?? d?.data?.productId ?? null;
|
||||||
|
const productStatus = d?.product_status ?? d?.productStatus ?? d?.data?.product_status ?? d?.data?.productStatus ?? null;
|
||||||
|
const outerId = d?.outer_id ?? d?.outerId ?? d?.data?.outer_id ?? d?.data?.outerId ?? null;
|
||||||
|
return { productId, productStatus, outerId };
|
||||||
|
} catch(e) { return { productId: null, productStatus: null, outerId: null }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFavoriteFromTransfer(product, pub) {
|
||||||
|
const spuid = product?.spuid || product?.skuId || product?.skuid || '';
|
||||||
|
return {
|
||||||
|
skuid: spuid,
|
||||||
|
productName: product?.skuName || product?.title || '',
|
||||||
|
shopName: product?.shopName || '',
|
||||||
|
productUrl: product?.url || '',
|
||||||
|
productImage: Array.isArray(product?.images) && product.images.length ? product.images[0] : '',
|
||||||
|
price: (product?.price != null ? String(product.price) : (product?.lowestCouponPrice != null ? String(product.lowestCouponPrice) : '')),
|
||||||
|
commissionInfo: product?.commissionShare ? `${product.commissionShare}%` : (product?.commission != null ? String(product.commission) : ''),
|
||||||
|
remark: `自动添加 - 发品时间: ${new Date().toLocaleString()}${pub.outerId ? `, 商家编码: ${pub.outerId}` : ''}`,
|
||||||
|
productId: pub.productId,
|
||||||
|
productStatus: pub.productStatus
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFavoriteFromXb(child, pub) {
|
||||||
|
const jq = parseMaybeJson(child?.jsonQueryResult);
|
||||||
|
const spuid = (jq && (jq.spuid || getNested(jq, 'spuid'))) || child?.spuid || child?.skuid || '';
|
||||||
|
const shopName = getNested(jq, 'shopInfo.shopName') || child?.shopName || '';
|
||||||
|
const shopId = getNested(jq, 'shopInfo.shopId') || child?.shopId || '';
|
||||||
|
const productUrl = jq?.materialUrl || jq?.url || child?.materialUrl || child?.productUrl || '';
|
||||||
|
const productImage = getNested(jq, 'imageInfo.mainImage') || child?.productImage || '';
|
||||||
|
const price = child?.firstPrice || getNested(jq, 'priceInfo.lowestCouponPrice') || getNested(jq, 'priceInfo.price') || '';
|
||||||
|
const commissionInfo = getNested(jq, 'commissionInfo.commissionShare') ? `${getNested(jq, 'commissionInfo.commissionShare')}%` : (getNested(jq, 'commissionInfo.commission') || '');
|
||||||
|
return {
|
||||||
|
skuid: spuid,
|
||||||
|
productName: child?.skuName || child?.productName || '',
|
||||||
|
shopName,
|
||||||
|
shopId,
|
||||||
|
productUrl,
|
||||||
|
productImage,
|
||||||
|
price,
|
||||||
|
commissionInfo,
|
||||||
|
productId: pub.productId,
|
||||||
|
productStatus: pub.productStatus,
|
||||||
|
remark: `自动添加 - 发品时间: ${new Date().toLocaleString()}${pub.outerId ? `, 商家编码: ${pub.outerId}` : ''}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addToFavoritesAfterPublishFromTransfer(product, res) {
|
||||||
|
const pub = parsePublishResponse(res);
|
||||||
|
const spuid = product?.spuid || product?.skuId || product?.skuid || '';
|
||||||
|
if (!spuid) return { success: false, reason: 'no_spuid' };
|
||||||
|
try {
|
||||||
|
const exist = await getBySkuid(spuid);
|
||||||
|
if (exist && exist.data) return { success: true, skipped: true };
|
||||||
|
} catch(e) {}
|
||||||
|
const payload = buildFavoriteFromTransfer(product, pub);
|
||||||
|
const addRes = await addToFavorites(payload);
|
||||||
|
return { success: addRes && addRes.code === 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addToFavoritesAfterPublishFromXb(child, res) {
|
||||||
|
const pub = parsePublishResponse(res);
|
||||||
|
const jq = parseMaybeJson(child?.jsonQueryResult);
|
||||||
|
const spuid = (jq && (jq.spuid || getNested(jq, 'spuid'))) || child?.spuid || child?.skuid || '';
|
||||||
|
if (!spuid) return { success: false, reason: 'no_spuid' };
|
||||||
|
try {
|
||||||
|
const exist = await getBySkuid(spuid);
|
||||||
|
if (exist && exist.data) return { success: true, skipped: true };
|
||||||
|
} catch(e) {}
|
||||||
|
const payload = buildFavoriteFromXb(child, pub);
|
||||||
|
const addRes = await addToFavorites(payload);
|
||||||
|
return { success: addRes && addRes.code === 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1523
src/views/jarvis/batchPublish/index.vue
Normal file
1523
src/views/jarvis/batchPublish/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
445
src/views/jarvis/docSync/components/KdocsCloudConfig.vue
Normal file
445
src/views/jarvis/docSync/components/KdocsCloudConfig.vue
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
<template>
|
||||||
|
<div class="kdocs-config">
|
||||||
|
<div class="config-container">
|
||||||
|
<div class="config-left">
|
||||||
|
<el-card class="config-section">
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<i class="el-icon-key"></i>
|
||||||
|
<span>授权状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-status">
|
||||||
|
<el-tag v-if="isAuthorized" type="success" size="medium">
|
||||||
|
<i class="el-icon-circle-check"></i> 已授权
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="medium">
|
||||||
|
<i class="el-icon-circle-close"></i> 未授权
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-if="!isAuthorized"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-unlock"
|
||||||
|
@click="handleAuthorize"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>立即授权</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleRefreshAuth"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>刷新状态</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isAuthorized && tokenInfo" class="token-info" style="margin-top: 15px;">
|
||||||
|
<el-descriptions :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="用户ID">{{ tokenInfo.userId || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Token状态">
|
||||||
|
<el-tag v-if="tokenInfo.isValid" type="success" size="small">有效</el-tag>
|
||||||
|
<el-tag v-else type="warning" size="small">已过期</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="tokenInfo.expiresIn" label="有效期">
|
||||||
|
{{ Math.floor(tokenInfo.expiresIn / 60) }} 分钟
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="config-section">
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span>H-TF订单自动写入配置</span>
|
||||||
|
<el-tag v-if="config.isConfigured" type="success" size="mini" style="margin-left: 10px;">已配置</el-tag>
|
||||||
|
<el-tag v-else type="warning" size="mini" style="margin-left: 10px;">未配置</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
|
||||||
|
<el-form-item label="file_token" prop="fileId">
|
||||||
|
<el-input
|
||||||
|
v-model="form.fileId"
|
||||||
|
placeholder="个人云文档列表接口返回的 file_token(文档 open_id)"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
slot="append"
|
||||||
|
icon="el-icon-search"
|
||||||
|
:disabled="!form.fileId || !isAuthorized"
|
||||||
|
@click="handleTestRead"
|
||||||
|
>测试读取</el-button>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="工作表 sheet_idx">
|
||||||
|
<el-input-number v-model="form.sheetIdx" :min="0" controls-position="right" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="表头行号" prop="headerRow">
|
||||||
|
<el-input-number v-model="form.headerRow" :min="1" controls-position="right" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="起始行号" prop="startRow">
|
||||||
|
<el-input-number v-model="form.startRow" :min="1" controls-position="right" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="saveLoading" icon="el-icon-check" @click="handleSave">保存配置</el-button>
|
||||||
|
<el-button :loading="testLoading" icon="el-icon-setting" @click="handleTest">测试配置</el-button>
|
||||||
|
<el-button type="danger" plain :loading="clearLoading" icon="el-icon-delete" @click="handleClear">清除配置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-right">
|
||||||
|
<el-card class="status-card-wrapper">
|
||||||
|
<div class="status-card" :class="config.isConfigured ? 'success' : 'warning'">
|
||||||
|
<div class="status-icon" :class="config.isConfigured ? 'success' : 'warning'">
|
||||||
|
<i :class="config.isConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-text">
|
||||||
|
<div class="status-title">{{ config.isConfigured ? '配置完成' : '配置未完成' }}</div>
|
||||||
|
<div class="status-desc">
|
||||||
|
{{ config.hint || (config.isConfigured ? 'H-TF订单将写入金山文档在线表格(工作表)' : '请完成配置') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="help-card-wrapper">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<i class="el-icon-question"></i>
|
||||||
|
<span>说明</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-content">
|
||||||
|
<div class="help-item"><i class="el-icon-check"></i><span>file_token 来自「在线表格管理」文件列表或开放平台文档列表</span></div>
|
||||||
|
<div class="help-item"><i class="el-icon-check"></i><span>工作表读写使用 KSheet 单元格接口,sheet_idx 与后台 sheet 一致</span></div>
|
||||||
|
<div class="help-item"><i class="el-icon-check"></i><span>数据表(db)需改用数据表记录类 API,本页为工作表(et)场景</span></div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getKdocsAuthUrl,
|
||||||
|
getKdocsTokenStatus,
|
||||||
|
readKdocsCells,
|
||||||
|
updateKdocsCells
|
||||||
|
} from '@/api/jarvis/kdocs'
|
||||||
|
|
||||||
|
const LS_KEY = 'kdocs_auto_write_config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KdocsCloudConfig',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isAuthorized: false,
|
||||||
|
userId: 'default_user',
|
||||||
|
tokenInfo: null,
|
||||||
|
config: { isConfigured: false, hint: '' },
|
||||||
|
form: {
|
||||||
|
fileId: '',
|
||||||
|
sheetIdx: 0,
|
||||||
|
headerRow: 2,
|
||||||
|
startRow: 3
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
fileId: [{ required: true, message: '请输入 file_token', trigger: 'blur' }],
|
||||||
|
headerRow: [
|
||||||
|
{ required: true, message: '请输入表头行号', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '表头行号必须大于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
startRow: [
|
||||||
|
{ required: true, message: '请输入数据起始行', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '数据起始行必须大于0', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
saveLoading: false,
|
||||||
|
testLoading: false,
|
||||||
|
clearLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.checkAuthStatus()
|
||||||
|
this.loadConfig()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
this.checkAuthStatus()
|
||||||
|
this.loadConfig()
|
||||||
|
},
|
||||||
|
async checkAuthStatus() {
|
||||||
|
try {
|
||||||
|
const response = await getKdocsTokenStatus(this.userId)
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 有 token 即视为已授权;isValid 单独展示(避免 expires_in 缺失时误判未保存)
|
||||||
|
this.isAuthorized = !!response.data.hasToken
|
||||||
|
this.tokenInfo = response.data
|
||||||
|
if (response.data.userId) {
|
||||||
|
this.userId = response.data.userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadConfig() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LS_KEY)
|
||||||
|
if (raw) {
|
||||||
|
const c = JSON.parse(raw)
|
||||||
|
this.form.fileId = c.fileId || ''
|
||||||
|
this.form.sheetIdx = c.sheetIdx != null ? c.sheetIdx : 0
|
||||||
|
this.form.headerRow = c.headerRow || 2
|
||||||
|
this.form.startRow = c.startRow || 3
|
||||||
|
this.config.isConfigured = !!c.fileId
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractValues(payload) {
|
||||||
|
if (!payload) return []
|
||||||
|
if (Array.isArray(payload.values)) return payload.values
|
||||||
|
if (payload.data && Array.isArray(payload.data.values)) return payload.data.values
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async handleAuthorize() {
|
||||||
|
try {
|
||||||
|
const response = await getKdocsAuthUrl()
|
||||||
|
if (response.code === 200) {
|
||||||
|
const w = 600
|
||||||
|
const h = 700
|
||||||
|
window.open(
|
||||||
|
response.data,
|
||||||
|
'KdocsAuth',
|
||||||
|
`width=${w},height=${h},left=${(screen.width - w) / 2},top=${(screen.height - h) / 2},resizable=yes,scrollbars=yes`
|
||||||
|
)
|
||||||
|
this.$message.success('请在弹出窗口完成授权')
|
||||||
|
const handler = (event) => {
|
||||||
|
if (event.data && event.data.type === 'kdocs_oauth_callback') {
|
||||||
|
window.removeEventListener('message', handler)
|
||||||
|
if (event.data.userId) {
|
||||||
|
this.userId = event.data.userId
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.checkAuthStatus()
|
||||||
|
this.$message.success('授权完成')
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('message', handler)
|
||||||
|
setTimeout(() => this.checkAuthStatus(), 3000)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '获取授权地址失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleRefreshAuth() {
|
||||||
|
await this.checkAuthStatus()
|
||||||
|
this.$message.success('已刷新')
|
||||||
|
},
|
||||||
|
async handleTestRead() {
|
||||||
|
if (!this.isAuthorized) {
|
||||||
|
this.$message.warning('请先授权')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.form.fileId) {
|
||||||
|
this.$message.warning('请输入 file_token')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await readKdocsCells({
|
||||||
|
userId: this.userId,
|
||||||
|
fileToken: this.form.fileId,
|
||||||
|
sheetIdx: this.form.sheetIdx,
|
||||||
|
range: 'A1:B5'
|
||||||
|
})
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$message.success('读取成功')
|
||||||
|
console.log('read', response.data)
|
||||||
|
} else {
|
||||||
|
this.$message.warning(response.msg || '读取失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '读取失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
if (!this.isAuthorized) {
|
||||||
|
this.$message.warning('请先授权')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.saveLoading = true
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
LS_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
sheetIdx: this.form.sheetIdx,
|
||||||
|
headerRow: this.form.headerRow,
|
||||||
|
startRow: this.form.startRow
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.config.isConfigured = true
|
||||||
|
this.config.hint = '将使用金山文档 KSheet 写入工作表'
|
||||||
|
this.$message.success('已保存')
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleTest() {
|
||||||
|
if (!this.isAuthorized) {
|
||||||
|
this.$message.warning('请先授权')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$refs.form.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
this.testLoading = true
|
||||||
|
try {
|
||||||
|
const readRes = await readKdocsCells({
|
||||||
|
userId: this.userId,
|
||||||
|
fileToken: this.form.fileId,
|
||||||
|
sheetIdx: this.form.sheetIdx,
|
||||||
|
range: 'A1:B5'
|
||||||
|
})
|
||||||
|
if (readRes.code !== 200) {
|
||||||
|
this.$message.error(readRes.msg || '读失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const testRange = `A${this.form.startRow}:B${this.form.startRow}`
|
||||||
|
const writeRes = await updateKdocsCells({
|
||||||
|
userId: this.userId,
|
||||||
|
fileToken: this.form.fileId,
|
||||||
|
sheetIdx: this.form.sheetIdx,
|
||||||
|
range: testRange,
|
||||||
|
values: [['测试1', '测试2']]
|
||||||
|
})
|
||||||
|
if (writeRes.code === 200) {
|
||||||
|
this.$message.success('读写测试成功')
|
||||||
|
} else {
|
||||||
|
this.$message.warning(writeRes.msg || '写失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '测试失败')
|
||||||
|
} finally {
|
||||||
|
this.testLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleClear() {
|
||||||
|
this.$confirm('确定清除本地配置?', '提示', { type: 'warning' })
|
||||||
|
.then(() => {
|
||||||
|
localStorage.removeItem(LS_KEY)
|
||||||
|
this.form.fileId = ''
|
||||||
|
this.form.sheetIdx = 0
|
||||||
|
this.form.headerRow = 2
|
||||||
|
this.form.startRow = 3
|
||||||
|
this.config.isConfigured = false
|
||||||
|
this.config.hint = ''
|
||||||
|
this.$message.success('已清除')
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.kdocs-config {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.config-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.config-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.config-right {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.auth-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.status-card-wrapper {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.status-card.success {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
}
|
||||||
|
.status-card.warning {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border: 1px solid #fbc4c4;
|
||||||
|
}
|
||||||
|
.status-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.status-icon.success {
|
||||||
|
background-color: #67c23a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-icon.warning {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.status-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.help-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.help-item i {
|
||||||
|
color: #67c23a;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
723
src/views/jarvis/docSync/components/TencentDocConfig.vue
Normal file
723
src/views/jarvis/docSync/components/TencentDocConfig.vue
Normal file
@@ -0,0 +1,723 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tendoc-config">
|
||||||
|
<div class="config-container">
|
||||||
|
<!-- 左侧:配置表单 -->
|
||||||
|
<div class="config-left">
|
||||||
|
<!-- 授权状态 -->
|
||||||
|
<el-card class="config-section">
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<i class="el-icon-key"></i>
|
||||||
|
<span>授权状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-status">
|
||||||
|
<el-tag v-if="config.hasAccessToken" type="success" size="medium">
|
||||||
|
<i class="el-icon-circle-check"></i> {{ config.accessTokenStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="medium">
|
||||||
|
<i class="el-icon-circle-close"></i> {{ config.accessTokenStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-if="!config.hasAccessToken"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-unlock"
|
||||||
|
@click="handleAuth"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>
|
||||||
|
立即授权
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleRefreshAuth"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>
|
||||||
|
刷新状态
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="config.hint" class="config-hint" style="margin-top: 10px; color: #909399; font-size: 12px;">
|
||||||
|
<i class="el-icon-info"></i> {{ config.hint }}
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 文档配置表单 -->
|
||||||
|
<el-card class="config-section">
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span>H-TF订单自动写入配置</span>
|
||||||
|
<el-tag v-if="config.isConfigured" type="success" size="mini" style="margin-left: 10px;">
|
||||||
|
<i class="el-icon-success"></i> 已配置
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="warning" size="mini" style="margin-left: 10px;">
|
||||||
|
<i class="el-icon-warning"></i> 未配置
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px" size="small">
|
||||||
|
<el-form-item label="文件ID" prop="fileId">
|
||||||
|
<el-input
|
||||||
|
v-model="form.fileId"
|
||||||
|
placeholder="例如:DUW50RUprWXh2TGJK"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
slot="append"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleFetchSheets"
|
||||||
|
:disabled="!form.fileId"
|
||||||
|
>
|
||||||
|
获取工作表
|
||||||
|
</el-button>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="工作表ID" prop="sheetId">
|
||||||
|
<el-select
|
||||||
|
v-if="sheetList.length > 0"
|
||||||
|
v-model="form.sheetId"
|
||||||
|
placeholder="请选择工作表"
|
||||||
|
style="width: 100%;"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="sheet in sheetList"
|
||||||
|
:key="sheet.sheetId"
|
||||||
|
:label="sheet.title"
|
||||||
|
:value="sheet.sheetId"
|
||||||
|
>
|
||||||
|
<span style="float: left">{{ sheet.title }}</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 12px;">{{ sheet.sheetId }}</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="form.sheetId"
|
||||||
|
placeholder="例如:BB08J2"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="表头行号" prop="headerRow">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.headerRow"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="起始行号" prop="startRow">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.startRow"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSave" :loading="saveLoading" icon="el-icon-check">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleTest" :loading="testLoading" icon="el-icon-setting">
|
||||||
|
测试配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleClear" :loading="clearLoading" type="danger" plain icon="el-icon-delete">
|
||||||
|
清除配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="showOperationLogs = true" icon="el-icon-document">
|
||||||
|
操作日志
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:状态信息 -->
|
||||||
|
<div class="config-right">
|
||||||
|
<!-- 配置状态提示 -->
|
||||||
|
<el-card class="status-card-wrapper">
|
||||||
|
<div class="status-card" :class="config.isConfigured ? 'success' : 'warning'">
|
||||||
|
<div class="status-icon" :class="config.isConfigured ? 'success' : 'warning'">
|
||||||
|
<i :class="config.isConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-text">
|
||||||
|
<div class="status-title">{{ config.isConfigured ? '配置完成' : '配置未完成' }}</div>
|
||||||
|
<div class="status-desc">{{ config.hint || (config.isConfigured ? 'H-TF订单将自动写入腾讯文档' : '请完成配置') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 表格行数(从接口获取,用于决定同步范围) -->
|
||||||
|
<el-card v-if="config.progressHint || config.currentProgress" class="progress-card-wrapper">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<i class="el-icon-data-line"></i>
|
||||||
|
<span>表格行数</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-content">
|
||||||
|
<div v-if="config.currentProgress" class="progress-detail">
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="label">当前有数据行数</span>
|
||||||
|
<span class="value">第 {{ config.currentProgress }} 行(接口获取)</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="label">下次同步起始</span>
|
||||||
|
<span class="value">第 {{ config.nextStartRow != null ? config.nextStartRow : form.startRow }} 行</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-hint">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
由接口实时获取表格行数,不再使用本地保存的进度
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-progress">
|
||||||
|
{{ config.progressHint || '暂无表格行数(请先授权并配置)' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 快速帮助 -->
|
||||||
|
<el-card class="help-card-wrapper">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<i class="el-icon-question"></i>
|
||||||
|
<span>配置说明</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-content">
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>文件ID从腾讯文档URL中获取</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>点击"获取工作表"自动加载</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>表头行号默认为第2行</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>数据起始行默认为第3行</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作日志查看对话框 -->
|
||||||
|
<tencent-doc-operation-logs
|
||||||
|
v-model="showOperationLogs"
|
||||||
|
:file-id="form.fileId"
|
||||||
|
:sheet-id="form.sheetId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getAutoWriteConfig,
|
||||||
|
updateAutoWriteConfig,
|
||||||
|
testAutoWriteConfig,
|
||||||
|
clearAutoWriteConfig,
|
||||||
|
getDocSheetList,
|
||||||
|
getTencentDocAuthUrl
|
||||||
|
} from '@/api/jarvis/tendoc'
|
||||||
|
import TencentDocOperationLogs from '@/views/system/jdorder/components/TencentDocOperationLogs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TencentDocConfig',
|
||||||
|
components: {
|
||||||
|
TencentDocOperationLogs
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showOperationLogs: false,
|
||||||
|
config: {
|
||||||
|
hasAccessToken: false,
|
||||||
|
accessTokenStatus: '未授权',
|
||||||
|
fileId: '',
|
||||||
|
sheetId: '',
|
||||||
|
appId: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
isConfigured: false,
|
||||||
|
hint: '',
|
||||||
|
progressHint: '',
|
||||||
|
currentProgress: null
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
fileId: '',
|
||||||
|
sheetId: '',
|
||||||
|
headerRow: 2,
|
||||||
|
startRow: 3
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
fileId: [
|
||||||
|
{ required: true, message: '请输入文件ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
sheetId: [
|
||||||
|
{ required: true, message: '请输入工作表ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
headerRow: [
|
||||||
|
{ required: true, message: '请输入表头行号', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '表头行号必须大于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
startRow: [
|
||||||
|
{ required: true, message: '请输入数据起始行', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '数据起始行必须大于0', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sheetList: [],
|
||||||
|
saveLoading: false,
|
||||||
|
testLoading: false,
|
||||||
|
clearLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadConfig()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 刷新配置 */
|
||||||
|
refresh() {
|
||||||
|
this.loadConfig()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 加载当前配置 */
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const res = await getAutoWriteConfig()
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
this.config = res.data
|
||||||
|
this.form.fileId = res.data.fileId || ''
|
||||||
|
this.form.sheetId = res.data.sheetId || ''
|
||||||
|
// 确保 headerRow 和 startRow 是数字类型
|
||||||
|
this.form.headerRow = parseInt(res.data.headerRow) || 2
|
||||||
|
this.form.startRow = parseInt(res.data.startRow) || 3
|
||||||
|
console.log('配置加载成功 - headerRow:', this.form.headerRow, 'startRow:', this.form.startRow)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载配置失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 打开授权页面 */
|
||||||
|
async handleAuth() {
|
||||||
|
try {
|
||||||
|
const res = await getTencentDocAuthUrl()
|
||||||
|
if (res.code !== 200 || !res.data) {
|
||||||
|
this.$message.error('获取授权URL失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUrl = res.data
|
||||||
|
const width = 600
|
||||||
|
const height = 700
|
||||||
|
const left = (window.screen.width - width) / 2
|
||||||
|
const top = (window.screen.height - height) / 2
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
authUrl,
|
||||||
|
'腾讯文档授权',
|
||||||
|
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$message.success('授权页面已打开,请在新窗口中完成授权')
|
||||||
|
|
||||||
|
// 监听授权完成消息
|
||||||
|
const messageHandler = (event) => {
|
||||||
|
if (event.data && event.data.type === 'tendoc_oauth_callback') {
|
||||||
|
window.removeEventListener('message', messageHandler)
|
||||||
|
this.loadConfig()
|
||||||
|
this.$message.success('授权完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('message', messageHandler)
|
||||||
|
|
||||||
|
// 3秒后刷新配置状态
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadConfig()
|
||||||
|
}, 3000)
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('打开授权页面失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 刷新授权状态 */
|
||||||
|
async handleRefreshAuth() {
|
||||||
|
await this.loadConfig()
|
||||||
|
this.$message.success('授权状态已刷新')
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取工作表列表 */
|
||||||
|
async handleFetchSheets() {
|
||||||
|
if (!this.form.fileId) {
|
||||||
|
this.$message.warning('请先输入文件ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$message.info('正在获取工作表列表...')
|
||||||
|
const res = await getDocSheetList(this.form.fileId)
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data && res.data.sheets) {
|
||||||
|
this.sheetList = res.data.sheets
|
||||||
|
this.$message.success(`获取成功,共 ${this.sheetList.length} 个工作表`)
|
||||||
|
} else {
|
||||||
|
this.$message.error('获取工作表列表失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('获取工作表列表失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存配置 */
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(async valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveLoading = true
|
||||||
|
try {
|
||||||
|
const res = await updateAutoWriteConfig({
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
sheetId: this.form.sheetId,
|
||||||
|
headerRow: this.form.headerRow,
|
||||||
|
startRow: this.form.startRow
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success(`配置保存成功!表头第${this.form.headerRow}行,数据从第${this.form.startRow}行开始`)
|
||||||
|
console.log('配置保存成功 - 保存的值:', {
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
sheetId: this.form.sheetId,
|
||||||
|
headerRow: this.form.headerRow,
|
||||||
|
startRow: this.form.startRow
|
||||||
|
})
|
||||||
|
// 延迟重新加载配置,确保后端已保存
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadConfig()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
this.$message.error('保存失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('保存失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 测试配置 */
|
||||||
|
async handleTest() {
|
||||||
|
this.testLoading = true
|
||||||
|
try {
|
||||||
|
const res = await testAutoWriteConfig()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$alert(
|
||||||
|
'<pre style="text-align: left; max-height: 400px; overflow: auto;">' +
|
||||||
|
JSON.stringify(res.data, null, 2) +
|
||||||
|
'</pre>',
|
||||||
|
'测试成功',
|
||||||
|
{
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.$message.error('测试失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('测试失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.testLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清除配置 */
|
||||||
|
async handleClear() {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要清除配置吗?这不会清除授权令牌。', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.clearLoading = true
|
||||||
|
const res = await clearAutoWriteConfig()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('配置已清除')
|
||||||
|
this.form.fileId = ''
|
||||||
|
this.form.sheetId = ''
|
||||||
|
this.form.startRow = 3
|
||||||
|
this.sheetList = []
|
||||||
|
this.loadConfig()
|
||||||
|
} else {
|
||||||
|
this.$message.error('清除失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
this.$message.error('清除失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.clearLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 容器布局 */
|
||||||
|
.config-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置区块 */
|
||||||
|
.config-section {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 授权状态 */
|
||||||
|
.auth-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态卡片 */
|
||||||
|
.status-card-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.success {
|
||||||
|
background: rgba(103, 194, 58, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.warning {
|
||||||
|
background: rgba(230, 162, 60, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度卡片 */
|
||||||
|
.progress-card-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-card-wrapper >>> .el-card__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fef0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #f56c6c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-progress {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 帮助卡片 */
|
||||||
|
.help-card-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-card-wrapper >>> .el-card__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item i {
|
||||||
|
color: #67c23a;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element UI 覆盖样式 */
|
||||||
|
.config-section >>> .el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section >>> .el-form-item__label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section >>> .el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.config-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/views/jarvis/docSync/index.vue
Normal file
69
src/views/jarvis/docSync/index.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span style="font-weight: bold; font-size: 16px;">
|
||||||
|
<i class="el-icon-document"></i> 文档同步配置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab切换 -->
|
||||||
|
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
||||||
|
<el-tab-pane label="腾讯文档" name="tendoc">
|
||||||
|
<span slot="label">
|
||||||
|
<i class="el-icon-document"></i> 腾讯文档
|
||||||
|
</span>
|
||||||
|
<TencentDocConfig ref="tendocConfig" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="金山文档" name="kdocs">
|
||||||
|
<span slot="label">
|
||||||
|
<i class="el-icon-document-copy"></i> 金山文档
|
||||||
|
</span>
|
||||||
|
<KdocsCloudConfig ref="kdocsConfig" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TencentDocConfig from './components/TencentDocConfig'
|
||||||
|
import KdocsCloudConfig from './components/KdocsCloudConfig'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DocSync',
|
||||||
|
components: {
|
||||||
|
TencentDocConfig,
|
||||||
|
KdocsCloudConfig
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: 'tendoc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabClick(tab) {
|
||||||
|
// Tab切换时的处理
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (tab.name === 'tendoc' && this.$refs.tendocConfig) {
|
||||||
|
this.$refs.tendocConfig.refresh()
|
||||||
|
} else if (tab.name === 'kdocs' && this.$refs.kdocsConfig) {
|
||||||
|
this.$refs.kdocsConfig.refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-tabs__header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
325
src/views/jarvis/kdocs/index.vue
Normal file
325
src/views/jarvis/kdocs/index.vue
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>金山文档 在线表格</span>
|
||||||
|
<el-button style="float: right; padding: 3px 0" type="text" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert v-if="!isAuthorized" title="未授权" type="warning" :closable="false" show-icon>
|
||||||
|
<template slot="default">
|
||||||
|
<span>请完成金山文档开放平台授权(个人云)</span>
|
||||||
|
<el-button type="primary" size="small" style="margin-left: 10px" @click="handleAuthorize">立即授权</el-button>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-alert v-else title="已授权" type="success" :closable="false" show-icon>
|
||||||
|
<template slot="default">
|
||||||
|
<span>授权状态:正常</span>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-card v-if="isAuthorized && userInfo" style="margin-top: 20px">
|
||||||
|
<div slot="header">用户信息</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="open_id">{{ userInfo.user_id || userInfo.open_id || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="昵称">{{ userInfo.nickname || userInfo.name || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="isAuthorized" style="margin-top: 20px">
|
||||||
|
<div slot="header">
|
||||||
|
<span>文件列表(个人云扁平列表)</span>
|
||||||
|
<el-button type="primary" size="small" style="float: right" @click="handleLoadFiles(true)">加载 / 刷新</el-button>
|
||||||
|
</div>
|
||||||
|
<p v-if="listHint" class="list-hint">{{ listHint }}</p>
|
||||||
|
<el-table v-loading="fileListLoading" :data="fileList" border style="width: 100%">
|
||||||
|
<el-table-column prop="file_name" label="文件名" min-width="200" />
|
||||||
|
<el-table-column prop="file_token" label="file_token" min-width="260" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="file_type" label="类型" width="100" />
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="success" size="mini" @click="handleEditFile(scope.row)">编辑表格</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="hasMoreFiles" style="margin-top: 12px">
|
||||||
|
<el-button type="default" size="small" :loading="fileListLoading" @click="handleLoadMore">加载更多</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog title="编辑表格" :visible.sync="editDialogVisible" width="80%" :close-on-click-modal="false">
|
||||||
|
<div v-if="currentFile">
|
||||||
|
<el-form :inline="true" class="demo-form-inline">
|
||||||
|
<el-form-item label="工作表">
|
||||||
|
<el-select v-model="currentSheetIdx" placeholder="请选择" @change="handleLoadSheetData">
|
||||||
|
<el-option
|
||||||
|
v-for="sheet in sheetList"
|
||||||
|
:key="sheet.sheet_idx + '-' + sheet.name"
|
||||||
|
:label="sheet.name + ' (idx:' + sheet.sheet_idx + ')'"
|
||||||
|
:value="sheet.sheet_idx"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="单元格范围">
|
||||||
|
<el-input v-model="cellRange" placeholder="例如:A1:B10" style="width: 200px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleLoadSheetData">读取数据</el-button>
|
||||||
|
<el-button type="success" @click="handleUpdateCells">保存数据</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="sheetDataLoading" :data="sheetData" border style="width: 100%; margin-top: 20px">
|
||||||
|
<el-table-column
|
||||||
|
v-for="(col, index) in sheetColumns"
|
||||||
|
:key="index"
|
||||||
|
:prop="'col' + index"
|
||||||
|
:label="getColumnLabel(index)"
|
||||||
|
min-width="120"
|
||||||
|
>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-input v-model="scope.row['col' + index]" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getKdocsAuthUrl,
|
||||||
|
getKdocsTokenStatus,
|
||||||
|
getKdocsUserInfo,
|
||||||
|
getKdocsFileList,
|
||||||
|
getKdocsSheetList,
|
||||||
|
readKdocsCells,
|
||||||
|
updateKdocsCells
|
||||||
|
} from '@/api/jarvis/kdocs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KdocsCloud',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
isAuthorized: false,
|
||||||
|
userInfo: null,
|
||||||
|
userId: 'default_user',
|
||||||
|
fileList: [],
|
||||||
|
fileListLoading: false,
|
||||||
|
listHint: '',
|
||||||
|
hasMoreFiles: false,
|
||||||
|
nextOffset: null,
|
||||||
|
nextFilter: null,
|
||||||
|
editDialogVisible: false,
|
||||||
|
currentFile: null,
|
||||||
|
currentSheetIdx: 0,
|
||||||
|
sheetList: [],
|
||||||
|
sheetData: [],
|
||||||
|
sheetDataLoading: false,
|
||||||
|
cellRange: 'A1:Z100',
|
||||||
|
sheetColumns: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.checkAuthStatus()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async checkAuthStatus() {
|
||||||
|
try {
|
||||||
|
const response = await getKdocsTokenStatus(this.userId)
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.isAuthorized = !!response.data.hasToken
|
||||||
|
if (this.isAuthorized) {
|
||||||
|
this.loadUserInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadUserInfo() {
|
||||||
|
try {
|
||||||
|
const response = await getKdocsUserInfo(this.userId)
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.userInfo = response.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleAuthorize() {
|
||||||
|
try {
|
||||||
|
const response = await getKdocsAuthUrl()
|
||||||
|
if (response.code === 200) {
|
||||||
|
window.open(response.data, '_blank')
|
||||||
|
this.$message.success('请在新窗口完成授权,完成后回到本页刷新')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '获取授权地址失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleRefresh() {
|
||||||
|
this.checkAuthStatus()
|
||||||
|
if (this.isAuthorized) {
|
||||||
|
this.handleLoadFiles(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** @param {boolean} reset 是否重置游标从第一页拉取 */
|
||||||
|
async handleLoadFiles(reset) {
|
||||||
|
if (!this.isAuthorized) {
|
||||||
|
this.$message.warning('请先授权')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
this.nextOffset = null
|
||||||
|
this.nextFilter = null
|
||||||
|
this.fileList = []
|
||||||
|
}
|
||||||
|
this.fileListLoading = true
|
||||||
|
this.listHint = ''
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
userId: this.userId,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
|
}
|
||||||
|
if (this.nextOffset != null) {
|
||||||
|
params.next_offset = this.nextOffset
|
||||||
|
}
|
||||||
|
if (this.nextFilter) {
|
||||||
|
params.next_filter = this.nextFilter
|
||||||
|
}
|
||||||
|
const response = await getKdocsFileList(params)
|
||||||
|
if (response.code === 200) {
|
||||||
|
const chunk = response.data.files || []
|
||||||
|
this.fileList = reset ? chunk : this.fileList.concat(chunk)
|
||||||
|
this.nextOffset = response.data.next_offset
|
||||||
|
this.nextFilter = response.data.next_filter || null
|
||||||
|
this.hasMoreFiles = !!response.data.has_more
|
||||||
|
this.listHint =
|
||||||
|
'file_token 请使用列表中的值(来自文档 open_id)。在线表格工作表请选 sheet_type 为 et 的 sheet;数据表为 db 时需用数据表 API。'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '加载失败')
|
||||||
|
} finally {
|
||||||
|
this.fileListLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLoadMore() {
|
||||||
|
if (!this.hasMoreFiles) return
|
||||||
|
this.handleLoadFiles(false)
|
||||||
|
},
|
||||||
|
async handleEditFile(file) {
|
||||||
|
this.currentFile = file
|
||||||
|
this.editDialogVisible = true
|
||||||
|
try {
|
||||||
|
const response = await getKdocsSheetList(this.userId, file.file_token)
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.sheetList = response.data.sheets || []
|
||||||
|
if (this.sheetList.length > 0) {
|
||||||
|
const first = this.sheetList[0]
|
||||||
|
this.currentSheetIdx = first.sheet_idx != null ? first.sheet_idx : 0
|
||||||
|
this.handleLoadSheetData()
|
||||||
|
} else {
|
||||||
|
this.$message.warning('未获取到工作表列表')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '加载工作表失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractCellValues(payload) {
|
||||||
|
if (!payload) return []
|
||||||
|
if (Array.isArray(payload.values)) return payload.values
|
||||||
|
if (payload.data && Array.isArray(payload.data.values)) return payload.data.values
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async handleLoadSheetData() {
|
||||||
|
if (!this.currentFile) return
|
||||||
|
this.sheetDataLoading = true
|
||||||
|
try {
|
||||||
|
const response = await readKdocsCells({
|
||||||
|
userId: this.userId,
|
||||||
|
fileToken: this.currentFile.file_token,
|
||||||
|
sheetIdx: this.currentSheetIdx,
|
||||||
|
range: this.cellRange
|
||||||
|
})
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.processSheetData(this.extractCellValues(response.data))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '读取失败')
|
||||||
|
} finally {
|
||||||
|
this.sheetDataLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
processSheetData(values) {
|
||||||
|
if (!values || values.length === 0) {
|
||||||
|
this.sheetData = []
|
||||||
|
this.sheetColumns = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxCols = Math.max(...values.map(row => (row ? row.length : 0)))
|
||||||
|
this.sheetColumns = Array.from({ length: maxCols }, (_, i) => i)
|
||||||
|
this.sheetData = values.map(row => {
|
||||||
|
const rowData = {}
|
||||||
|
const r = row || []
|
||||||
|
r.forEach((cell, index) => {
|
||||||
|
rowData['col' + index] = cell !== null && cell !== undefined ? String(cell) : ''
|
||||||
|
})
|
||||||
|
for (let i = r.length; i < maxCols; i++) {
|
||||||
|
rowData['col' + i] = ''
|
||||||
|
}
|
||||||
|
return rowData
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getColumnLabel(index) {
|
||||||
|
let label = ''
|
||||||
|
let num = index
|
||||||
|
while (num >= 0) {
|
||||||
|
label = String.fromCharCode(65 + (num % 26)) + label
|
||||||
|
num = Math.floor(num / 26) - 1
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
},
|
||||||
|
async handleUpdateCells() {
|
||||||
|
if (!this.currentFile) return
|
||||||
|
const values = this.sheetData.map(row => {
|
||||||
|
return this.sheetColumns.map(colIndex => {
|
||||||
|
const v = row['col' + colIndex]
|
||||||
|
return v !== undefined ? v : ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const response = await updateKdocsCells({
|
||||||
|
userId: this.userId,
|
||||||
|
fileToken: this.currentFile.file_token,
|
||||||
|
sheetIdx: this.currentSheetIdx,
|
||||||
|
range: this.cellRange,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$message.success('保存成功')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e.msg || e.message || '保存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.list-hint {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
208
src/views/jarvis/phoneReplaceConfig/index.vue
Normal file
208
src/views/jarvis/phoneReplaceConfig/index.vue
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||||
|
<el-tab-pane label="腾锋" name="腾锋">
|
||||||
|
<div class="phone-config-container">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>腾锋手机号配置</span>
|
||||||
|
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAddPhone('腾锋')">添加手机号</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="tfPhoneList" border>
|
||||||
|
<el-table-column label="序号" type="index" width="80" align="center" />
|
||||||
|
<el-table-column label="手机号" align="center" prop="phone" />
|
||||||
|
<el-table-column label="操作" align="center" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleRemovePhone('腾锋', scope.row.phone)"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<el-button type="primary" @click="handleSave('腾锋')">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="昭迎" name="昭迎">
|
||||||
|
<div class="phone-config-container">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>昭迎手机号配置</span>
|
||||||
|
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAddPhone('昭迎')">添加手机号</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="zyPhoneList" border>
|
||||||
|
<el-table-column label="序号" type="index" width="80" align="center" />
|
||||||
|
<el-table-column label="手机号" align="center" prop="phone" />
|
||||||
|
<el-table-column label="操作" align="center" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleRemovePhone('昭迎', scope.row.phone)"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<el-button type="primary" @click="handleSave('昭迎')">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 添加手机号对话框 -->
|
||||||
|
<el-dialog :title="'添加手机号 - ' + currentType" :visible.sync="addPhoneDialogVisible" width="400px" append-to-body>
|
||||||
|
<el-form ref="addPhoneForm" :model="addPhoneForm" :rules="addPhoneRules" label-width="80px">
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="addPhoneForm.phone" placeholder="请输入11位手机号" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitAddPhone">确 定</el-button>
|
||||||
|
<el-button @click="addPhoneDialogVisible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getPhoneList, setPhoneList, addPhone, removePhone } from "@/api/jarvis/phoneReplaceConfig";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PhoneReplaceConfig",
|
||||||
|
data() {
|
||||||
|
// 手机号验证规则
|
||||||
|
const validatePhone = (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请输入手机号'));
|
||||||
|
} else if (!/^1[3-9]\d{9}$/.test(value)) {
|
||||||
|
callback(new Error('请输入正确的11位手机号'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 遮罩层
|
||||||
|
loading: false,
|
||||||
|
// 当前激活的标签页
|
||||||
|
activeTab: "腾锋",
|
||||||
|
// 腾锋手机号列表
|
||||||
|
tfPhoneList: [],
|
||||||
|
// 昭迎手机号列表
|
||||||
|
zyPhoneList: [],
|
||||||
|
// 添加手机号对话框
|
||||||
|
addPhoneDialogVisible: false,
|
||||||
|
// 当前操作的类型
|
||||||
|
currentType: "腾锋",
|
||||||
|
// 添加手机号表单
|
||||||
|
addPhoneForm: {
|
||||||
|
phone: ""
|
||||||
|
},
|
||||||
|
// 添加手机号表单校验
|
||||||
|
addPhoneRules: {
|
||||||
|
phone: [
|
||||||
|
{ required: true, validator: validatePhone, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadPhoneList("腾锋");
|
||||||
|
this.loadPhoneList("昭迎");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 加载手机号列表 */
|
||||||
|
loadPhoneList(type) {
|
||||||
|
this.loading = true;
|
||||||
|
getPhoneList(type).then(response => {
|
||||||
|
const phoneList = response.data || [];
|
||||||
|
const formattedList = phoneList.map(phone => ({ phone }));
|
||||||
|
if (type === "腾锋") {
|
||||||
|
this.tfPhoneList = formattedList;
|
||||||
|
} else if (type === "昭迎") {
|
||||||
|
this.zyPhoneList = formattedList;
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 标签页切换 */
|
||||||
|
handleTabClick(tab) {
|
||||||
|
// 标签页切换时不需要重新加载,因为已经在created中加载了
|
||||||
|
},
|
||||||
|
/** 添加手机号 */
|
||||||
|
handleAddPhone(type) {
|
||||||
|
this.currentType = type;
|
||||||
|
this.addPhoneForm.phone = "";
|
||||||
|
this.addPhoneDialogVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.addPhoneForm) {
|
||||||
|
this.$refs.addPhoneForm.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 提交添加手机号 */
|
||||||
|
submitAddPhone() {
|
||||||
|
this.$refs.addPhoneForm.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
const phoneList = this.currentType === "腾锋" ? this.tfPhoneList : this.zyPhoneList;
|
||||||
|
// 检查是否已存在
|
||||||
|
if (phoneList.some(item => item.phone === this.addPhoneForm.phone)) {
|
||||||
|
this.$modal.msgError("该手机号已存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 添加到列表
|
||||||
|
phoneList.push({ phone: this.addPhoneForm.phone });
|
||||||
|
// 保存到后端
|
||||||
|
const phoneArray = phoneList.map(item => item.phone);
|
||||||
|
setPhoneList(this.currentType, phoneArray).then(() => {
|
||||||
|
this.$modal.msgSuccess("添加成功");
|
||||||
|
this.addPhoneDialogVisible = false;
|
||||||
|
this.loadPhoneList(this.currentType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 删除手机号 */
|
||||||
|
handleRemovePhone(type, phone) {
|
||||||
|
this.$modal.confirm('是否确认删除手机号"' + phone + '"?').then(() => {
|
||||||
|
const phoneList = type === "腾锋" ? this.tfPhoneList : this.zyPhoneList;
|
||||||
|
const index = phoneList.findIndex(item => item.phone === phone);
|
||||||
|
if (index > -1) {
|
||||||
|
phoneList.splice(index, 1);
|
||||||
|
const phoneArray = phoneList.map(item => item.phone);
|
||||||
|
return setPhoneList(type, phoneArray);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess("删除成功");
|
||||||
|
this.loadPhoneList(type);
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 保存配置 */
|
||||||
|
handleSave(type) {
|
||||||
|
const phoneList = type === "腾锋" ? this.tfPhoneList : this.zyPhoneList;
|
||||||
|
const phoneArray = phoneList.map(item => item.phone);
|
||||||
|
setPhoneList(type, phoneArray).then(() => {
|
||||||
|
this.$modal.msgSuccess("保存成功");
|
||||||
|
this.loadPhoneList(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.phone-config-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
279
src/views/jarvis/productJdConfig/index.vue
Normal file
279
src/views/jarvis/productJdConfig/index.vue
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||||
|
<el-form-item label="产品型号" prop="productModel">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.productModel"
|
||||||
|
placeholder="请输入产品型号"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-edit"
|
||||||
|
size="mini"
|
||||||
|
:disabled="single"
|
||||||
|
@click="handleUpdate"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleInitData"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:init']"
|
||||||
|
>初始化数据</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="productJdConfigList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="产品型号" align="center" prop="productModel" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="京东链接" align="center" prop="jdUrl" :show-overflow-tooltip="true">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link :href="scope.row.jdUrl" target="_blank" type="primary">{{ scope.row.jdUrl }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="佣金(收取)" align="center" prop="commissionReceive" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.commissionReceive ? '¥' + scope.row.commissionReceive : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="佣金(支付)" align="center" prop="commissionPay" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.commissionPay ? '¥' + scope.row.commissionPay : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['jarvis:productJdConfig:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 添加或修改产品京东配置对话框 -->
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="产品型号" prop="productModel">
|
||||||
|
<el-input v-model="form.productModel" placeholder="请输入产品型号" :disabled="form.productModel && !isAdd" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="京东链接" prop="jdUrl">
|
||||||
|
<el-input v-model="form.jdUrl" type="textarea" :rows="3" placeholder="请输入京东链接" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="佣金(收取)" prop="commissionReceive">
|
||||||
|
<el-input-number v-model="form.commissionReceive" :precision="2" :step="0.1" :min="0" placeholder="请输入佣金(收取)" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="佣金(支付)" prop="commissionPay">
|
||||||
|
<el-input-number v-model="form.commissionPay" :precision="2" :step="0.1" :min="0" placeholder="请输入佣金(支付)" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listProductJdConfig, getProductJdConfig, delProductJdConfig, addProductJdConfig, updateProductJdConfig, initDefaultData } from "@/api/jarvis/productJdConfig";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ProductJdConfig",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 遮罩层
|
||||||
|
loading: true,
|
||||||
|
// 选中数组
|
||||||
|
ids: [],
|
||||||
|
// 非单个禁用
|
||||||
|
single: true,
|
||||||
|
// 非多个禁用
|
||||||
|
multiple: true,
|
||||||
|
// 显示搜索条件
|
||||||
|
showSearch: true,
|
||||||
|
// 总条数
|
||||||
|
total: 0,
|
||||||
|
// 产品京东配置表格数据
|
||||||
|
productJdConfigList: [],
|
||||||
|
// 弹出层标题
|
||||||
|
title: "",
|
||||||
|
// 是否显示弹出层
|
||||||
|
open: false,
|
||||||
|
// 是否为新增
|
||||||
|
isAdd: false,
|
||||||
|
// 查询参数
|
||||||
|
queryParams: {
|
||||||
|
productModel: null,
|
||||||
|
},
|
||||||
|
// 表单参数
|
||||||
|
form: {},
|
||||||
|
// 表单校验
|
||||||
|
rules: {
|
||||||
|
productModel: [
|
||||||
|
{ required: true, message: "产品型号不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
jdUrl: [
|
||||||
|
{ required: true, message: "京东链接不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
commissionReceive: [
|
||||||
|
{ required: true, message: "佣金(收取)不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
commissionPay: [
|
||||||
|
{ required: true, message: "佣金(支付)不能为空", trigger: "blur" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询产品京东配置列表 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true;
|
||||||
|
listProductJdConfig(this.queryParams).then(response => {
|
||||||
|
this.productJdConfigList = response.data || [];
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 取消按钮
|
||||||
|
cancel() {
|
||||||
|
this.open = false;
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
// 表单重置
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
productModel: null,
|
||||||
|
jdUrl: null,
|
||||||
|
commission: 0,
|
||||||
|
commissionReceive: 0,
|
||||||
|
commissionPay: 0
|
||||||
|
};
|
||||||
|
this.resetForm("form");
|
||||||
|
},
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm");
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
// 多选框选中数据
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.productModel);
|
||||||
|
this.single = selection.length !== 1;
|
||||||
|
this.multiple = !selection.length;
|
||||||
|
},
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
handleAdd() {
|
||||||
|
this.reset();
|
||||||
|
this.isAdd = true;
|
||||||
|
this.open = true;
|
||||||
|
this.title = "添加产品京东配置";
|
||||||
|
},
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset();
|
||||||
|
this.isAdd = false;
|
||||||
|
const productModel = row.productModel || this.ids[0];
|
||||||
|
getProductJdConfig(productModel).then(response => {
|
||||||
|
this.form = response.data;
|
||||||
|
this.open = true;
|
||||||
|
this.title = "修改产品京东配置";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 提交按钮 */
|
||||||
|
submitForm() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
if (this.isAdd) {
|
||||||
|
addProductJdConfig(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("新增成功");
|
||||||
|
this.open = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateProductJdConfig(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("修改成功");
|
||||||
|
this.open = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
handleDelete(row) {
|
||||||
|
const productModels = row.productModel ? [row.productModel] : this.ids;
|
||||||
|
this.$modal.confirm('是否确认删除产品型号为"' + productModels + '"的数据项?').then(() => {
|
||||||
|
return delProductJdConfig(productModels.join(','));
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess("删除成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 初始化数据按钮操作 */
|
||||||
|
handleInitData() {
|
||||||
|
this.$modal.confirm('是否确认初始化默认产品数据?').then(() => {
|
||||||
|
return initDefaultData();
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess("初始化成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
260
src/views/jarvis/wecomInboundTrace/index.vue
Normal file
260
src/views/jarvis/wecomInboundTrace/index.vue
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="88px">
|
||||||
|
<el-form-item label="发送人" prop="fromUserName">
|
||||||
|
<el-input v-model="queryParams.fromUserName" placeholder="UserID" clearable style="width: 160px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="MsgId" prop="msgId">
|
||||||
|
<el-input v-model="queryParams.msgId" placeholder="精确匹配" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="AgentID" prop="agentId">
|
||||||
|
<el-input v-model="queryParams.agentId" placeholder="应用ID" clearable style="width: 120px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="会话中" prop="sessionActive">
|
||||||
|
<el-select v-model="queryParams.sessionActive" placeholder="全部" clearable style="width: 100px">
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="接收时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['jarvis:wecom:inboundTrace:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete-solid"
|
||||||
|
size="mini"
|
||||||
|
@click="openCleanDialog"
|
||||||
|
v-hasPermi="['jarvis:wecom:inboundTrace:remove']"
|
||||||
|
>清空测试数据</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" border @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column label="ID" prop="id" width="72" />
|
||||||
|
<el-table-column label="AgentID" prop="agentId" width="96" />
|
||||||
|
<el-table-column label="发送人" prop="fromUserName" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="内容摘要" prop="content" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="微信时间" prop="wxMsgTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.wxMsgTime) || '—' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="服务端接收" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="回复摘要" prop="replyContent" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="会话空间" width="88" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.sessionActive === 1" type="warning" size="small">进行中</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">无</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="场景/步骤" width="140" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.sessionScene">{{ scope.row.sessionScene }} / {{ scope.row.sessionStep || '—' }}</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog title="清空测试数据" :visible.sync="cleanOpen" width="520px" append-to-body @close="resetCleanForm">
|
||||||
|
<p style="color:#E6A23C;margin:0 0 12px 0;">将按下方勾选删除数据,生产环境请谨慎操作。</p>
|
||||||
|
<el-checkbox v-model="cleanOptions.clearTraceTable">消息追踪表(wecom_inbound_trace 全部行)</el-checkbox>
|
||||||
|
<br><br>
|
||||||
|
<el-checkbox v-model="cleanOptions.clearWecomSessions">企微多轮会话 Redis(interaction_state:wecom:*)</el-checkbox>
|
||||||
|
<br><br>
|
||||||
|
<el-checkbox v-model="cleanOptions.clearAdhocQueue">分享链待扫描队列(logistics:adhoc:pending:queue)</el-checkbox>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="cleanOpen = false">取 消</el-button>
|
||||||
|
<el-button type="danger" @click="submitClean">确 定清理</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="消息详情" :visible.sync="detailOpen" width="720px" append-to-body>
|
||||||
|
<el-descriptions :column="1" border size="small" v-if="detail">
|
||||||
|
<el-descriptions-item label="MsgId">{{ detail.msgId || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="AgentID">{{ detail.agentId || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="CorpId">{{ detail.corpId || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发送人">{{ detail.fromUserName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="微信发送时间">{{ parseTime(detail.wxMsgTime) || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="服务端接收">{{ parseTime(detail.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户内容">
|
||||||
|
<pre class="trace-pre">{{ detail.content || '(空)' }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Jarvis 回复">
|
||||||
|
<pre class="trace-pre">{{ detail.replyContent || '(空)' }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="会话">
|
||||||
|
<span v-if="detail.sessionActive === 1">进行中 — {{ detail.sessionScene }} / {{ detail.sessionStep }}</span>
|
||||||
|
<span v-else>本次处理后无未完成的多轮会话</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listWecomInboundTrace, delWecomInboundTrace, getWecomInboundTrace, cleanWecomInboundTraceTestData } from '@/api/jarvis/wecomInboundTrace'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WecomInboundTrace',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
ids: [],
|
||||||
|
single: true,
|
||||||
|
multiple: true,
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
dateRange: [],
|
||||||
|
detailOpen: false,
|
||||||
|
detail: null,
|
||||||
|
cleanOpen: false,
|
||||||
|
cleanOptions: {
|
||||||
|
clearTraceTable: true,
|
||||||
|
clearWecomSessions: true,
|
||||||
|
clearAdhocQueue: true
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
fromUserName: null,
|
||||||
|
msgId: null,
|
||||||
|
agentId: null,
|
||||||
|
sessionActive: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
const q = { ...this.queryParams, params: {} }
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
q.params = { beginTime: this.dateRange[0], endTime: this.dateRange[1] }
|
||||||
|
}
|
||||||
|
listWecomInboundTrace(q).then(res => {
|
||||||
|
this.list = res.rows
|
||||||
|
this.total = res.total
|
||||||
|
this.loading = false
|
||||||
|
}).catch(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.id)
|
||||||
|
this.single = selection.length !== 1
|
||||||
|
this.multiple = !selection.length
|
||||||
|
},
|
||||||
|
handleDelete() {
|
||||||
|
const ids = this.ids
|
||||||
|
this.$modal.confirm('是否确认删除选中的追踪记录?').then(() => {
|
||||||
|
return delWecomInboundTrace(ids)
|
||||||
|
}).then(() => {
|
||||||
|
this.getList()
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
openDetail(row) {
|
||||||
|
getWecomInboundTrace(row.id).then(res => {
|
||||||
|
this.detail = res.data
|
||||||
|
this.detailOpen = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openCleanDialog() {
|
||||||
|
this.resetCleanForm()
|
||||||
|
this.cleanOpen = true
|
||||||
|
},
|
||||||
|
resetCleanForm() {
|
||||||
|
this.cleanOptions = {
|
||||||
|
clearTraceTable: true,
|
||||||
|
clearWecomSessions: true,
|
||||||
|
clearAdhocQueue: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitClean() {
|
||||||
|
if (!this.cleanOptions.clearTraceTable && !this.cleanOptions.clearWecomSessions && !this.cleanOptions.clearAdhocQueue) {
|
||||||
|
this.$modal.msgWarning('请至少勾选一项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$modal.confirm('确认按勾选项清理测试数据?此操作不可恢复。').then(() => {
|
||||||
|
return cleanWecomInboundTraceTestData(this.cleanOptions)
|
||||||
|
}).then(res => {
|
||||||
|
this.cleanOpen = false
|
||||||
|
this.getList()
|
||||||
|
const d = res.data || {}
|
||||||
|
const parts = []
|
||||||
|
if (d.traceRowsDeleted != null) parts.push('追踪表删除 ' + d.traceRowsDeleted + ' 行')
|
||||||
|
if (d.wecomSessionKeysDeleted != null) parts.push('会话键 ' + d.wecomSessionKeysDeleted + ' 个')
|
||||||
|
if (d.adhocQueueCleared) parts.push('adhoc 队列已删')
|
||||||
|
this.$modal.msgSuccess(parts.length ? parts.join(';') : '已执行')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trace-pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
343
src/views/jarvis/wecomShareLinkLogistics/index.vue
Normal file
343
src/views/jarvis/wecomShareLinkLogistics/index.vue
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="88px">
|
||||||
|
<el-form-item label="发送人" prop="fromUserName">
|
||||||
|
<el-input v-model="queryParams.fromUserName" placeholder="企微 UserID" clearable style="width: 140px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
|
||||||
|
<el-option label="待扫描" value="PENDING" />
|
||||||
|
<el-option label="等待运单/重试" value="WAITING" />
|
||||||
|
<el-option label="已推送" value="PUSHED" />
|
||||||
|
<el-option label="已放弃" value="ABANDONED" />
|
||||||
|
<el-option label="历史补录" value="IMPORTED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="短链" prop="trackingUrl">
|
||||||
|
<el-input v-model="queryParams.trackingUrl" placeholder="3.cn 片段" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="运单号" prop="waybillNo">
|
||||||
|
<el-input v-model="queryParams.waybillNo" placeholder="模糊" clearable style="width: 120px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="userRemark">
|
||||||
|
<el-input v-model="queryParams.userRemark" placeholder="模糊" clearable style="width: 160px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="入队时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="用于排查企微 3.cn 分享链物流:WAITING + no_waybill_yet 未出单;push_failed 推送未成功;ABANDONED 为当轮队列重试用尽,定时对账(一月内)会归零次数并重新入队;IMPORTED 为从「企微消息跟踪」补录留痕,不表示实时推送结果。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="mb8"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-video-play"
|
||||||
|
size="mini"
|
||||||
|
:loading="drainQueueLoading"
|
||||||
|
v-hasPermi="['jarvis:wecom:shareLinkLog:list']"
|
||||||
|
@click="handleDrainQueueOnce"
|
||||||
|
>执行待队列一轮</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
size="mini"
|
||||||
|
:loading="backfillLoading"
|
||||||
|
v-hasPermi="['jarvis:wecom:shareLinkLog:import']"
|
||||||
|
@click="handleBackfill"
|
||||||
|
>从追踪补录历史</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" border>
|
||||||
|
<el-table-column label="ID" prop="id" width="72" />
|
||||||
|
<el-table-column label="发送人" prop="fromUserName" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="推送目标" prop="touserPush" width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" prop="status" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.status === 'PUSHED'" type="success" size="small">{{ scope.row.status }}</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 'WAITING'" type="warning" size="small">{{ scope.row.status }}</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 'ABANDONED'" type="danger" size="small">{{ scope.row.status }}</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 'IMPORTED'" type="info" size="small">{{ scope.row.status }}</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">{{ scope.row.status || '—' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="扫描次数" prop="scanAttempts" width="88" align="center" />
|
||||||
|
<el-table-column label="运单号" prop="waybillNo" width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="用户备注" prop="userRemark" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="最近说明" prop="lastNote" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="短链" prop="trackingUrl" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="创建时间" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="168" align="center" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-truck" @click="handleFetchShareLink(scope.row)">获取物流</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog title="获取物流信息(分享链)" :visible.sync="fetchLogisticsDialogVisible" width="720px" append-to-body @close="fetchLogisticsResult = null">
|
||||||
|
<div v-loading="fetchLogisticsLoading">
|
||||||
|
<el-alert
|
||||||
|
v-if="fetchLogisticsResult"
|
||||||
|
:title="fetchLogisticsResult.success ? '请求已完成' : '请求失败'"
|
||||||
|
:type="fetchLogisticsResult.success ? 'success' : 'error'"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="mb8"
|
||||||
|
/>
|
||||||
|
<el-form v-if="fetchLogisticsResult && fetchLogisticsResult.success" label-width="120px" size="small">
|
||||||
|
<el-form-item label="jobKey"><span>{{ fetchLogisticsResult.jobKey }}</span></el-form-item>
|
||||||
|
<el-form-item label="终端成功"><span>{{ fetchLogisticsResult.terminalSuccess }}</span>(已推送或已去重)</el-form-item>
|
||||||
|
<el-form-item label="已发推送"><span>{{ fetchLogisticsResult.pushSent }}</span></el-form-item>
|
||||||
|
<el-form-item label="运单号" v-if="fetchLogisticsResult.waybillNo"><span>{{ fetchLogisticsResult.waybillNo }}</span></el-form-item>
|
||||||
|
<el-form-item label="说明" v-if="fetchLogisticsResult.adhocNote"><span>{{ fetchLogisticsResult.adhocNote }}</span></el-form-item>
|
||||||
|
<el-form-item label="物流短链"><span style="word-break:break-all">{{ fetchLogisticsResult.logisticsLink }}</span></el-form-item>
|
||||||
|
<el-form-item label="请求URL" v-if="fetchLogisticsResult.requestUrl">
|
||||||
|
<el-input type="textarea" :rows="2" readonly :value="fetchLogisticsResult.requestUrl" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="健康检查" v-if="fetchLogisticsResult.healthOk != null">
|
||||||
|
<span>{{ fetchLogisticsResult.healthOk }}</span>
|
||||||
|
<span v-if="fetchLogisticsResult.healthMessage"> — {{ fetchLogisticsResult.healthMessage }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="推送错误" v-if="fetchLogisticsResult.pushError">
|
||||||
|
<el-input type="textarea" :rows="3" readonly :value="fetchLogisticsResult.pushError" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="返回(原始)" v-if="fetchLogisticsResult.responseRaw">
|
||||||
|
<el-input type="textarea" :rows="5" readonly :value="fetchLogisticsResult.responseRaw" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="返回(解析)" v-if="fetchLogisticsResult.responseData">
|
||||||
|
<el-input type="textarea" :rows="8" readonly :value="formatJson(fetchLogisticsResult.responseData)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-form v-else-if="fetchLogisticsResult && !fetchLogisticsResult.success" label-width="100px" size="small">
|
||||||
|
<el-form-item label="错误"><span>{{ fetchLogisticsResult.error }}</span></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<span v-else-if="fetchLogisticsLoading" style="color:#999">正在请求…</span>
|
||||||
|
</div>
|
||||||
|
<div slot="footer">
|
||||||
|
<el-button @click="fetchLogisticsDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" v-if="fetchLogisticsResult && fetchLogisticsResult.success" @click="copyFetchLogisticsResult">复制 JSON</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="任务详情" :visible.sync="detailOpen" width="720px" append-to-body>
|
||||||
|
<el-descriptions v-if="detail" :column="1" border size="small">
|
||||||
|
<el-descriptions-item label="jobKey">{{ detail.jobKey }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发送人">{{ detail.fromUserName || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="推送接收人">{{ detail.touserPush || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">{{ detail.status }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="扫描次数">{{ detail.scanAttempts }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="运单号">{{ detail.waybillNo || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最近说明">{{ detail.lastNote || '—' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="短链">
|
||||||
|
<span style="word-break:break-all">{{ detail.trackingUrl }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户备注">
|
||||||
|
<pre class="trace-pre">{{ detail.userRemark || '(空)' }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ parseTime(detail.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ parseTime(detail.updateTime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listWecomShareLinkLogisticsJob, getWecomShareLinkLogisticsJob, backfillShareLinkLogisticsFromTrace, fetchShareLinkManually, drainShareLinkPendingQueueOnce } from '@/api/jarvis/wecomShareLinkLogistics'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WecomShareLinkLogistics',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
backfillLoading: false,
|
||||||
|
drainQueueLoading: false,
|
||||||
|
fetchLogisticsDialogVisible: false,
|
||||||
|
fetchLogisticsLoading: false,
|
||||||
|
fetchLogisticsResult: null,
|
||||||
|
loading: true,
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
dateRange: [],
|
||||||
|
detailOpen: false,
|
||||||
|
detail: null,
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
fromUserName: null,
|
||||||
|
status: null,
|
||||||
|
trackingUrl: null,
|
||||||
|
waybillNo: null,
|
||||||
|
userRemark: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
const q = { ...this.queryParams, params: {} }
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
q.params = { beginTime: this.dateRange[0], endTime: this.dateRange[1] }
|
||||||
|
}
|
||||||
|
listWecomShareLinkLogisticsJob(q).then(res => {
|
||||||
|
this.list = res.rows
|
||||||
|
this.total = res.total
|
||||||
|
this.loading = false
|
||||||
|
}).catch(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
openDetail(row) {
|
||||||
|
getWecomShareLinkLogisticsJob(row.jobKey).then(res => {
|
||||||
|
this.detail = res.data
|
||||||
|
this.detailOpen = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatJson(v) {
|
||||||
|
if (v == null) return ''
|
||||||
|
if (typeof v === 'string') return v
|
||||||
|
try {
|
||||||
|
return JSON.stringify(v, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleFetchShareLink(row) {
|
||||||
|
if (!row.trackingUrl || !String(row.trackingUrl).trim()) {
|
||||||
|
this.$message.warning('该任务暂无物流短链')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.fetchLogisticsDialogVisible = true
|
||||||
|
this.fetchLogisticsLoading = true
|
||||||
|
this.fetchLogisticsResult = null
|
||||||
|
try {
|
||||||
|
const res = await fetchShareLinkManually({ jobKey: row.jobKey })
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.fetchLogisticsResult = { success: true, ...res.data }
|
||||||
|
this.$message.success('已请求物流接口,列表状态已更新')
|
||||||
|
this.getList()
|
||||||
|
} else {
|
||||||
|
this.fetchLogisticsResult = { success: false, error: res.msg || '失败' }
|
||||||
|
this.$message.error(res.msg || '获取失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.fetchLogisticsResult = { success: false, error: e.message || '请求异常' }
|
||||||
|
this.$message.error('获取物流失败: ' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.fetchLogisticsLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyFetchLogisticsResult() {
|
||||||
|
if (!this.fetchLogisticsResult) return
|
||||||
|
const t = JSON.stringify(this.fetchLogisticsResult, null, 2)
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(t).then(() => this.$message.success('已复制到剪贴板')).catch(() => this.fallbackCopy(t))
|
||||||
|
} else {
|
||||||
|
this.fallbackCopy(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fallbackCopy(text) {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.left = '-999999px'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('已复制到剪贴板')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('复制失败')
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
},
|
||||||
|
handleDrainQueueOnce() {
|
||||||
|
this.$modal.confirm('立即执行一轮 Redis 待扫描队列(与定时任务末尾逻辑相同,每批条数受后端 adhoc-pending-batch-size 限制)?').then(() => {
|
||||||
|
this.drainQueueLoading = true
|
||||||
|
return drainShareLinkPendingQueueOnce()
|
||||||
|
}).then(res => {
|
||||||
|
const d = res.data || {}
|
||||||
|
this.$modal.msgSuccess('已弹出处理 ' + (d.processedFromQueue != null ? d.processedFromQueue : 0) + ' 条')
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {}).finally(() => { this.drainQueueLoading = false })
|
||||||
|
},
|
||||||
|
handleBackfill() {
|
||||||
|
this.$modal.confirm('从「企微消息跟踪」补录历史分享链任务(状态 IMPORTED),已存在的 jobKey 将跳过。是否继续?').then(() => {
|
||||||
|
this.backfillLoading = true
|
||||||
|
return backfillShareLinkLogisticsFromTrace()
|
||||||
|
}).then(res => {
|
||||||
|
const d = res.data || {}
|
||||||
|
const parts = [
|
||||||
|
'扫描 ' + (d.scannedRemarkDoneRows != null ? d.scannedRemarkDoneRows : '—') + ' 条',
|
||||||
|
'新增 ' + (d.imported != null ? d.imported : 0),
|
||||||
|
'跳过已有 ' + (d.skippedDuplicate != null ? d.skippedDuplicate : 0),
|
||||||
|
'无短链 ' + (d.skippedNoUrl != null ? d.skippedNoUrl : 0)
|
||||||
|
]
|
||||||
|
this.$modal.msgSuccess(parts.join(','))
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {}).finally(() => { this.backfillLoading = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trace-pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.mb8 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,17 +24,19 @@
|
|||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="code" v-if="captchaEnabled">
|
<el-form-item prop="code" v-if="captchaEnabled">
|
||||||
<el-input
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
v-model="loginForm.code"
|
<el-input
|
||||||
auto-complete="off"
|
v-model="loginForm.code"
|
||||||
placeholder="验证码"
|
auto-complete="off"
|
||||||
style="width: 63%"
|
placeholder="验证码"
|
||||||
@keyup.enter.native="handleLogin"
|
style="flex: 1"
|
||||||
>
|
@keyup.enter.native="handleLogin"
|
||||||
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
|
>
|
||||||
</el-input>
|
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
|
||||||
<div class="login-code">
|
</el-input>
|
||||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
<div class="login-code">
|
||||||
|
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||||
@@ -73,8 +75,8 @@ export default {
|
|||||||
title: process.env.VUE_APP_TITLE,
|
title: process.env.VUE_APP_TITLE,
|
||||||
codeUrl: "",
|
codeUrl: "",
|
||||||
loginForm: {
|
loginForm: {
|
||||||
username: "admin",
|
username: "",
|
||||||
password: "admin123",
|
password: "",
|
||||||
rememberMe: false,
|
rememberMe: false,
|
||||||
code: "",
|
code: "",
|
||||||
uuid: ""
|
uuid: ""
|
||||||
@@ -142,7 +144,17 @@ export default {
|
|||||||
Cookies.remove('rememberMe')
|
Cookies.remove('rememberMe')
|
||||||
}
|
}
|
||||||
this.$store.dispatch("Login", this.loginForm).then(() => {
|
this.$store.dispatch("Login", this.loginForm).then(() => {
|
||||||
this.$router.push({ path: this.redirect || "/order/list" }).catch(()=>{})
|
// 先获取用户信息和生成路由,然后再跳转
|
||||||
|
this.$store.dispatch('GetInfo').then(() => {
|
||||||
|
this.$store.dispatch('GenerateRoutes').then(() => {
|
||||||
|
// 使用 replace 而不是 push,避免路由历史问题
|
||||||
|
const redirectPath = this.redirect || "/sloworder/index"
|
||||||
|
this.$router.replace(redirectPath).catch(() => {
|
||||||
|
// 如果目标路由不存在,跳转到默认路由
|
||||||
|
this.$router.replace("/sloworder/index")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
if (this.captchaEnabled) {
|
if (this.captchaEnabled) {
|
||||||
@@ -164,23 +176,55 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-image: url("../assets/images/login-background.jpg");
|
background-image: url("../assets/images/login-background.jpg");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
background-position: right center; /* 始终优先显示背景图右侧(含移动端) */
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 10vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
margin: 0px auto 30px auto;
|
margin: 0px auto 30px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #707070;
|
color: #707070;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0px auto 20px auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.75);
|
||||||
width: 400px;
|
backdrop-filter: blur(10px);
|
||||||
padding: 25px 25px 5px 25px;
|
width: 440px;
|
||||||
|
padding: 32px 32px 12px 32px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
// 移动端优化
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 24px 20px 12px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.el-input {
|
.el-input {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
input {
|
input {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 16px; // 防止iOS自动缩放
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.input-icon {
|
.input-icon {
|
||||||
@@ -188,6 +232,27 @@ export default {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-checkbox {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 44px; // 增大触摸目标
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.login-tip {
|
.login-tip {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -198,9 +263,22 @@ export default {
|
|||||||
width: 33%;
|
width: 33%;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 35%;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-login-footer {
|
.el-login-footer {
|
||||||
@@ -214,8 +292,18 @@ export default {
|
|||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 11px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.login-code-img {
|
.login-code-img {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
168
src/views/monitor/logfile/index.vue
Normal file
168
src/views/monitor/logfile/index.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>日志文件查看</span>
|
||||||
|
<span class="hint">通过 HTTPS 轮询读取最新内容,无需 SSH</span>
|
||||||
|
</div>
|
||||||
|
<el-form :inline="true" size="small" class="toolbar">
|
||||||
|
<el-form-item label="日志文件">
|
||||||
|
<el-select v-model="currentFile" placeholder="请选择" style="width: 180px" @change="handleFileChange">
|
||||||
|
<el-option
|
||||||
|
v-for="f in fileList"
|
||||||
|
:key="f"
|
||||||
|
:label="f"
|
||||||
|
:value="f"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="行数">
|
||||||
|
<el-input-number v-model="lines" :min="100" :max="5000" :step="100" style="width: 120px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-refresh" :loading="loading" @click="fetchLog">刷新</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="autoRefresh">自动刷新</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="autoRefresh">
|
||||||
|
<el-select v-model="refreshInterval" style="width: 100px" @change="restartTimer">
|
||||||
|
<el-option label="5 秒" :value="5" />
|
||||||
|
<el-option label="10 秒" :value="10" />
|
||||||
|
<el-option label="30 秒" :value="30" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div v-if="infoText" class="info-bar">
|
||||||
|
{{ infoText }}
|
||||||
|
</div>
|
||||||
|
<div class="log-wrap">
|
||||||
|
<pre ref="logPre" class="log-content">{{ logContent }}</pre>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listLogfiles, tailLogfile } from '@/api/monitor/logfile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LogfileViewer',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fileList: [],
|
||||||
|
currentFile: 'all.log',
|
||||||
|
lines: 200,
|
||||||
|
loading: false,
|
||||||
|
logContent: '',
|
||||||
|
totalLines: 0,
|
||||||
|
fromLine: 0,
|
||||||
|
toLine: 0,
|
||||||
|
autoRefresh: false,
|
||||||
|
refreshInterval: 10,
|
||||||
|
timer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
infoText() {
|
||||||
|
if (!this.currentFile || !this.totalLines) return ''
|
||||||
|
return `文件: ${this.currentFile} | 显示第 ${this.fromLine} - ${this.toLine} 行,共 ${this.totalLines} 行`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadFileList()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTimer()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadFileList() {
|
||||||
|
listLogfiles().then(res => {
|
||||||
|
const list = res.data || []
|
||||||
|
this.fileList = Array.isArray(list) ? list : []
|
||||||
|
if (this.fileList.length && !this.fileList.includes(this.currentFile)) {
|
||||||
|
this.currentFile = this.fileList[0]
|
||||||
|
}
|
||||||
|
this.fetchLog()
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.error('获取日志列表失败')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleFileChange() {
|
||||||
|
this.fetchLog()
|
||||||
|
},
|
||||||
|
fetchLog() {
|
||||||
|
if (!this.currentFile) return
|
||||||
|
this.loading = true
|
||||||
|
tailLogfile(this.currentFile, this.lines).then(res => {
|
||||||
|
const d = res.data || {}
|
||||||
|
this.logContent = d.content != null ? d.content : ''
|
||||||
|
this.totalLines = d.totalLines || 0
|
||||||
|
this.fromLine = d.fromLine || 0
|
||||||
|
this.toLine = d.toLine || 0
|
||||||
|
this.$nextTick(() => this.scrollToBottom())
|
||||||
|
}).catch(e => {
|
||||||
|
this.$message.error(e.msg || '读取日志失败')
|
||||||
|
this.logContent = ''
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrollToBottom() {
|
||||||
|
const el = this.$refs.logPre
|
||||||
|
if (el) el.scrollTop = el.scrollHeight
|
||||||
|
},
|
||||||
|
startTimer() {
|
||||||
|
this.stopTimer()
|
||||||
|
this.timer = setInterval(() => this.fetchLog(), this.refreshInterval * 1000)
|
||||||
|
},
|
||||||
|
stopTimer() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restartTimer() {
|
||||||
|
if (this.autoRefresh) this.startTimer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
autoRefresh(v) {
|
||||||
|
if (v) this.startTimer()
|
||||||
|
else this.stopTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.info-bar {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.log-wrap {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.log-content {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #d4d4d4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -171,23 +171,161 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12" class="card-box">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header">
|
||||||
|
<span><i class="el-icon-truck"></i> 物流服务健康度</span>
|
||||||
|
</div>
|
||||||
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
|
<table cellspacing="0" style="width: 100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务状态</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell">
|
||||||
|
<el-tag :type="health.logistics && health.logistics.healthy ? 'success' : 'danger'">
|
||||||
|
{{ health.logistics && health.logistics.status ? health.logistics.status : '未知' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务地址</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" style="word-break: break-all;">
|
||||||
|
{{ health.logistics && health.logistics.serviceUrl ? health.logistics.serviceUrl : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">状态信息</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" :class="{'text-danger': health.logistics && !health.logistics.healthy}">
|
||||||
|
{{ health.logistics && health.logistics.message ? health.logistics.message : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12" class="card-box">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header">
|
||||||
|
<span><i class="el-icon-message"></i> 微信推送服务健康度</span>
|
||||||
|
</div>
|
||||||
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
|
<table cellspacing="0" style="width: 100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务状态</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell">
|
||||||
|
<el-tag :type="health.wxSend && health.wxSend.healthy ? 'success' : 'danger'">
|
||||||
|
{{ health.wxSend && health.wxSend.status ? health.wxSend.status : '未知' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务地址</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" style="word-break: break-all;">
|
||||||
|
{{ health.wxSend && health.wxSend.serviceUrl ? health.wxSend.serviceUrl : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">状态信息</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" :class="{'text-danger': health.wxSend && !health.wxSend.healthy}">
|
||||||
|
{{ health.wxSend && health.wxSend.message ? health.wxSend.message : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- Ollama 服务健康度(调试用) -->
|
||||||
|
<el-col :span="12" class="card-box">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span><i class="el-icon-cpu"></i> Ollama 服务健康度(调试)</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 10px;"
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
:loading="ollamaTesting"
|
||||||
|
@click="testOllamaHealth"
|
||||||
|
>
|
||||||
|
{{ ollamaTesting ? '检测中...' : '测试' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
|
<table cellspacing="0" style="width: 100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务状态</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell">
|
||||||
|
<el-tag :type="health.ollama && health.ollama.healthy ? 'success' : (health.ollama ? 'danger' : 'info')">
|
||||||
|
{{ health.ollama && health.ollama.status ? health.ollama.status : '未检测' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务地址</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" style="word-break: break-all;">
|
||||||
|
{{ health.ollama && health.ollama.serviceUrl ? health.ollama.serviceUrl : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">状态信息</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" :class="{'text-danger': health.ollama && !health.ollama.healthy}">
|
||||||
|
{{ health.ollama && health.ollama.message ? health.ollama.message : '点击「测试」获取健康度' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getServer } from "@/api/monitor/server"
|
import { getServer, getHealth } from "@/api/monitor/server"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Server",
|
name: "Server",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 服务器信息
|
// 服务器信息
|
||||||
server: []
|
server: [],
|
||||||
|
// 健康度检测信息
|
||||||
|
health: {
|
||||||
|
logistics: null,
|
||||||
|
wxSend: null,
|
||||||
|
ollama: null
|
||||||
|
},
|
||||||
|
ollamaTesting: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList()
|
this.getList()
|
||||||
|
this.getHealthInfo()
|
||||||
this.openLoading()
|
this.openLoading()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -198,6 +336,39 @@ export default {
|
|||||||
this.$modal.closeLoading()
|
this.$modal.closeLoading()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/** 查询健康度检测信息 */
|
||||||
|
getHealthInfo() {
|
||||||
|
getHealth().then(response => {
|
||||||
|
if (response.data) {
|
||||||
|
this.health = response.data
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("获取健康度检测信息失败", error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 测试 Ollama 健康度(调试用) */
|
||||||
|
testOllamaHealth() {
|
||||||
|
this.ollamaTesting = true
|
||||||
|
getHealth()
|
||||||
|
.then(response => {
|
||||||
|
if (response.data) {
|
||||||
|
this.health = response.data
|
||||||
|
const ollama = response.data.ollama
|
||||||
|
if (ollama && ollama.healthy) {
|
||||||
|
this.$message.success('Ollama 服务正常')
|
||||||
|
} else {
|
||||||
|
this.$message.warning(ollama && ollama.message ? ollama.message : 'Ollama 服务异常或未配置')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Ollama 健康度检测失败', error)
|
||||||
|
this.$message.error('检测失败: ' + (error.message || '网络异常'))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.ollamaTesting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
// 打开加载层
|
// 打开加载层
|
||||||
openLoading() {
|
openLoading() {
|
||||||
this.$modal.loading("正在加载服务监控数据,请稍候!")
|
this.$modal.loading("正在加载服务监控数据,请稍候!")
|
||||||
|
|||||||
1255
src/views/public/CommentGenerator.vue
Normal file
1255
src/views/public/CommentGenerator.vue
Normal file
File diff suppressed because it is too large
Load Diff
528
src/views/public/OrderSearch.vue
Normal file
528
src/views/public/OrderSearch.vue
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-search-container">
|
||||||
|
<div class="search-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>订单搜索工具</h3>
|
||||||
|
<span class="header-desc">快速搜索下好的订单</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-form">
|
||||||
|
<el-form :model="searchForm" label-width="100px" label-position="top">
|
||||||
|
<el-form-item label="单号搜索">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.orderNo"
|
||||||
|
placeholder="请输入订单号/第三方单号/内部单号(至少5个字符)"
|
||||||
|
clearable
|
||||||
|
size="medium"
|
||||||
|
@keyup.enter.native="handleSearch"
|
||||||
|
@input="handleOrderNoInput"
|
||||||
|
/>
|
||||||
|
<div class="input-tip">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
至少输入5个字符,将自动过滤TF、H、F、PDD等搜索词
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="地址搜索">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.address"
|
||||||
|
placeholder="请输入收货地址关键词(至少3个字符)"
|
||||||
|
clearable
|
||||||
|
size="medium"
|
||||||
|
@keyup.enter.native="handleSearch"
|
||||||
|
@input="handleAddressInput"
|
||||||
|
/>
|
||||||
|
<div class="input-tip">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
至少输入3个字符进行模糊搜索
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleSearch"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!canSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="medium"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleReset"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<div v-if="hasSearched" class="result-section">
|
||||||
|
<el-divider>
|
||||||
|
<span>搜索结果(共 {{ total }} 条)</span>
|
||||||
|
</el-divider>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<i class="el-icon-loading"></i>
|
||||||
|
<span>搜索中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="orderList.length === 0" class="empty-container">
|
||||||
|
<el-empty description="未找到匹配的订单" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="order-list">
|
||||||
|
<el-table
|
||||||
|
:data="orderList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
:default-sort="{prop: 'createTime', order: 'descending'}"
|
||||||
|
>
|
||||||
|
<el-table-column label="内部单号" prop="remark" width="140" />
|
||||||
|
<el-table-column label="京东单号" prop="orderId" width="180" />
|
||||||
|
<el-table-column label="第三方单号" prop="thirdPartyOrderNo" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.thirdPartyOrderNo">{{ scope.row.thirdPartyOrderNo }}</span>
|
||||||
|
<span v-else style="color: #999;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="型号" prop="modelNumber" width="160" />
|
||||||
|
<el-table-column label="地址" prop="address" min-width="280" show-overflow-tooltip />
|
||||||
|
<el-table-column label="需要退款" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag
|
||||||
|
:type="scope.row.isRefunded === 1 ? 'warning' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ scope.row.isRefunded === 1 ? '需要退款' : '正常订单' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="赔付/价保" prop="proPriceAmount" width="110" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatAmount(scope.row.proPriceAmount) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="订单状态" prop="orderStatus" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag
|
||||||
|
v-if="scope.row.orderStatus != null"
|
||||||
|
:type="getOrderStatusType(scope.row.orderStatus)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getOrderStatusText(scope.row.orderStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else style="color: #999;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" prop="status" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="创建时间" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ parseTime(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="queryParams.pageNum"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页尾导航 -->
|
||||||
|
<PublicFooterNav />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listJDOrders, searchOrders } from '@/api/system/jdorder'
|
||||||
|
import { parseTime as formatTime } from '@/utils/ruoyi'
|
||||||
|
import PublicFooterNav from '@/components/PublicFooterNav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OrderSearch',
|
||||||
|
components: {
|
||||||
|
PublicFooterNav
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchForm: {
|
||||||
|
orderNo: '',
|
||||||
|
address: ''
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
hasSearched: false,
|
||||||
|
orderList: [],
|
||||||
|
total: 0,
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 20
|
||||||
|
},
|
||||||
|
// 需要过滤的搜索词
|
||||||
|
filteredKeywords: ['TF', 'H', 'F', 'PDD']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canSearch() {
|
||||||
|
const orderNo = (this.searchForm.orderNo || '').trim()
|
||||||
|
const address = (this.searchForm.address || '').trim()
|
||||||
|
|
||||||
|
// 至少有一个搜索条件满足要求
|
||||||
|
const orderNoValid = orderNo.length >= 5 && !this.isFilteredKeyword(orderNo)
|
||||||
|
const addressValid = address.length >= 3
|
||||||
|
|
||||||
|
return orderNoValid || addressValid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 检查是否是过滤的关键词
|
||||||
|
isFilteredKeyword(keyword) {
|
||||||
|
const upperKeyword = keyword.toUpperCase()
|
||||||
|
return this.filteredKeywords.some(kw => upperKeyword.includes(kw.toUpperCase()))
|
||||||
|
},
|
||||||
|
// 处理单号输入
|
||||||
|
handleOrderNoInput(value) {
|
||||||
|
// 如果输入的是过滤关键词,清空
|
||||||
|
if (value && value.length < 5) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isFilteredKeyword(value)) {
|
||||||
|
this.$message.warning('该搜索词已被过滤,请输入其他关键词')
|
||||||
|
this.searchForm.orderNo = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理地址输入
|
||||||
|
handleAddressInput(value) {
|
||||||
|
// 地址输入不需要特殊处理
|
||||||
|
},
|
||||||
|
// 执行搜索
|
||||||
|
async handleSearch() {
|
||||||
|
if (!this.canSearch) {
|
||||||
|
this.$message.warning('请输入有效的搜索条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderNo = (this.searchForm.orderNo || '').trim()
|
||||||
|
const address = (this.searchForm.address || '').trim()
|
||||||
|
|
||||||
|
// 验证搜索条件
|
||||||
|
if (orderNo && (orderNo.length < 5 || this.isFilteredKeyword(orderNo))) {
|
||||||
|
this.$message.warning('单号搜索至少需要5个字符,且不能包含TF、H、F、PDD等关键词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address && address.length < 3) {
|
||||||
|
this.$message.warning('地址搜索至少需要3个字符')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orderNo && !address) {
|
||||||
|
this.$message.warning('请至少输入一个搜索条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.hasSearched = true
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建查询参数
|
||||||
|
const queryParams = {
|
||||||
|
pageNum: this.queryParams.pageNum,
|
||||||
|
pageSize: this.queryParams.pageSize,
|
||||||
|
orderSearch: orderNo || undefined,
|
||||||
|
address: address || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用专门的搜索接口
|
||||||
|
const res = await searchOrders(queryParams)
|
||||||
|
|
||||||
|
console.log('搜索响应:', res) // 调试用
|
||||||
|
|
||||||
|
// 判断响应是否成功(兼容多种响应格式)
|
||||||
|
const isSuccess = res && (
|
||||||
|
res.code === 200 ||
|
||||||
|
res.code === 0 ||
|
||||||
|
res.msg === '操作成功' ||
|
||||||
|
res.msg === '查询成功' ||
|
||||||
|
(res.rows && Array.isArray(res.rows))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
const list = (res.rows || res.data || [])
|
||||||
|
console.log('解析到的列表:', list) // 调试用
|
||||||
|
|
||||||
|
// 处理退款相关字段的默认值
|
||||||
|
this.orderList = list.map(item => ({
|
||||||
|
...item,
|
||||||
|
isRefunded: item.isRefunded != null ? item.isRefunded : 0,
|
||||||
|
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
|
||||||
|
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.total = res.total || 0
|
||||||
|
|
||||||
|
console.log('最终订单列表:', this.orderList) // 调试用
|
||||||
|
console.log('总数:', this.total) // 调试用
|
||||||
|
|
||||||
|
if (this.orderList.length === 0) {
|
||||||
|
this.$message.info('未找到匹配的订单')
|
||||||
|
} else {
|
||||||
|
this.$message.success(`找到 ${this.total} 条匹配的订单`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('响应判断失败:', res) // 调试用
|
||||||
|
this.$message.error(res && res.msg ? res.msg : '搜索失败')
|
||||||
|
this.orderList = []
|
||||||
|
this.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error)
|
||||||
|
this.$message.error('搜索失败,请稍后重试')
|
||||||
|
this.orderList = []
|
||||||
|
this.total = 0
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 重置搜索
|
||||||
|
handleReset() {
|
||||||
|
this.searchForm = {
|
||||||
|
orderNo: '',
|
||||||
|
address: ''
|
||||||
|
}
|
||||||
|
this.orderList = []
|
||||||
|
this.total = 0
|
||||||
|
this.hasSearched = false
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.queryParams.pageSize = 20
|
||||||
|
},
|
||||||
|
// 分页大小变化
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.queryParams.pageSize = val
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.handleSearch()
|
||||||
|
},
|
||||||
|
// 当前页变化
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.queryParams.pageNum = val
|
||||||
|
this.handleSearch()
|
||||||
|
},
|
||||||
|
// 格式化金额
|
||||||
|
formatAmount(amount) {
|
||||||
|
if (amount == null || amount === '') return '-'
|
||||||
|
const num = Number(amount)
|
||||||
|
if (Number.isNaN(num)) return amount
|
||||||
|
return num.toFixed(2)
|
||||||
|
},
|
||||||
|
// 解析时间
|
||||||
|
parseTime(time) {
|
||||||
|
if (!time) return '-'
|
||||||
|
return formatTime(time, '{y}-{m}-{d} {h}:{i}:{s}')
|
||||||
|
},
|
||||||
|
// 获取订单状态文本
|
||||||
|
getOrderStatusText(status) {
|
||||||
|
if (status == null) return '-'
|
||||||
|
const statusMap = {
|
||||||
|
'-100': '无变化',
|
||||||
|
'-1': '未知',
|
||||||
|
2: '无效-拆单',
|
||||||
|
3: '无效-取消',
|
||||||
|
4: '无效-京东帮帮主订单',
|
||||||
|
5: '无效-账号异常',
|
||||||
|
6: '无效-赠品类目不返佣',
|
||||||
|
7: '无效-校园订单',
|
||||||
|
8: '无效-企业订单',
|
||||||
|
9: '无效-团购订单',
|
||||||
|
11: '无效-乡村推广员下单',
|
||||||
|
13: '违规订单-其他',
|
||||||
|
14: '无效-来源与备案网址不符',
|
||||||
|
15: '待付款',
|
||||||
|
16: '已付款',
|
||||||
|
17: '已完成',
|
||||||
|
19: '无效-佣金比例为0',
|
||||||
|
20: '无效-此复购订单对应的首购订单无效',
|
||||||
|
21: '无效-云店订单',
|
||||||
|
22: '无效-PLUS会员佣金比例为0',
|
||||||
|
23: '无效-支付有礼',
|
||||||
|
24: '已付定金',
|
||||||
|
25: '违规订单-流量劫持',
|
||||||
|
26: '违规订单-流量异常',
|
||||||
|
27: '违规订单-违反京东平台规则',
|
||||||
|
28: '违规订单-多笔交易异常',
|
||||||
|
29: '无效-跨屏跨店',
|
||||||
|
30: '无效-累计件数超出类目上限',
|
||||||
|
31: '无效-黑名单sku',
|
||||||
|
33: '超市卡充值订单',
|
||||||
|
34: '无效-推卡订单无效'
|
||||||
|
}
|
||||||
|
return statusMap[status] || `状态${status}`
|
||||||
|
},
|
||||||
|
// 获取订单状态类型
|
||||||
|
getOrderStatusType(status) {
|
||||||
|
if (status == null) return 'info'
|
||||||
|
// 取消状态(优先级最高)
|
||||||
|
if (status === 3) return 'danger' // 无效-取消(红色,优先级高于违规)
|
||||||
|
// 正常状态
|
||||||
|
if (status === 16) return 'success' // 已付款
|
||||||
|
if (status === 17) return 'success' // 已完成
|
||||||
|
if (status === 15) return 'warning' // 待付款
|
||||||
|
if (status === 24) return 'warning' // 已付定金
|
||||||
|
// 违规状态
|
||||||
|
if ([13, 25, 26, 27, 28].includes(status)) return 'warning' // 违规订单(黄色,优先级低于取消)
|
||||||
|
// 无效状态
|
||||||
|
if ([2, 4, 5, 6, 7, 8, 9, 11, 14, 19, 20, 21, 22, 23, 29, 30, 31, 34].includes(status)) return 'info' // 无效订单(灰色)
|
||||||
|
// 其他状态
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-search-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: calc(80px + 20px); /* 为页尾导航留出空间 */
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
max-width: 1400px;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-tip i {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
animation: rotating 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotating {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.order-search-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
241
src/views/public/PublicHome.vue
Normal file
241
src/views/public/PublicHome.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="public-home-container">
|
||||||
|
<div class="home-content">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="main-title">工具中心</h1>
|
||||||
|
<p class="subtitle">选择您需要的功能</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tools-grid">
|
||||||
|
<div
|
||||||
|
v-for="tool in tools"
|
||||||
|
:key="tool.path"
|
||||||
|
class="tool-card"
|
||||||
|
@click="handleToolClick(tool.path)"
|
||||||
|
>
|
||||||
|
<div class="tool-icon-wrapper">
|
||||||
|
<i :class="tool.icon"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="tool-title">{{ tool.title }}</h3>
|
||||||
|
<p class="tool-desc">{{ tool.description }}</p>
|
||||||
|
<div class="tool-footer">
|
||||||
|
<span class="tool-action">点击使用 <i class="el-icon-arrow-right"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页尾导航 -->
|
||||||
|
<PublicFooterNav />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PublicFooterNav from '@/components/PublicFooterNav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PublicHome',
|
||||||
|
components: {
|
||||||
|
PublicFooterNav
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
title: '评论生成',
|
||||||
|
description: '快速生成产品评论内容',
|
||||||
|
path: '/tools/comment-gen',
|
||||||
|
icon: 'el-icon-edit-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单提交',
|
||||||
|
description: '提交订单信息,录入系统',
|
||||||
|
path: '/public/order-submit',
|
||||||
|
icon: 'el-icon-upload2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单搜索',
|
||||||
|
description: '快速搜索订单信息',
|
||||||
|
path: '/tools/order-search',
|
||||||
|
icon: 'el-icon-search'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleToolClick(path) {
|
||||||
|
this.$router.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.public-home-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 40px 20px 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon-wrapper {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover .tool-icon-wrapper {
|
||||||
|
transform: scale(1.1) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon-wrapper i {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-footer {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-action {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-action i {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover .tool-action i {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.public-home-container {
|
||||||
|
padding: 24px 16px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon-wrapper {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon-wrapper i {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.main-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
501
src/views/public/order-submit/index.vue
Normal file
501
src/views/public/order-submit/index.vue
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
<template>
|
||||||
|
<div class="public-order-container">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>订单提交台</span>
|
||||||
|
<span class="header-desc">请按照格式提交订单信息</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="80px" label-position="top">
|
||||||
|
<el-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<span>输入订单信息</span>
|
||||||
|
<el-tag type="warning" size="mini" style="margin-left: 10px;">只能提交今天的订单</el-tag>
|
||||||
|
</template>
|
||||||
|
<el-input
|
||||||
|
v-model="form.command"
|
||||||
|
type="textarea"
|
||||||
|
:rows="12"
|
||||||
|
:placeholder="getPlaceholder()"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitOrder" :loading="loading" size="medium">
|
||||||
|
<i class="el-icon-upload"></i> 提交订单
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="clearAll" size="medium">
|
||||||
|
<i class="el-icon-delete"></i> 清空
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-divider>响应结果</el-divider>
|
||||||
|
|
||||||
|
<div v-if="resultList.length === 0" style="padding: 12px 0;">
|
||||||
|
<el-empty description="暂无响应" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(msg, idx) in resultList" :key="idx" class="msg-block">
|
||||||
|
<div class="msg-header">
|
||||||
|
<span>第 {{ idx + 1 }} 段</span>
|
||||||
|
<el-button size="mini" type="success" @click="copyOne(msg)">复制此段</el-button>
|
||||||
|
</div>
|
||||||
|
<el-input :value="msg" type="textarea" :rows="8" readonly />
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<el-button size="mini" type="primary" @click="copyAll">复制全部</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用说明 -->
|
||||||
|
<el-divider>使用说明</el-divider>
|
||||||
|
<div class="usage-guide">
|
||||||
|
<el-collapse>
|
||||||
|
<el-collapse-item title="订单格式说明" name="1">
|
||||||
|
<div class="guide-content">
|
||||||
|
<p><strong>请严格按照以下格式填写订单信息:</strong></p>
|
||||||
|
<pre class="format-example">单:
|
||||||
|
{{ getTodayDate() }} 001
|
||||||
|
备注:测试订单
|
||||||
|
分销标记:H-TF
|
||||||
|
型号:ZQD180F-EB200
|
||||||
|
链接:https://item.jd.com/...
|
||||||
|
下单付款:1650
|
||||||
|
后返金额:50
|
||||||
|
地址:张三13800138000上海市浦东新区张江高科技园区...
|
||||||
|
物流链接:https://...
|
||||||
|
订单号:1234567890
|
||||||
|
下单人:张三</pre>
|
||||||
|
<p class="tips"><i class="el-icon-warning"></i> 重要提示:订单日期必须是今天({{ getTodayDate() }}),每个字段都不能省略</p>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item title="注意事项" name="2">
|
||||||
|
<div class="guide-content">
|
||||||
|
<ul>
|
||||||
|
<li><strong style="color: #E6A23C;">只能提交今天的订单,历史订单不允许提交</strong></li>
|
||||||
|
<li>请确保订单信息准确无误</li>
|
||||||
|
<li>每次只能提交一个订单</li>
|
||||||
|
<li>提交成功后会显示确认信息</li>
|
||||||
|
<li>如遇错误,请检查格式和日期是否正确</li>
|
||||||
|
<li>限流策略:每半小时最多提交120个订单</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 地址重复验证码弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
title="地址重复验证"
|
||||||
|
:visible.sync="verifyDialogVisible"
|
||||||
|
width="400px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<el-alert
|
||||||
|
:title="verifyMessage"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
/>
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #409EFF; margin: 20px 0;">
|
||||||
|
{{ verifyCode }}
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="verifyInput"
|
||||||
|
placeholder="请输入上方四位数字验证码"
|
||||||
|
maxlength="4"
|
||||||
|
style="width: 200px;"
|
||||||
|
@keyup.enter.native="handleVerify"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="verifyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleVerify" :loading="verifyLoading">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 页尾导航 -->
|
||||||
|
<PublicFooterNav />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { submitPublicOrder, submitPublicOrderWithForce } from '@/api/public/order'
|
||||||
|
import PublicFooterNav from '@/components/PublicFooterNav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PublicOrderSubmit',
|
||||||
|
components: {
|
||||||
|
PublicFooterNav
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: { command: '' },
|
||||||
|
loading: false,
|
||||||
|
resultList: [],
|
||||||
|
// 验证码相关
|
||||||
|
verifyDialogVisible: false,
|
||||||
|
verifyCode: '',
|
||||||
|
verifyInput: '',
|
||||||
|
verifyMessage: '',
|
||||||
|
verifyLoading: false,
|
||||||
|
pendingCommand: '' // 待执行的命令(验证通过后执行)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getPlaceholder() {
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
return `请按照以下格式输入订单信息(注意:订单日期必须是今天 ${today}):
|
||||||
|
单:
|
||||||
|
${today} 001
|
||||||
|
备注:测试订单
|
||||||
|
分销标记:H-TF
|
||||||
|
型号:ZQD180F-EB200
|
||||||
|
链接:https://...
|
||||||
|
下单付款:1650
|
||||||
|
后返金额:50
|
||||||
|
地址:张三13800138000上海市浦东新区...
|
||||||
|
物流链接:https://...
|
||||||
|
订单号:1234567890
|
||||||
|
下单人:张三`
|
||||||
|
},
|
||||||
|
copyOne(text) {
|
||||||
|
if (!text) return
|
||||||
|
this.doCopy(text)
|
||||||
|
},
|
||||||
|
copyAll() {
|
||||||
|
if (!this.resultList || this.resultList.length === 0) return
|
||||||
|
const text = this.resultList.join('\n\n')
|
||||||
|
this.doCopy(text)
|
||||||
|
},
|
||||||
|
doCopy(text) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$message.success('复制成功')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopyText(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopyText(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fallbackCopyText(text) {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.focus()
|
||||||
|
ta.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('复制成功')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('复制失败')
|
||||||
|
}
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
},
|
||||||
|
submitOrder() {
|
||||||
|
const cmd = (this.form.command || '').trim()
|
||||||
|
if (!cmd) {
|
||||||
|
this.$message.error('请输入订单信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否以"单:"开头
|
||||||
|
if (!cmd.startsWith('单:') && !cmd.startsWith('单:')) {
|
||||||
|
this.$message.error('订单信息必须以"单:"开头')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
submitPublicOrder({ command: cmd }).then(res => {
|
||||||
|
this.loading = false
|
||||||
|
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||||
|
const data = res.data
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
this.resultList = data
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
this.resultList = data ? [data] : []
|
||||||
|
} else {
|
||||||
|
this.resultList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是地址重复错误
|
||||||
|
if (this.checkAddressDuplicate(this.resultList)) {
|
||||||
|
// 显示验证码弹窗
|
||||||
|
this.showVerifyDialog(cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有警告信息
|
||||||
|
this.checkWarningAlert(this.resultList)
|
||||||
|
|
||||||
|
// 如果没有警告,显示成功提示
|
||||||
|
if (!this.hasWarning(this.resultList)) {
|
||||||
|
this.$message.success('订单提交成功')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res && res.msg ? res.msg : '提交失败')
|
||||||
|
this.resultList = []
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.loading = false
|
||||||
|
const errorMsg = error.response?.data?.msg || error.message || '提交失败,请稍后重试'
|
||||||
|
this.$message.error(errorMsg)
|
||||||
|
this.resultList = []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 检查是否是地址重复错误
|
||||||
|
checkAddressDuplicate(resultList) {
|
||||||
|
if (!resultList || resultList.length === 0) return false
|
||||||
|
for (let i = 0; i < resultList.length; i++) {
|
||||||
|
const result = resultList[i]
|
||||||
|
if (typeof result === 'string' && result.startsWith('ERROR_CODE:ADDRESS_DUPLICATE')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
// 显示验证码弹窗
|
||||||
|
showVerifyDialog(command) {
|
||||||
|
// 生成四位随机数字验证码
|
||||||
|
this.verifyCode = String(Math.floor(1000 + Math.random() * 9000))
|
||||||
|
this.verifyInput = ''
|
||||||
|
this.verifyMessage = '检测到地址重复,请输入验证码以强制生成表单'
|
||||||
|
this.pendingCommand = command
|
||||||
|
this.verifyDialogVisible = true
|
||||||
|
},
|
||||||
|
// 处理验证码验证
|
||||||
|
handleVerify() {
|
||||||
|
if (!this.verifyInput || this.verifyInput.length !== 4) {
|
||||||
|
this.$message.error('请输入四位数字验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.verifyInput !== this.verifyCode) {
|
||||||
|
this.$message.error('验证码错误,请重新输入')
|
||||||
|
this.verifyInput = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过,使用forceGenerate参数重新提交
|
||||||
|
this.verifyLoading = true
|
||||||
|
submitPublicOrderWithForce({ command: this.pendingCommand }).then(res => {
|
||||||
|
this.verifyLoading = false
|
||||||
|
this.verifyDialogVisible = false
|
||||||
|
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||||
|
const data = res.data
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
this.resultList = data
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
this.resultList = data ? [data] : []
|
||||||
|
} else {
|
||||||
|
this.resultList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有警告信息
|
||||||
|
this.checkWarningAlert(this.resultList)
|
||||||
|
|
||||||
|
// 如果没有警告,显示成功提示
|
||||||
|
if (!this.hasWarning(this.resultList)) {
|
||||||
|
this.$message.success('订单提交成功(已强制生成)')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res && res.msg ? res.msg : '提交失败')
|
||||||
|
this.resultList = []
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.verifyLoading = false
|
||||||
|
const errorMsg = error.response?.data?.msg || error.message || '提交失败,请稍后重试'
|
||||||
|
this.$message.error(errorMsg)
|
||||||
|
this.resultList = []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearAll() {
|
||||||
|
this.form.command = ''
|
||||||
|
this.resultList = []
|
||||||
|
},
|
||||||
|
checkWarningAlert(resultList) {
|
||||||
|
if (!resultList || resultList.length === 0) return
|
||||||
|
|
||||||
|
// 检查是否有以[炸弹]开头的警告消息
|
||||||
|
const warningMessages = resultList
|
||||||
|
.filter(msg => {
|
||||||
|
return msg && typeof msg === 'string' && msg.trim().includes('[炸弹]')
|
||||||
|
})
|
||||||
|
.map(msg => {
|
||||||
|
// 移除所有的[炸弹]标记
|
||||||
|
return msg.trim().replace(/\[炸弹\]\s*/g, '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (warningMessages.length > 0) {
|
||||||
|
// 显示警告弹窗
|
||||||
|
this.$alert(warningMessages.join('\n\n'), '⚠️ 警告提示', {
|
||||||
|
confirmButtonText: '我已知晓',
|
||||||
|
type: 'warning',
|
||||||
|
center: true,
|
||||||
|
customClass: 'warning-alert-dialog',
|
||||||
|
showClose: false,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
closeOnPressEscape: false,
|
||||||
|
dangerouslyUseHTMLString: false
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasWarning(resultList) {
|
||||||
|
if (!resultList || resultList.length === 0) return false
|
||||||
|
return resultList.some(msg => msg && typeof msg === 'string' && msg.trim().includes('[炸弹]'))
|
||||||
|
},
|
||||||
|
getTodayDate() {
|
||||||
|
return new Date().toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.public-order-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: calc(80px + 20px); /* 为页尾导航留出空间 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix span:first-child {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-block {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 6px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-guide {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content p {
|
||||||
|
margin: 10px 0;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-example {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #f5dab1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content ul li {
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content ul li:before {
|
||||||
|
content: "•";
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式:警告弹窗 */
|
||||||
|
.warning-alert-dialog {
|
||||||
|
width: 80vw !important;
|
||||||
|
max-width: 800px !important;
|
||||||
|
min-width: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-alert-dialog .el-message-box__header {
|
||||||
|
padding: 20px 20px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-alert-dialog .el-message-box__title {
|
||||||
|
font-size: 18px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-alert-dialog .el-message-box__message {
|
||||||
|
font-size: 15px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #e6a23c !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
max-height: 60vh !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
padding: 15px 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-alert-dialog .el-message-box__btns {
|
||||||
|
text-align: center !important;
|
||||||
|
padding: 15px 20px 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-alert-dialog .el-message-box__btns .el-button {
|
||||||
|
padding: 12px 40px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
169
src/views/public/tendoc-callback.vue
Normal file
169
src/views/public/tendoc-callback.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div style="padding: 40px; text-align: center; font-family: Arial, sans-serif;">
|
||||||
|
<div v-if="loading" style="margin-top: 100px;">
|
||||||
|
<i class="el-icon-loading" style="font-size: 48px; color: #409EFF;"></i>
|
||||||
|
<p style="margin-top: 20px; font-size: 16px; color: #666;">正在处理授权...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="success" style="margin-top: 100px;">
|
||||||
|
<i class="el-icon-success" style="font-size: 64px; color: #67C23A;"></i>
|
||||||
|
<h2 style="margin-top: 20px; color: #67C23A;">授权成功!</h2>
|
||||||
|
<div style="margin-top: 30px; padding: 20px; background: #f5f7fa; border-radius: 8px; max-width: 600px; margin-left: auto; margin-right: auto;">
|
||||||
|
<p style="margin-bottom: 15px; font-size: 14px; color: #666;">请复制以下访问令牌:</p>
|
||||||
|
<el-input
|
||||||
|
:value="accessToken"
|
||||||
|
readonly
|
||||||
|
style="margin-bottom: 15px;"
|
||||||
|
>
|
||||||
|
<template slot="append">
|
||||||
|
<el-button @click="copyToken" icon="el-icon-document-copy">复制</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<p style="font-size: 12px; color: #999; margin-top: 10px;">
|
||||||
|
访问令牌有效期:{{ expiresIn }} 秒
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 30px;">
|
||||||
|
<el-button type="primary" @click="closeWindow">关闭窗口</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else style="margin-top: 100px;">
|
||||||
|
<i class="el-icon-error" style="font-size: 64px; color: #F56C6C;"></i>
|
||||||
|
<h2 style="margin-top: 20px; color: #F56C6C;">授权失败</h2>
|
||||||
|
<p style="margin-top: 20px; color: #666;">{{ errorMessage }}</p>
|
||||||
|
<div style="margin-top: 30px;">
|
||||||
|
<el-button type="primary" @click="closeWindow">关闭窗口</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getTencentDocAccessToken } from '@/api/jarvis/tendoc'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TencentDocCallback',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
success: false,
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
expiresIn: 0,
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.handleCallback()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleCallback() {
|
||||||
|
// 从URL参数中获取code和state
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const code = urlParams.get('code')
|
||||||
|
const state = urlParams.get('state')
|
||||||
|
const error = urlParams.get('error')
|
||||||
|
const errorDescription = urlParams.get('error_description')
|
||||||
|
|
||||||
|
// 处理授权错误
|
||||||
|
if (error) {
|
||||||
|
this.loading = false
|
||||||
|
this.errorMessage = errorDescription || error || '授权失败'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证授权码
|
||||||
|
if (!code) {
|
||||||
|
this.loading = false
|
||||||
|
this.errorMessage = '未收到授权码,请重新授权'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用后端接口获取访问令牌
|
||||||
|
const res = await getTencentDocAccessToken(code)
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
this.accessToken = data.access_token || data.accessToken || ''
|
||||||
|
this.refreshToken = data.refresh_token || data.refreshToken || ''
|
||||||
|
this.expiresIn = data.expires_in || data.expiresIn || 0
|
||||||
|
|
||||||
|
// 保存到localStorage,供主窗口使用
|
||||||
|
if (this.accessToken) {
|
||||||
|
localStorage.setItem('tendoc_access_token', this.accessToken)
|
||||||
|
if (this.refreshToken) {
|
||||||
|
localStorage.setItem('tendoc_refresh_token', this.refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父窗口(如果是从弹窗打开的)
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage({
|
||||||
|
type: 'tendoc_auth_success',
|
||||||
|
access_token: this.accessToken,
|
||||||
|
refresh_token: this.refreshToken,
|
||||||
|
expires_in: this.expiresIn
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.success = true
|
||||||
|
} else {
|
||||||
|
this.errorMessage = res.msg || '获取访问令牌失败'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取访问令牌失败', e)
|
||||||
|
this.errorMessage = e.message || '获取访问令牌失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
copyToken() {
|
||||||
|
if (this.accessToken) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(this.accessToken).then(() => {
|
||||||
|
this.$message.success('访问令牌已复制到剪贴板')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopy(this.accessToken)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopy(this.accessToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fallbackCopy(text) {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.left = '-999999px'
|
||||||
|
textArea.style.top = '-999999px'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('访问令牌已复制到剪贴板')
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
},
|
||||||
|
|
||||||
|
closeWindow() {
|
||||||
|
if (window.opener) {
|
||||||
|
window.close()
|
||||||
|
} else {
|
||||||
|
// 如果不是弹窗,跳转到订单列表页面
|
||||||
|
this.$router.push('/system/jdorder/orderList')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
689
src/views/system/comment/index.vue
Normal file
689
src/views/system/comment/index.vue
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||||
|
<!-- 京东评论标签页 -->
|
||||||
|
<el-tab-pane label="京东评论" name="jd">
|
||||||
|
<div class="comment-container">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<mobile-search-form
|
||||||
|
:model="jdQueryParams"
|
||||||
|
@search="handleJdQuery"
|
||||||
|
@reset="resetJdQuery"
|
||||||
|
>
|
||||||
|
<template #form="{ expanded }">
|
||||||
|
<el-form
|
||||||
|
:inline="true"
|
||||||
|
:model="jdQueryParams"
|
||||||
|
class="demo-form-inline"
|
||||||
|
size="small"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="商品ID">
|
||||||
|
<el-input v-model="jdQueryParams.productId" placeholder="商品ID" clearable @keyup.enter.native="handleJdQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品类型">
|
||||||
|
<el-select v-model="jdQueryParams.productType" placeholder="请选择" clearable filterable style="width: 200px;">
|
||||||
|
<el-option
|
||||||
|
v-for="(value, key) in jdProductTypeMap"
|
||||||
|
:key="key"
|
||||||
|
:label="key"
|
||||||
|
:value="key">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="jdQueryParams.userName" placeholder="用户名" clearable @keyup.enter.native="handleJdQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用状态">
|
||||||
|
<el-select v-model="jdQueryParams.isUse" placeholder="全部" clearable style="width: 120px;">
|
||||||
|
<el-option label="未使用" :value="0" />
|
||||||
|
<el-option label="已使用" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="jdDateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
@change="handleJdDateRangeChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 桌面端搜索按钮 -->
|
||||||
|
<el-form-item v-if="!expanded">
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="small" @click="handleJdQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="small" @click="resetJdQuery">重置</el-button>
|
||||||
|
<el-button type="warning" icon="el-icon-upload2" size="small" :loading="jdFetchLoading" @click="handleJdFetchComments">获取评论</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-download" size="small" @click="handleJdExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</mobile-search-form>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
|
<div class="action-buttons-section mobile-only">
|
||||||
|
<mobile-button-group
|
||||||
|
:buttons="jdActionButtons"
|
||||||
|
:primary-count="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<div class="desktop-action-buttons desktop-only">
|
||||||
|
<el-button type="warning" icon="el-icon-upload2" size="small" :loading="jdFetchLoading" @click="handleJdFetchComments">获取评论</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-download" size="small" @click="handleJdExport">导出</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<el-table v-loading="jdLoading" :data="jdList" border>
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="ID" prop="id" width="80" />
|
||||||
|
<el-table-column label="产品类型" prop="productType" width="150" />
|
||||||
|
<el-table-column label="商品ID" prop="productId" width="200" />
|
||||||
|
<el-table-column label="用户名" prop="userName" width="120" />
|
||||||
|
<el-table-column label="评论内容" prop="commentText" min-width="300" show-overflow-tooltip />
|
||||||
|
<el-table-column label="评论ID" prop="commentId" width="150" />
|
||||||
|
<el-table-column label="图片" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button v-if="scope.row.pictureUrls" type="text" @click="viewImages(scope.row.pictureUrls)">查看图片</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用状态" prop="isUse" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.isUse === 0 ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.isUse === 0 ? '未使用' : '已使用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="createdAt" width="180" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
:type="scope.row.isUse === 0 ? 'warning' : 'success'"
|
||||||
|
@click="toggleJdCommentUse(scope.row)"
|
||||||
|
>
|
||||||
|
{{ scope.row.isUse === 0 ? '标记已使用' : '标记未使用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="danger"
|
||||||
|
@click="handleJdDelete(scope.row)"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<pagination
|
||||||
|
v-show="jdTotal > 0"
|
||||||
|
:total="jdTotal"
|
||||||
|
:page.sync="jdQueryParams.pageNum"
|
||||||
|
:limit.sync="jdQueryParams.pageSize"
|
||||||
|
@pagination="getJdList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 淘宝评论标签页 -->
|
||||||
|
<el-tab-pane label="淘宝评论" name="tb">
|
||||||
|
<div class="comment-container">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<mobile-search-form
|
||||||
|
:model="tbQueryParams"
|
||||||
|
@search="handleTbQuery"
|
||||||
|
@reset="resetTbQuery"
|
||||||
|
>
|
||||||
|
<template #form="{ expanded }">
|
||||||
|
<el-form
|
||||||
|
:inline="true"
|
||||||
|
:model="tbQueryParams"
|
||||||
|
class="demo-form-inline"
|
||||||
|
size="small"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="商品ID">
|
||||||
|
<el-input v-model="tbQueryParams.productId" placeholder="商品ID" clearable @keyup.enter.native="handleTbQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品类型">
|
||||||
|
<el-select v-model="tbQueryParams.productType" placeholder="请选择" clearable filterable style="width: 200px;">
|
||||||
|
<el-option
|
||||||
|
v-for="(value, key) in tbProductTypeMap"
|
||||||
|
:key="key"
|
||||||
|
:label="key"
|
||||||
|
:value="key">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="tbQueryParams.userName" placeholder="用户名" clearable @keyup.enter.native="handleTbQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用状态">
|
||||||
|
<el-select v-model="tbQueryParams.isUse" placeholder="全部" clearable style="width: 120px;">
|
||||||
|
<el-option label="未使用" :value="0" />
|
||||||
|
<el-option label="已使用" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="tbDateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
@change="handleTbDateRangeChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 桌面端搜索按钮 -->
|
||||||
|
<el-form-item v-if="!expanded">
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="small" @click="handleTbQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="small" @click="resetTbQuery">重置</el-button>
|
||||||
|
<el-button type="warning" icon="el-icon-upload2" size="small" :loading="tbFetchLoading" @click="handleTbFetchComments">获取评论</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-download" size="small" @click="handleTbExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</mobile-search-form>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
|
<div class="action-buttons-section mobile-only">
|
||||||
|
<mobile-button-group
|
||||||
|
:buttons="tbActionButtons"
|
||||||
|
:primary-count="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<div class="desktop-action-buttons desktop-only">
|
||||||
|
<el-button type="warning" icon="el-icon-upload2" size="small" :loading="tbFetchLoading" @click="handleTbFetchComments">获取评论</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-download" size="small" @click="handleTbExport">导出</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<el-table v-loading="tbLoading" :data="tbList" border>
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="ID" prop="id" width="80" />
|
||||||
|
<el-table-column label="产品类型" prop="productType" width="150" />
|
||||||
|
<el-table-column label="商品ID" prop="productId" width="200" />
|
||||||
|
<el-table-column label="用户名" prop="userName" width="120" />
|
||||||
|
<el-table-column label="评论内容" prop="commentText" min-width="300" show-overflow-tooltip />
|
||||||
|
<el-table-column label="评论ID" prop="commentId" width="150" />
|
||||||
|
<el-table-column label="图片" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button v-if="scope.row.pictureUrls" type="text" @click="viewImages(scope.row.pictureUrls)">查看图片</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用状态" prop="isUse" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.isUse === 0 ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.isUse === 0 ? '未使用' : '已使用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="createdAt" width="180" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
:type="scope.row.isUse === 0 ? 'warning' : 'success'"
|
||||||
|
@click="toggleTbCommentUse(scope.row)"
|
||||||
|
>
|
||||||
|
{{ scope.row.isUse === 0 ? '标记已使用' : '标记未使用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="danger"
|
||||||
|
@click="handleTbDelete(scope.row)"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<pagination
|
||||||
|
v-show="tbTotal > 0"
|
||||||
|
:total="tbTotal"
|
||||||
|
:page.sync="tbQueryParams.pageNum"
|
||||||
|
:limit.sync="tbQueryParams.pageSize"
|
||||||
|
@pagination="getTbList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 统计信息标签页 -->
|
||||||
|
<el-tab-pane label="统计信息" name="statistics">
|
||||||
|
<div class="statistics-container">
|
||||||
|
<el-form :inline="true" :model="statQueryParams" class="demo-form-inline" size="small">
|
||||||
|
<el-form-item label="评论来源">
|
||||||
|
<el-select v-model="statQueryParams.source" placeholder="全部" clearable style="width: 150px;">
|
||||||
|
<el-option label="京东评论" value="jd" />
|
||||||
|
<el-option label="淘宝评论" value="tb" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="small" @click="getStatistics">查询</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="small" @click="resetStatistics">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 统计表格 -->
|
||||||
|
<el-table v-loading="statLoading" :data="statisticsList" border style="margin-top: 20px;">
|
||||||
|
<el-table-column label="来源" prop="source" width="120" />
|
||||||
|
<el-table-column label="产品类型" prop="productType" width="200" />
|
||||||
|
<el-table-column label="商品ID" prop="productId" width="200" />
|
||||||
|
<el-table-column label="总评论数" prop="totalCount" width="120" align="center" />
|
||||||
|
<el-table-column label="可用数" prop="availableCount" width="120" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #67C23A; font-weight: bold;">{{ scope.row.availableCount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="已使用" prop="usedCount" width="120" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #909399;">{{ scope.row.usedCount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="接口调用次数" prop="apiCallCount" width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #409EFF; font-weight: bold;">{{ scope.row.apiCallCount || 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="今日调用" prop="todayCallCount" width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #E6A23C; font-weight: bold;">{{ scope.row.todayCallCount || 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用率" width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-progress
|
||||||
|
:percentage="getUsagePercentage(scope.row)"
|
||||||
|
:color="getUsageColor(scope.row)"
|
||||||
|
:stroke-width="20"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.source === '京东评论'"
|
||||||
|
type="warning"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
:loading="statFetchLoading"
|
||||||
|
@click="handleStatFetchComments(scope.row)"
|
||||||
|
>获取评论</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 图片查看对话框 -->
|
||||||
|
<el-dialog title="评论图片" :visible.sync="imageDialogVisible" width="80%">
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="(img, index) in imageList"
|
||||||
|
:key="index"
|
||||||
|
:src="img"
|
||||||
|
:preview-src-list="imageList"
|
||||||
|
fit="contain"
|
||||||
|
style="width: 200px; height: 200px; margin: 10px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
listJdComment, getJdComment, updateJdComment, delJdComment, resetJdCommentByProductId,
|
||||||
|
listTbComment, getTbComment, updateTbComment, delTbComment, resetTbCommentByProductId,
|
||||||
|
getCommentStatistics, getJdProductTypeMap, getTbProductTypeMap, fetchComments
|
||||||
|
} from '@/api/jarvis/comment'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommentManagement',
|
||||||
|
components: {
|
||||||
|
MobileSearchForm,
|
||||||
|
MobileButtonGroup
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: 'jd',
|
||||||
|
// 京东评论
|
||||||
|
jdLoading: false,
|
||||||
|
jdList: [],
|
||||||
|
jdTotal: 0,
|
||||||
|
jdQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
productId: null,
|
||||||
|
productType: null,
|
||||||
|
userName: null,
|
||||||
|
isUse: null
|
||||||
|
},
|
||||||
|
jdDateRange: [],
|
||||||
|
jdProductTypeMap: {},
|
||||||
|
// 淘宝评论
|
||||||
|
tbLoading: false,
|
||||||
|
tbList: [],
|
||||||
|
tbTotal: 0,
|
||||||
|
tbQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
productId: null,
|
||||||
|
productType: null,
|
||||||
|
userName: null,
|
||||||
|
isUse: null
|
||||||
|
},
|
||||||
|
tbDateRange: [],
|
||||||
|
tbProductTypeMap: {},
|
||||||
|
// 统计信息
|
||||||
|
statLoading: false,
|
||||||
|
statisticsList: [],
|
||||||
|
statQueryParams: {
|
||||||
|
source: null
|
||||||
|
},
|
||||||
|
// 图片查看
|
||||||
|
imageDialogVisible: false,
|
||||||
|
imageList: [],
|
||||||
|
// 获取评论 loading
|
||||||
|
jdFetchLoading: false,
|
||||||
|
tbFetchLoading: false,
|
||||||
|
statFetchLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
isMobile() {
|
||||||
|
if (this.device === 'mobile') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
jdActionButtons() {
|
||||||
|
return [
|
||||||
|
{ key: 'fetchComments', label: '获取评论', type: 'warning', icon: 'el-icon-upload2', handler: () => this.handleJdFetchComments(), disabled: this.jdFetchLoading },
|
||||||
|
{ key: 'export', label: '导出', type: 'success', icon: 'el-icon-download', handler: () => this.handleJdExport(), disabled: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tbActionButtons() {
|
||||||
|
return [
|
||||||
|
{ key: 'fetchComments', label: '获取评论', type: 'warning', icon: 'el-icon-upload2', handler: () => this.handleTbFetchComments(), disabled: this.tbFetchLoading },
|
||||||
|
{ key: 'export', label: '导出', type: 'success', icon: 'el-icon-download', handler: () => this.handleTbExport(), disabled: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getJdList()
|
||||||
|
this.getJdProductTypeMap()
|
||||||
|
this.getTbProductTypeMap()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 京东评论相关
|
||||||
|
getJdList() {
|
||||||
|
this.jdLoading = true
|
||||||
|
listJdComment(this.addDateRange(this.jdQueryParams, this.jdDateRange)).then(response => {
|
||||||
|
this.jdList = response.rows
|
||||||
|
this.jdTotal = response.total
|
||||||
|
this.jdLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleJdQuery() {
|
||||||
|
this.jdQueryParams.pageNum = 1
|
||||||
|
this.getJdList()
|
||||||
|
},
|
||||||
|
resetJdQuery() {
|
||||||
|
this.jdDateRange = []
|
||||||
|
this.resetForm('jdQueryParams')
|
||||||
|
this.handleJdQuery()
|
||||||
|
},
|
||||||
|
handleJdDateRangeChange(value) {
|
||||||
|
this.jdDateRange = value
|
||||||
|
},
|
||||||
|
toggleJdCommentUse(row) {
|
||||||
|
const newIsUse = row.isUse === 0 ? 1 : 0
|
||||||
|
updateJdComment({ id: row.id, isUse: newIsUse }).then(() => {
|
||||||
|
this.$modal.msgSuccess('操作成功')
|
||||||
|
this.getJdList()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleJdDelete(row) {
|
||||||
|
this.$modal.confirm('是否确认删除ID为"' + row.id + '"的评论?').then(() => {
|
||||||
|
return delJdComment(row.id)
|
||||||
|
}).then(() => {
|
||||||
|
this.getJdList()
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleJdExport() {
|
||||||
|
this.download('jarvis/comment/jd/export', {
|
||||||
|
...this.jdQueryParams
|
||||||
|
}, `jd_comment_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
/** 统计信息表格:仅京东商品可点击,用该行商品ID 调用获取评论 */
|
||||||
|
handleStatFetchComments(row) {
|
||||||
|
if (row.source !== '京东评论') return
|
||||||
|
const productId = (row.productId || '').toString().trim()
|
||||||
|
if (!productId) {
|
||||||
|
this.$modal.msgWarning('商品ID为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.statFetchLoading = true
|
||||||
|
fetchComments(productId).then(() => {
|
||||||
|
this.$modal.msgSuccess('获取评论请求已发送')
|
||||||
|
this.getStatistics()
|
||||||
|
}).catch(e => {
|
||||||
|
this.$modal.msgError(e && (e.message || e.msg) || '获取评论失败')
|
||||||
|
}).finally(() => {
|
||||||
|
this.statFetchLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 京东:调用获取评论接口,使用商品ID(SKU) 作为 product_id */
|
||||||
|
handleJdFetchComments() {
|
||||||
|
const productId = (this.jdQueryParams.productId || '').toString().trim()
|
||||||
|
if (!productId) {
|
||||||
|
this.$modal.msgWarning('请先输入商品ID(SKU)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.jdFetchLoading = true
|
||||||
|
fetchComments(productId).then(() => {
|
||||||
|
this.$modal.msgSuccess('获取评论请求已发送')
|
||||||
|
this.getJdList()
|
||||||
|
}).catch(e => {
|
||||||
|
this.$modal.msgError(e && (e.message || e.msg) || '获取评论失败')
|
||||||
|
}).finally(() => {
|
||||||
|
this.jdFetchLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 淘宝评论相关
|
||||||
|
getTbList() {
|
||||||
|
this.tbLoading = true
|
||||||
|
listTbComment(this.addDateRange(this.tbQueryParams, this.tbDateRange)).then(response => {
|
||||||
|
this.tbList = response.rows
|
||||||
|
this.tbTotal = response.total
|
||||||
|
this.tbLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleTbQuery() {
|
||||||
|
this.tbQueryParams.pageNum = 1
|
||||||
|
this.getTbList()
|
||||||
|
},
|
||||||
|
resetTbQuery() {
|
||||||
|
this.tbDateRange = []
|
||||||
|
this.resetForm('tbQueryParams')
|
||||||
|
this.handleTbQuery()
|
||||||
|
},
|
||||||
|
handleTbDateRangeChange(value) {
|
||||||
|
this.tbDateRange = value
|
||||||
|
},
|
||||||
|
toggleTbCommentUse(row) {
|
||||||
|
const newIsUse = row.isUse === 0 ? 1 : 0
|
||||||
|
updateTbComment({ id: row.id, isUse: newIsUse }).then(() => {
|
||||||
|
this.$modal.msgSuccess('操作成功')
|
||||||
|
this.getTbList()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleTbDelete(row) {
|
||||||
|
this.$modal.confirm('是否确认删除ID为"' + row.id + '"的评论?').then(() => {
|
||||||
|
return delTbComment(row.id)
|
||||||
|
}).then(() => {
|
||||||
|
this.getTbList()
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleTbExport() {
|
||||||
|
this.download('jarvis/taobaoComment/export', {
|
||||||
|
...this.tbQueryParams
|
||||||
|
}, `tb_comment_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
/** 淘宝:调用获取评论接口,使用商品ID(SKU) 作为 product_id */
|
||||||
|
handleTbFetchComments() {
|
||||||
|
const productId = (this.tbQueryParams.productId || '').toString().trim()
|
||||||
|
if (!productId) {
|
||||||
|
this.$modal.msgWarning('请先输入商品ID(SKU)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.tbFetchLoading = true
|
||||||
|
fetchComments(productId).then(() => {
|
||||||
|
this.$modal.msgSuccess('获取评论请求已发送')
|
||||||
|
this.getTbList()
|
||||||
|
}).catch(e => {
|
||||||
|
this.$modal.msgError(e && (e.message || e.msg) || '获取评论失败')
|
||||||
|
}).finally(() => {
|
||||||
|
this.tbFetchLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 统计信息相关
|
||||||
|
getStatistics() {
|
||||||
|
this.statLoading = true
|
||||||
|
getCommentStatistics(this.statQueryParams.source).then(response => {
|
||||||
|
this.statisticsList = response.data
|
||||||
|
this.statLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetStatistics() {
|
||||||
|
this.statQueryParams.source = null
|
||||||
|
this.getStatistics()
|
||||||
|
},
|
||||||
|
getUsagePercentage(row) {
|
||||||
|
if (!row.totalCount || row.totalCount === 0) return 0
|
||||||
|
return Math.round((row.usedCount / row.totalCount) * 100)
|
||||||
|
},
|
||||||
|
getUsageColor(row) {
|
||||||
|
const percentage = this.getUsagePercentage(row)
|
||||||
|
if (percentage < 50) return '#67C23A'
|
||||||
|
if (percentage < 80) return '#E6A23C'
|
||||||
|
return '#F56C6C'
|
||||||
|
},
|
||||||
|
// Redis映射
|
||||||
|
getJdProductTypeMap() {
|
||||||
|
getJdProductTypeMap().then(response => {
|
||||||
|
this.jdProductTypeMap = response.data || {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getTbProductTypeMap() {
|
||||||
|
getTbProductTypeMap().then(response => {
|
||||||
|
this.tbProductTypeMap = response.data || {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 标签页切换
|
||||||
|
handleTabClick(tab) {
|
||||||
|
if (tab.name === 'jd') {
|
||||||
|
if (this.jdList.length === 0) {
|
||||||
|
this.getJdList()
|
||||||
|
}
|
||||||
|
} else if (tab.name === 'tb') {
|
||||||
|
if (this.tbList.length === 0) {
|
||||||
|
this.getTbList()
|
||||||
|
}
|
||||||
|
} else if (tab.name === 'statistics') {
|
||||||
|
if (this.statisticsList.length === 0) {
|
||||||
|
this.getStatistics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 查看图片
|
||||||
|
viewImages(pictureUrls) {
|
||||||
|
if (pictureUrls) {
|
||||||
|
try {
|
||||||
|
this.imageList = JSON.parse(pictureUrls)
|
||||||
|
} catch (e) {
|
||||||
|
this.imageList = pictureUrls.split(',').filter(url => url.trim())
|
||||||
|
}
|
||||||
|
this.imageDialogVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.comment-container, .statistics-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.action-buttons-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端和桌面端按钮组显示控制 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-section.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-action-buttons.desktop-only {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
724
src/views/system/erpProduct/index.vue
Normal file
724
src/views/system/erpProduct/index.vue
Normal file
@@ -0,0 +1,724 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
|
||||||
|
<el-form-item label="ERP账号" prop="appid" required>
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.appid"
|
||||||
|
placeholder="请选择ERP账号(必选)"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@change="handleAccountChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in erpAccountList"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<span style="color: #f56c6c; margin-left: 10px; font-size: 12px;">* 不同账号的商品列表和权限不同</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品标题" prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.title"
|
||||||
|
placeholder="请输入商品标题"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品状态" prop="productStatus">
|
||||||
|
<el-select v-model="queryParams.productStatus" placeholder="请选择" clearable style="width: 150px">
|
||||||
|
<el-option label="全部" :value="null" />
|
||||||
|
<el-option label="删除" :value="-1" />
|
||||||
|
<el-option label="待发布" :value="21" />
|
||||||
|
<el-option label="销售中" :value="22" />
|
||||||
|
<el-option label="已售罄" :value="23" />
|
||||||
|
<el-option label="手动下架" :value="31" />
|
||||||
|
<el-option label="售出下架" :value="33" />
|
||||||
|
<el-option label="自动下架" :value="36" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="闲鱼会员名" prop="userName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.userName"
|
||||||
|
placeholder="请输入闲鱼会员名"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
size="mini"
|
||||||
|
@click="handleSyncAll"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:pull']"
|
||||||
|
>全量同步</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-top"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleBatchPublish"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:publish']"
|
||||||
|
>批量上架</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-bottom"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleBatchDownShelf"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:downShelf']"
|
||||||
|
>批量下架</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="erpProductList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="商品图片" align="center" prop="mainImage" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.mainImage"
|
||||||
|
:src="scope.row.mainImage"
|
||||||
|
:preview-src-list="[scope.row.mainImage]"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品信息" align="left" min-width="250" :show-overflow-tooltip="true">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: bold; margin-bottom: 5px;">
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">
|
||||||
|
商品ID: {{ scope.row.productId }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格/库存" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div style="color: #f56c6c; font-weight: bold;">
|
||||||
|
¥{{ formatPrice(scope.row.price) }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">
|
||||||
|
库存: {{ scope.row.stock || 0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品状态" align="center" prop="productStatus" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag
|
||||||
|
:type="getStatusType(scope.row.productStatus)"
|
||||||
|
size="mini"
|
||||||
|
>
|
||||||
|
{{ getStatusText(scope.row.productStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="闲鱼会员名" align="center" prop="userName" width="120" />
|
||||||
|
<el-table-column label="上架时间" align="center" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div v-if="scope.row.onlineTime">
|
||||||
|
{{ formatTime(scope.row.onlineTime) }}
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="更新时间" align="center" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div v-if="scope.row.updateTimeXy">
|
||||||
|
{{ formatTime(scope.row.updateTimeXy) }}
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="ERP应用" align="center" prop="appid" width="120" />
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-view"
|
||||||
|
@click="handleView(scope.row)"
|
||||||
|
>查看</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-top"
|
||||||
|
@click="handleSinglePublish(scope.row)"
|
||||||
|
v-if="scope.row.productStatus === 2"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:publish']"
|
||||||
|
>上架</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-bottom"
|
||||||
|
@click="handleSingleDownShelf(scope.row)"
|
||||||
|
v-if="scope.row.productStatus === 1"
|
||||||
|
v-hasPermi="['jarvis:erpProduct:downShelf']"
|
||||||
|
>下架</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 全量同步对话框 -->
|
||||||
|
<el-dialog title="全量同步闲鱼商品" :visible.sync="syncDialogVisible" width="500px" append-to-body>
|
||||||
|
<el-form ref="syncForm" :model="syncForm" label-width="100px">
|
||||||
|
<el-form-item label="ERP应用">
|
||||||
|
<el-select v-model="syncForm.appid" placeholder="请选择ERP应用" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in erpAccountList"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品状态">
|
||||||
|
<el-select v-model="syncForm.productStatus" placeholder="请选择(留空为全部)" clearable style="width: 100%">
|
||||||
|
<el-option label="全部" :value="null" />
|
||||||
|
<el-option label="删除" :value="-1" />
|
||||||
|
<el-option label="待发布" :value="21" />
|
||||||
|
<el-option label="销售中" :value="22" />
|
||||||
|
<el-option label="已售罄" :value="23" />
|
||||||
|
<el-option label="手动下架" :value="31" />
|
||||||
|
<el-option label="售出下架" :value="33" />
|
||||||
|
<el-option label="自动下架" :value="36" />
|
||||||
|
</el-select>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||||
|
<div>• 留空表示同步全部状态的商品</div>
|
||||||
|
<div>• 系统将自动遍历所有页码,同步所有商品</div>
|
||||||
|
<div>• 会自动更新本地已有商品,删除远程已不存在的商品</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitSyncAll" :loading="syncing">开始同步</el-button>
|
||||||
|
<el-button @click="syncDialogVisible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 查看商品详情对话框 -->
|
||||||
|
<el-dialog title="商品详情" :visible.sync="viewDialogVisible" width="800px" append-to-body>
|
||||||
|
<el-descriptions :column="2" border v-if="viewForm">
|
||||||
|
<el-descriptions-item label="商品ID">{{ viewForm.productId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品标题">{{ viewForm.title }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品价格">
|
||||||
|
¥{{ formatPrice(viewForm.price) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品库存">{{ viewForm.stock || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品状态">
|
||||||
|
<el-tag :type="getStatusType(viewForm.productStatus)" size="mini">
|
||||||
|
{{ getStatusText(viewForm.productStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="销售状态">{{ viewForm.saleStatus || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="闲鱼会员名">{{ viewForm.userName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ERP应用">{{ viewForm.appid || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="上架时间">
|
||||||
|
{{ viewForm.onlineTime ? formatTime(viewForm.onlineTime) : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="下架时间">
|
||||||
|
{{ viewForm.offlineTime ? formatTime(viewForm.offlineTime) : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="售出时间">
|
||||||
|
{{ viewForm.soldTime ? formatTime(viewForm.soldTime) : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品链接" :span="2">
|
||||||
|
<el-link v-if="viewForm.productUrl" :href="viewForm.productUrl" target="_blank" type="primary">
|
||||||
|
{{ viewForm.productUrl }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品图片" :span="2">
|
||||||
|
<el-image
|
||||||
|
v-if="viewForm.mainImage"
|
||||||
|
:src="viewForm.mainImage"
|
||||||
|
style="width: 200px; height: 200px;"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ viewForm.remark || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="viewDialogVisible = false">关 闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 批量上架对话框 -->
|
||||||
|
<el-dialog title="批量上架商品" :visible.sync="publishDialogVisible" width="500px" append-to-body>
|
||||||
|
<el-form ref="publishForm" :model="publishForm" label-width="120px">
|
||||||
|
<el-form-item label="选择账号">
|
||||||
|
<el-select v-model="publishForm.appid" placeholder="请选择ERP应用" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in erpAccountList"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="闲鱼会员名" required>
|
||||||
|
<el-select
|
||||||
|
v-model="publishForm.userName"
|
||||||
|
placeholder="请选择闲鱼会员名"
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
@focus="loadUsernames"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in usernameList"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品数量">
|
||||||
|
<el-input :value="selectedProductIds.length" readonly />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitBatchPublish" :loading="publishing">确 定</el-button>
|
||||||
|
<el-button @click="publishDialogVisible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listErpProduct, getErpProduct, delErpProduct, pullProductList, syncAllProducts, batchPublish, batchDownShelf, getERPAccounts, getUsernames } from "@/api/system/erpProduct";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ErpProduct",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 遮罩层
|
||||||
|
loading: true,
|
||||||
|
// 选中数组
|
||||||
|
ids: [],
|
||||||
|
// 选中的商品ID数组
|
||||||
|
selectedProductIds: [],
|
||||||
|
// 非单个禁用
|
||||||
|
single: true,
|
||||||
|
// 非多个禁用
|
||||||
|
multiple: true,
|
||||||
|
// 显示搜索条件
|
||||||
|
showSearch: true,
|
||||||
|
// 总条数
|
||||||
|
total: 0,
|
||||||
|
// 闲鱼商品表格数据
|
||||||
|
erpProductList: [],
|
||||||
|
// 查询参数
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
title: null,
|
||||||
|
productStatus: null,
|
||||||
|
userName: null,
|
||||||
|
appid: null
|
||||||
|
},
|
||||||
|
// ERP账号列表
|
||||||
|
erpAccountList: [],
|
||||||
|
// 全量同步对话框
|
||||||
|
syncDialogVisible: false,
|
||||||
|
syncForm: {
|
||||||
|
appid: null,
|
||||||
|
productStatus: null
|
||||||
|
},
|
||||||
|
syncing: false,
|
||||||
|
// 查看对话框
|
||||||
|
viewDialogVisible: false,
|
||||||
|
viewForm: null,
|
||||||
|
// 账号切换提示
|
||||||
|
accountWarningShown: false,
|
||||||
|
// 批量上架对话框
|
||||||
|
publishDialogVisible: false,
|
||||||
|
publishForm: {
|
||||||
|
appid: null,
|
||||||
|
userName: null
|
||||||
|
},
|
||||||
|
publishing: false,
|
||||||
|
// 会员名列表
|
||||||
|
usernameList: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadERPAccounts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询闲鱼商品列表 */
|
||||||
|
getList() {
|
||||||
|
// 如果没有选择账号,提示用户
|
||||||
|
if (!this.queryParams.appid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号");
|
||||||
|
this.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
listErpProduct(this.queryParams).then(response => {
|
||||||
|
this.erpProductList = response.rows;
|
||||||
|
this.total = response.total;
|
||||||
|
this.loading = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 加载ERP账号列表 */
|
||||||
|
loadERPAccounts() {
|
||||||
|
getERPAccounts().then(response => {
|
||||||
|
this.erpAccountList = response.data || [];
|
||||||
|
// 自动将第一个ERP账号作为参数并调用列表数据
|
||||||
|
if (this.erpAccountList.length > 0 && !this.queryParams.appid) {
|
||||||
|
this.queryParams.appid = this.erpAccountList[0].value;
|
||||||
|
this.getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 加载会员名列表 */
|
||||||
|
loadUsernames() {
|
||||||
|
getUsernames({ pageSize: 100 }).then(response => {
|
||||||
|
this.usernameList = response.data || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 账号变更处理 */
|
||||||
|
handleAccountChange() {
|
||||||
|
// 账号切换时清空选中项
|
||||||
|
this.ids = [];
|
||||||
|
this.selectedProductIds = [];
|
||||||
|
this.multiple = true;
|
||||||
|
this.single = true;
|
||||||
|
// 重新加载列表
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
if (!this.queryParams.appid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm");
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
// 多选框选中数据
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.id);
|
||||||
|
this.selectedProductIds = selection.map(item => item.productId);
|
||||||
|
this.single = selection.length !== 1;
|
||||||
|
this.multiple = !selection.length;
|
||||||
|
},
|
||||||
|
/** 全量同步按钮操作 */
|
||||||
|
handleSyncAll() {
|
||||||
|
if (!this.queryParams.appid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.syncDialogVisible = true;
|
||||||
|
this.syncForm = {
|
||||||
|
appid: this.queryParams.appid,
|
||||||
|
productStatus: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** 提交全量同步 */
|
||||||
|
submitSyncAll() {
|
||||||
|
if (!this.syncForm.appid) {
|
||||||
|
this.$modal.msgWarning("请选择ERP账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$confirm(
|
||||||
|
'全量同步将自动遍历所有页码,同步所有商品数据,并删除远程已不存在的本地商品。是否继续?',
|
||||||
|
'确认全量同步',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定同步',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
this.syncing = true;
|
||||||
|
syncAllProducts(this.syncForm).then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$modal.msgSuccess(response.msg || "同步成功");
|
||||||
|
this.syncDialogVisible = false;
|
||||||
|
// 刷新列表
|
||||||
|
this.getList();
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(response.msg || "同步失败");
|
||||||
|
}
|
||||||
|
this.syncing = false;
|
||||||
|
}).catch((error) => {
|
||||||
|
this.$modal.msgError(error.message || "同步失败");
|
||||||
|
this.syncing = false;
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户取消
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 查看按钮操作 */
|
||||||
|
handleView(row) {
|
||||||
|
const id = row.id || this.ids[0];
|
||||||
|
getErpProduct(id).then(response => {
|
||||||
|
this.viewForm = response.data;
|
||||||
|
this.viewDialogVisible = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 单个上架 */
|
||||||
|
handleSinglePublish(row) {
|
||||||
|
if (!row.appid) {
|
||||||
|
this.$modal.msgWarning("该商品缺少ERP账号信息,无法上架");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$prompt('请输入闲鱼会员名', '上架商品', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPlaceholder: '请输入闲鱼会员名',
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return '闲鱼会员名不能为空';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).then(({ value }) => {
|
||||||
|
const data = {
|
||||||
|
productIds: [row.productId],
|
||||||
|
userName: value,
|
||||||
|
appid: row.appid
|
||||||
|
};
|
||||||
|
this.publishing = true;
|
||||||
|
batchPublish(data).then(response => {
|
||||||
|
this.$modal.msgSuccess("上架成功");
|
||||||
|
this.publishing = false;
|
||||||
|
this.getList();
|
||||||
|
}).catch(() => {
|
||||||
|
this.publishing = false;
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 单个下架 */
|
||||||
|
handleSingleDownShelf(row) {
|
||||||
|
if (!row.appid) {
|
||||||
|
this.$modal.msgWarning("该商品缺少ERP账号信息,无法下架");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm('确定要下架该商品吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const data = {
|
||||||
|
productIds: [row.productId],
|
||||||
|
appid: row.appid
|
||||||
|
};
|
||||||
|
batchDownShelf(data).then(response => {
|
||||||
|
this.$modal.msgSuccess("下架成功");
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 批量上架按钮操作 */
|
||||||
|
handleBatchPublish() {
|
||||||
|
if (this.selectedProductIds.length === 0) {
|
||||||
|
this.$modal.msgWarning("请先选择要上架的商品");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查选中的商品是否属于同一个账号
|
||||||
|
const selectedProducts = this.erpProductList.filter(item => this.selectedProductIds.includes(item.productId));
|
||||||
|
const appids = [...new Set(selectedProducts.map(p => p.appid))];
|
||||||
|
|
||||||
|
if (appids.length > 1) {
|
||||||
|
this.$modal.msgWarning("选中的商品属于不同的ERP账号,请分别操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountAppid = appids[0] || this.queryParams.appid;
|
||||||
|
if (!accountAppid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号或确保选中的商品有关联的账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.publishForm = {
|
||||||
|
appid: accountAppid,
|
||||||
|
userName: null
|
||||||
|
};
|
||||||
|
this.publishDialogVisible = true;
|
||||||
|
},
|
||||||
|
/** 提交批量上架 */
|
||||||
|
submitBatchPublish() {
|
||||||
|
if (!this.publishForm.userName) {
|
||||||
|
this.$modal.msgWarning("请选择闲鱼会员名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
productIds: this.selectedProductIds,
|
||||||
|
userName: this.publishForm.userName,
|
||||||
|
appid: this.publishForm.appid
|
||||||
|
};
|
||||||
|
this.publishing = true;
|
||||||
|
batchPublish(data).then(response => {
|
||||||
|
this.$modal.msgSuccess(response.msg || "批量上架成功");
|
||||||
|
this.publishDialogVisible = false;
|
||||||
|
this.publishing = false;
|
||||||
|
this.getList();
|
||||||
|
}).catch(() => {
|
||||||
|
this.publishing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 批量下架按钮操作 */
|
||||||
|
handleBatchDownShelf() {
|
||||||
|
if (this.selectedProductIds.length === 0) {
|
||||||
|
this.$modal.msgWarning("请先选择要下架的商品");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查选中的商品是否属于同一个账号
|
||||||
|
const selectedProducts = this.erpProductList.filter(item => this.selectedProductIds.includes(item.productId));
|
||||||
|
const appids = [...new Set(selectedProducts.map(p => p.appid))];
|
||||||
|
|
||||||
|
if (appids.length > 1) {
|
||||||
|
this.$modal.msgWarning("选中的商品属于不同的ERP账号,请分别操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountAppid = appids[0] || this.queryParams.appid;
|
||||||
|
if (!accountAppid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号或确保选中的商品有关联的账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$confirm('确定要批量下架选中的 ' + this.selectedProductIds.length + ' 个商品吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const data = {
|
||||||
|
productIds: this.selectedProductIds,
|
||||||
|
appid: accountAppid
|
||||||
|
};
|
||||||
|
batchDownShelf(data).then(response => {
|
||||||
|
this.$modal.msgSuccess(response.msg || "批量下架成功");
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
handleDelete(row) {
|
||||||
|
const ids = row.id || this.ids;
|
||||||
|
this.$modal.confirm('是否确认删除闲鱼商品编号为"' + ids + '"的数据项?').then(() => {
|
||||||
|
return delErpProduct(ids);
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess("删除成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
handleExport() {
|
||||||
|
if (!this.queryParams.appid) {
|
||||||
|
this.$modal.msgWarning("请先选择ERP账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.download('jarvis/erpProduct/export', {
|
||||||
|
...this.queryParams
|
||||||
|
}, `erpProduct_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
/** 格式化价格(分转元) */
|
||||||
|
formatPrice(price) {
|
||||||
|
if (price == null) return '0.00';
|
||||||
|
return (price / 100).toFixed(2);
|
||||||
|
},
|
||||||
|
/** 格式化时间(时间戳转日期) */
|
||||||
|
formatTime(timestamp) {
|
||||||
|
if (!timestamp) return '-';
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
return this.parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}');
|
||||||
|
},
|
||||||
|
/** 获取状态文本 */
|
||||||
|
getStatusText(status) {
|
||||||
|
if (status == null) return '-';
|
||||||
|
const statusMap = {
|
||||||
|
'-1': '删除',
|
||||||
|
'21': '待发布',
|
||||||
|
'22': '销售中',
|
||||||
|
'23': '已售罄',
|
||||||
|
'31': '手动下架',
|
||||||
|
'33': '售出下架',
|
||||||
|
'36': '自动下架'
|
||||||
|
};
|
||||||
|
return statusMap[String(status)] || '未知(' + status + ')';
|
||||||
|
},
|
||||||
|
/** 获取状态类型 */
|
||||||
|
getStatusType(status) {
|
||||||
|
if (status == null) return '';
|
||||||
|
const typeMap = {
|
||||||
|
'-1': 'danger', // 删除
|
||||||
|
'21': 'info', // 待发布
|
||||||
|
'22': 'success', // 销售中
|
||||||
|
'23': 'warning', // 已售罄
|
||||||
|
'31': 'warning', // 手动下架
|
||||||
|
'33': 'info', // 售出下架
|
||||||
|
'36': 'warning' // 自动下架
|
||||||
|
};
|
||||||
|
return typeMap[String(status)] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
707
src/views/system/favoriteProduct/index.vue
Normal file
707
src/views/system/favoriteProduct/index.vue
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<mobile-search-form
|
||||||
|
:model="queryParams"
|
||||||
|
@search="handleQuery"
|
||||||
|
@reset="resetQuery"
|
||||||
|
>
|
||||||
|
<template #form="{ expanded }">
|
||||||
|
<el-form
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryForm"
|
||||||
|
size="small"
|
||||||
|
:inline="true"
|
||||||
|
v-show="showSearch"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="商品名称" prop="productName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.productName"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺名称" prop="shopName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.shopName"
|
||||||
|
placeholder="请输入店铺名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SKUID" prop="skuid">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.skuid"
|
||||||
|
placeholder="请输入SKUID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否置顶" prop="isTop">
|
||||||
|
<el-select v-model="queryParams.isTop" placeholder="请选择" clearable>
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 桌面端搜索按钮 -->
|
||||||
|
<el-form-item v-if="!expanded">
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</mobile-search-form>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
|
<div class="action-buttons-section mobile-only">
|
||||||
|
<mobile-button-group
|
||||||
|
:buttons="actionButtons"
|
||||||
|
:primary-count="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<el-row :gutter="10" class="mb8 desktop-only">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-edit"
|
||||||
|
size="mini"
|
||||||
|
:disabled="single"
|
||||||
|
@click="handleUpdate"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-top"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleBatchTop"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:edit']"
|
||||||
|
>批量置顶</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="favoriteProductList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="商品图片" align="center" prop="productImage" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.productImage"
|
||||||
|
:src="scope.row.productImage"
|
||||||
|
:preview-src-list="[scope.row.productImage]"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品名称" align="left" prop="productName" min-width="200" :show-overflow-tooltip="true">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: bold; margin-bottom: 5px;">
|
||||||
|
<el-tag v-if="scope.row.isTop === 1" type="danger" size="mini">置顶</el-tag>
|
||||||
|
{{ scope.row.productName }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">
|
||||||
|
SKUID: {{ scope.row.skuid }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="店铺信息" align="left" prop="shopName" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div>{{ scope.row.shopName }}</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">ID: {{ scope.row.shopId }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格信息" align="center" prop="price" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div style="color: #f56c6c; font-weight: bold;">¥{{ scope.row.price || '-' }}</div>
|
||||||
|
<div v-if="scope.row.commissionInfo" style="color: #67c23a; font-size: 12px;">
|
||||||
|
佣金: {{ scope.row.commissionInfo }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="ERP商品" align="center" prop="erpProductIds" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.erpProductIds"
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
@click="showErpProducts(scope.row)"
|
||||||
|
>
|
||||||
|
查看({{ JSON.parse(scope.row.erpProductIds || '[]').length }})
|
||||||
|
</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用统计" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div>使用: {{ scope.row.useCount || 0 }}次</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">
|
||||||
|
{{ scope.row.lastUsedTime || '未使用' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建信息" align="center" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<div>{{ scope.row.createUserName }}</div>
|
||||||
|
<div style="color: #666; font-size: 12px;">
|
||||||
|
{{ parseTime(scope.row.createTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-view"
|
||||||
|
@click="handleView(scope.row)"
|
||||||
|
>查看</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-top"
|
||||||
|
@click="handleTop(scope.row)"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:edit']"
|
||||||
|
>
|
||||||
|
{{ scope.row.isTop === 1 ? '取消置顶' : '置顶' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-shopping-cart-2"
|
||||||
|
@click="handleQuickPublish(scope.row)"
|
||||||
|
>快速发品</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['jarvis:favoriteProduct:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 添加或修改常用商品对话框 -->
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="SKUID" prop="skuid">
|
||||||
|
<el-input v-model="form.skuid" placeholder="请输入SKUID" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品名称" prop="productName">
|
||||||
|
<el-input v-model="form.productName" placeholder="请输入商品名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="店铺名称" prop="shopName">
|
||||||
|
<el-input v-model="form.shopName" placeholder="请输入店铺名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="店铺ID" prop="shopId">
|
||||||
|
<el-input v-model="form.shopId" placeholder="请输入店铺ID" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品链接" prop="productUrl">
|
||||||
|
<el-input v-model="form.productUrl" placeholder="请输入商品链接" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品图片" prop="productImage">
|
||||||
|
<el-input v-model="form.productImage" placeholder="请输入商品图片URL" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品价格" prop="price">
|
||||||
|
<el-input v-model="form.price" placeholder="请输入商品价格" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="佣金信息" prop="commissionInfo">
|
||||||
|
<el-input v-model="form.commissionInfo" placeholder="请输入佣金信息" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品分类" prop="category">
|
||||||
|
<el-input v-model="form.category" placeholder="请输入商品分类" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品品牌" prop="brand">
|
||||||
|
<el-input v-model="form.brand" placeholder="请输入商品品牌" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="是否置顶" prop="isTop">
|
||||||
|
<el-radio-group v-model="form.isTop">
|
||||||
|
<el-radio :label="1">是</el-radio>
|
||||||
|
<el-radio :label="0">否</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="排序权重" prop="sortWeight">
|
||||||
|
<el-input-number v-model="form.sortWeight" :min="0" :max="999" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ERP商品详情对话框 -->
|
||||||
|
<el-dialog title="ERP商品详情" :visible.sync="erpDialogVisible" width="800px" append-to-body>
|
||||||
|
<el-table :data="erpProducts" border style="width: 100%">
|
||||||
|
<el-table-column label="ERP应用ID" prop="appid" width="120" />
|
||||||
|
<el-table-column label="ERP商品ID" prop="erpProductId" width="120" />
|
||||||
|
<el-table-column label="商品标题" prop="erpProductTitle" min-width="200" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="商品状态" prop="erpProductStatus" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.erpProductStatus === 'active' ? 'success' : 'info'">
|
||||||
|
{{ scope.row.erpProductStatus === 'active' ? '正常' : '其他' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品链接" prop="erpProductUrl" min-width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link :href="scope.row.erpProductUrl" target="_blank" type="primary">
|
||||||
|
查看商品
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" prop="remark" width="120" />
|
||||||
|
</el-table>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="erpDialogVisible = false">关 闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 通用发品对话框:从常用商品直接进入,不显示ERP应用选择 -->
|
||||||
|
<PublishDialog :visible.sync="publishDialogVisible" :initial-data="publishInitialData" :hideAppid="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listFavoriteProduct, getFavoriteProduct, delFavoriteProduct, addFavoriteProduct, updateFavoriteProduct, updateTopStatus } from "@/api/system/favoriteProduct";
|
||||||
|
import { generatePromotionContent } from "@/api/system/jdorder";
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
import PublishDialog from '@/components/PublishDialog.vue'
|
||||||
|
// 自动加入常用逻辑由 PublishDialog 内部触发(线报、转链页面),本页主要用于打开发品弹窗
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FavoriteProduct",
|
||||||
|
components: {
|
||||||
|
PublishDialog,
|
||||||
|
MobileSearchForm,
|
||||||
|
MobileButtonGroup
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['favoriteProductRefreshKey', 'device']),
|
||||||
|
isMobile() {
|
||||||
|
if (this.device === 'mobile') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
actionButtons() {
|
||||||
|
return [
|
||||||
|
{ key: 'add', label: '新增', type: 'primary', icon: 'el-icon-plus', handler: () => this.handleAdd(), disabled: false },
|
||||||
|
{ key: 'update', label: '修改', type: 'success', icon: 'el-icon-edit', handler: () => this.handleUpdate(), disabled: this.single },
|
||||||
|
{ key: 'delete', label: '删除', type: 'danger', icon: 'el-icon-delete', handler: () => this.handleDelete(), disabled: this.multiple },
|
||||||
|
{ key: 'top', label: '批量置顶', type: 'warning', icon: 'el-icon-top', handler: () => this.handleBatchTop(), disabled: this.multiple },
|
||||||
|
{ key: 'export', label: '导出', type: 'info', icon: 'el-icon-download', handler: () => this.handleExport(), disabled: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 遮罩层
|
||||||
|
loading: true,
|
||||||
|
// 选中数组
|
||||||
|
ids: [],
|
||||||
|
// 非单个禁用
|
||||||
|
single: true,
|
||||||
|
// 非多个禁用
|
||||||
|
multiple: true,
|
||||||
|
// 显示搜索条件
|
||||||
|
showSearch: true,
|
||||||
|
// 总条数
|
||||||
|
total: 0,
|
||||||
|
// 常用商品表格数据
|
||||||
|
favoriteProductList: [],
|
||||||
|
// 弹出层标题
|
||||||
|
title: "",
|
||||||
|
// 是否显示弹出层
|
||||||
|
open: false,
|
||||||
|
// 查询参数
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
productName: null,
|
||||||
|
shopName: null,
|
||||||
|
skuid: null,
|
||||||
|
isTop: null
|
||||||
|
},
|
||||||
|
// 表单参数
|
||||||
|
form: {},
|
||||||
|
// 表单校验
|
||||||
|
rules: {
|
||||||
|
skuid: [
|
||||||
|
{ required: true, message: "SKUID不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
productName: [
|
||||||
|
{ required: true, message: "商品名称不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
shopName: [
|
||||||
|
{ required: true, message: "店铺名称不能为空", trigger: "blur" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// ERP商品对话框
|
||||||
|
erpDialogVisible: false,
|
||||||
|
erpProducts: [],
|
||||||
|
|
||||||
|
// 通用发品弹窗
|
||||||
|
publishDialogVisible: false,
|
||||||
|
publishInitialData: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
favoriteProductRefreshKey() {
|
||||||
|
// 全局刷新标记变更时,自动刷新列表
|
||||||
|
this.getList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询常用商品列表 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true;
|
||||||
|
listFavoriteProduct(this.queryParams).then(response => {
|
||||||
|
this.favoriteProductList = response.rows;
|
||||||
|
this.total = response.total;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 取消按钮
|
||||||
|
cancel() {
|
||||||
|
this.open = false;
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
// 表单重置
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
skuid: null,
|
||||||
|
productName: null,
|
||||||
|
shopName: null,
|
||||||
|
shopId: null,
|
||||||
|
productUrl: null,
|
||||||
|
productImage: null,
|
||||||
|
price: null,
|
||||||
|
commissionInfo: null,
|
||||||
|
isTop: 0,
|
||||||
|
sortWeight: 0,
|
||||||
|
remark: null,
|
||||||
|
category: null,
|
||||||
|
brand: null
|
||||||
|
};
|
||||||
|
this.resetForm("form");
|
||||||
|
},
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm");
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
// 多选框选中数据
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.id)
|
||||||
|
this.single = selection.length!==1
|
||||||
|
this.multiple = !selection.length
|
||||||
|
},
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
handleAdd() {
|
||||||
|
this.reset();
|
||||||
|
this.open = true;
|
||||||
|
this.title = "添加常用商品";
|
||||||
|
},
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset();
|
||||||
|
const id = row.id || this.ids
|
||||||
|
getFavoriteProduct(id).then(response => {
|
||||||
|
this.form = response.data;
|
||||||
|
this.open = true;
|
||||||
|
this.title = "修改常用商品";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 查看按钮操作 */
|
||||||
|
handleView(row) {
|
||||||
|
this.reset();
|
||||||
|
const id = row.id || this.ids
|
||||||
|
getFavoriteProduct(id).then(response => {
|
||||||
|
this.form = response.data;
|
||||||
|
this.open = true;
|
||||||
|
this.title = "查看常用商品";
|
||||||
|
// 设置为只读
|
||||||
|
this.$nextTick(() => {
|
||||||
|
Object.keys(this.form).forEach(key => {
|
||||||
|
const input = this.$refs.form.$el.querySelector(`[name="${key}"]`);
|
||||||
|
if (input) {
|
||||||
|
input.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 提交按钮 */
|
||||||
|
submitForm() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
if (this.form.id != null) {
|
||||||
|
updateFavoriteProduct(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("修改成功");
|
||||||
|
this.open = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addFavoriteProduct(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("新增成功");
|
||||||
|
this.open = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
handleDelete(row) {
|
||||||
|
const ids = row.id || this.ids;
|
||||||
|
this.$modal.confirm('是否确认删除常用商品编号为"' + ids + '"的数据项?').then(function() {
|
||||||
|
return delFavoriteProduct(ids);
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess("删除成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 置顶按钮操作 */
|
||||||
|
handleTop(row) {
|
||||||
|
const isTop = row.isTop === 1 ? 0 : 1;
|
||||||
|
const action = isTop === 1 ? '置顶' : '取消置顶';
|
||||||
|
this.$modal.confirm('是否确认' + action + '商品"' + row.productName + '"?').then(function() {
|
||||||
|
return updateTopStatus(row.id, isTop);
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess(action + "成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 批量置顶操作 */
|
||||||
|
handleBatchTop() {
|
||||||
|
this.$modal.confirm('是否确认批量置顶选中的商品?').then(function() {
|
||||||
|
// 这里需要调用批量置顶接口
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.$modal.msgSuccess("批量置顶成功");
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
handleExport() {
|
||||||
|
this.download('jarvis/favoriteProduct/export', {
|
||||||
|
...this.queryParams
|
||||||
|
}, `favoriteProduct_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
/** 显示ERP商品详情 */
|
||||||
|
showErpProducts(row) {
|
||||||
|
try {
|
||||||
|
this.erpProducts = JSON.parse(row.erpProductIds || '[]');
|
||||||
|
this.erpDialogVisible = true;
|
||||||
|
} catch (e) {
|
||||||
|
this.$modal.msgError("ERP商品数据格式错误");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 快速发品 */
|
||||||
|
handleQuickPublish(row) {
|
||||||
|
const id = row.id || this.ids;
|
||||||
|
getFavoriteProduct(id).then(async res => {
|
||||||
|
const p = res && res.data ? res.data : (row || {});
|
||||||
|
try {
|
||||||
|
// 与转链保持一致:优先使用保存的商品链接,避免用名称/ID 产生歧义
|
||||||
|
if (!p.productUrl) {
|
||||||
|
this.$modal.msgWarning('该常用商品缺少商品链接,无法生成完整发品信息');
|
||||||
|
}
|
||||||
|
let detail = null;
|
||||||
|
if (p.productUrl) {
|
||||||
|
const r = await generatePromotionContent({ promotionContent: p.productUrl });
|
||||||
|
const resultStr = (r && (r.msg || r.data)) || '';
|
||||||
|
try { const arr = typeof resultStr === 'string' ? JSON.parse(resultStr) : resultStr; if (Array.isArray(arr) && arr.length) detail = arr[0]; } catch(e) {}
|
||||||
|
}
|
||||||
|
const images = Array.isArray(detail && detail.images) && detail.images.length ? detail.images : (p.productImage ? [p.productImage] : []);
|
||||||
|
const wenanArr = Array.isArray(detail && detail.wenan) ? detail.wenan : [];
|
||||||
|
const wenanOptions = wenanArr.map((w, i) => ({ label: w.type || `版本${i+1}` , content: w.content || '' }));
|
||||||
|
this.publishInitialData = {
|
||||||
|
title: (detail && (detail.skuName || detail.title)) || p.productName || '',
|
||||||
|
content: (wenanOptions[0] && wenanOptions[0].content) || '',
|
||||||
|
images: images,
|
||||||
|
originalPrice: detail && detail.price ? Number(detail.price) : (p.price ? Number(p.price) : undefined),
|
||||||
|
wenanOptions: wenanOptions,
|
||||||
|
userName: p.userName || '',
|
||||||
|
province: p.province || null,
|
||||||
|
city: p.city || null,
|
||||||
|
district: p.district || null
|
||||||
|
};
|
||||||
|
this.publishDialogVisible = true;
|
||||||
|
} catch (e) {
|
||||||
|
const imagesFallback = p.productImage ? [p.productImage] : [];
|
||||||
|
this.publishInitialData = { title: p.productName || '', images: imagesFallback, content: '' };
|
||||||
|
this.publishDialogVisible = true;
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
const images = row && row.productImage ? [row.productImage] : [];
|
||||||
|
this.publishInitialData = { title: row.productName || '', images, content: '' };
|
||||||
|
this.publishDialogVisible = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mb8 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .el-table__row:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tag {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.action-buttons-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端和桌面端按钮组显示控制 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-section.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
460
src/views/system/giftcoupon/batch.vue
Normal file
460
src/views/system/giftcoupon/batch.vue
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span style="font-weight: bold; font-size: 18px;">批量创建礼金并替换URL</span>
|
||||||
|
<el-divider direction="vertical"></el-divider>
|
||||||
|
<span style="color: #909399; font-size: 14px;">一键操作:粘贴文案 → 自动创建礼金 → 输出替换后的文案</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置区域 -->
|
||||||
|
<el-form :model="form" :rules="rules" ref="form" inline style="margin-bottom: 15px;">
|
||||||
|
<el-form-item label="礼金金额" prop="amount">
|
||||||
|
<el-input-number v-model="form.amount" :min="1" :max="50" :precision="2" :step="0.01" style="width: 120px" />
|
||||||
|
<span style="margin-left: 5px; color: #909399;">元</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="每张数量" prop="quantity">
|
||||||
|
<el-input-number v-model="form.quantity" :min="1" :max="100" style="width: 120px" />
|
||||||
|
<span style="margin-left: 5px; color: #909399;">个</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品类型" prop="owner">
|
||||||
|
<el-radio-group v-model="form.owner">
|
||||||
|
<el-radio label="g">自营</el-radio>
|
||||||
|
<el-radio label="pop">POP</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item style="margin-left: 20px;">
|
||||||
|
<el-tag v-if="detectedUrls.length > 0" type="success" size="medium">
|
||||||
|
<i class="el-icon-link"></i> 已识别 {{ detectedUrls.length }} 个URL
|
||||||
|
</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 左右两个文本框 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<!-- 左侧:输入文案 -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="text-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span style="font-weight: bold; font-size: 16px;">
|
||||||
|
<i class="el-icon-edit-outline"></i> 输入原始文案
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleProcess"
|
||||||
|
:loading="processing"
|
||||||
|
:disabled="detectedUrls.length === 0"
|
||||||
|
>
|
||||||
|
<i class="el-icon-magic-stick"></i> 一键生成 ({{ detectedUrls.length }}个)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="25"
|
||||||
|
v-model="content"
|
||||||
|
placeholder="💡 将包含京东链接的完整推广文案粘贴到这里 示例: 🔴【海尔电热水器】11月9号晚8好价~ ✅9折券:商品页面直接领取 1️⃣海尔无镁棒BK5PLUS(60升) 下单:https://u.jd.com/T1G7978 👉300券+388红包 2️⃣海尔无镁棒BK5PLUS(80升) 下单:https://u.jd.com/TrG7lCN 👉300券+388红包 ✨ 系统会自动识别所有京东链接并替换!"
|
||||||
|
class="textarea-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧:输出结果 -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="text-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span style="font-weight: bold; font-size: 16px;">
|
||||||
|
<i class="el-icon-document-checked"></i> 替换后的文案
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="copyResult"
|
||||||
|
:disabled="!result || !result.replacedContent"
|
||||||
|
>
|
||||||
|
<i class="el-icon-document-copy"></i> 复制结果
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-if="!processing && result && result.replacedContent"
|
||||||
|
type="textarea"
|
||||||
|
:rows="25"
|
||||||
|
v-model="result.replacedContent"
|
||||||
|
readonly
|
||||||
|
class="textarea-output"
|
||||||
|
/>
|
||||||
|
<div v-else-if="processing" class="loading-container">
|
||||||
|
<el-progress
|
||||||
|
:percentage="progress"
|
||||||
|
:status="progressStatus"
|
||||||
|
:stroke-width="15"
|
||||||
|
style="width: 80%;"
|
||||||
|
>
|
||||||
|
<template slot="format">
|
||||||
|
{{ progressText }}
|
||||||
|
</template>
|
||||||
|
</el-progress>
|
||||||
|
<div style="margin-top: 15px; color: #909399; font-size: 14px;">
|
||||||
|
{{ progressDetail }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-container">
|
||||||
|
<i class="el-icon-document" style="font-size: 64px; color: #DCDFE6; margin-bottom: 10px;"></i>
|
||||||
|
<div style="color: #909399; font-size: 14px;">点击左侧"一键生成"按钮后,替换结果将显示在这里</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div v-if="result && result.replacedContent" style="margin-top: 15px;">
|
||||||
|
<el-alert
|
||||||
|
:type="result.replacedCount === result.totalUrls ? 'success' : (result.replacedCount > 0 ? 'warning' : 'error')"
|
||||||
|
:closable="false"
|
||||||
|
>
|
||||||
|
<template slot="title">
|
||||||
|
<span style="font-weight: bold;">
|
||||||
|
<i :class="result.replacedCount === result.totalUrls ? 'el-icon-success' : (result.replacedCount > 0 ? 'el-icon-warning' : 'el-icon-error')"></i> 处理完成!
|
||||||
|
成功替换 {{ result.replacedCount || 0 }} / {{ result.totalUrls || 0 }} 个URL
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 详细结果展示区域 -->
|
||||||
|
<el-row v-if="result && result.replacements && result.replacements.length > 0" style="margin-top: 20px;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span style="font-weight: bold; font-size: 16px;">
|
||||||
|
<i class="el-icon-document"></i> 详细处理结果
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
style="float: right;"
|
||||||
|
@click="showDetailResults = !showDetailResults"
|
||||||
|
>
|
||||||
|
{{ showDetailResults ? '收起' : '展开' }}
|
||||||
|
<i :class="showDetailResults ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-show="showDetailResults">
|
||||||
|
<el-table
|
||||||
|
:data="result.replacements"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
style="width: 100%"
|
||||||
|
:default-sort="{prop: 'index', order: 'ascending'}"
|
||||||
|
>
|
||||||
|
<el-table-column prop="index" label="序号" width="80" align="center" sortable />
|
||||||
|
<el-table-column prop="skuName" label="商品名称" min-width="200" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.skuName">{{ scope.row.skuName }}</span>
|
||||||
|
<span v-else style="color: #909399;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="originalUrl" label="原始链接" min-width="250" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link :href="scope.row.originalUrl" target="_blank" type="primary" :underline="false">
|
||||||
|
{{ scope.row.originalUrl }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="newUrl" label="新链接" min-width="250" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link
|
||||||
|
v-if="scope.row.success && scope.row.newUrl"
|
||||||
|
:href="scope.row.newUrl"
|
||||||
|
target="_blank"
|
||||||
|
type="success"
|
||||||
|
:underline="false"
|
||||||
|
>
|
||||||
|
{{ scope.row.newUrl }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else style="color: #909399;">未替换</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="success" label="状态" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.success ? 'success' : 'danger'" size="small">
|
||||||
|
<i :class="scope.row.success ? 'el-icon-success' : 'el-icon-error'"></i>
|
||||||
|
{{ scope.row.success ? '成功' : '失败' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="error" label="错误信息" min-width="300" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.error" style="color: #F56C6C;">{{ scope.row.error }}</span>
|
||||||
|
<span v-else style="color: #67C23A;">✓ 处理成功</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="giftCouponKey" label="礼金券Key" min-width="150" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.giftCouponKey" style="color: #409EFF;">{{ scope.row.giftCouponKey }}</span>
|
||||||
|
<span v-else style="color: #909399;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { replaceUrlsWithGiftCoupons } from '@/api/system/jdorder'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BatchGiftCoupon',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
form: {
|
||||||
|
amount: 1.8,
|
||||||
|
quantity: 12,
|
||||||
|
owner: 'g'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
amount: [{ required: true, message: '请输入礼金金额', trigger: 'blur' }],
|
||||||
|
quantity: [{ required: true, message: '请输入礼金数量', trigger: 'blur' }]
|
||||||
|
},
|
||||||
|
processing: false,
|
||||||
|
progress: 0,
|
||||||
|
progressText: '',
|
||||||
|
progressDetail: '',
|
||||||
|
progressStatus: '',
|
||||||
|
result: null,
|
||||||
|
detectedUrls: [],
|
||||||
|
showDetailResults: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
content(newVal) {
|
||||||
|
this.detectUrls(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 检测文本中的URL */
|
||||||
|
detectUrls(text) {
|
||||||
|
if (!text || text.trim().length === 0) {
|
||||||
|
this.detectedUrls = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlPattern = /(https?:\/\/[^\s]+)|(u\.jd\.com\/[^\s]+)/gi
|
||||||
|
const urls = []
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = urlPattern.exec(text)) !== null) {
|
||||||
|
const url = match[0]
|
||||||
|
if (url && !urls.includes(url.trim())) {
|
||||||
|
urls.push(url.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.detectedUrls = urls
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 一键处理 */
|
||||||
|
async handleProcess() {
|
||||||
|
if (!this.$refs.form) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.form.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
if (this.detectedUrls.length === 0) {
|
||||||
|
this.$modal.msgWarning('文本中未找到URL,请输入包含京东商品链接的文案')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.detectedUrls.length > 100) {
|
||||||
|
this.$modal.msgError('检测到的URL数量超过100个,请分批处理')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认操作
|
||||||
|
try {
|
||||||
|
await this.$confirm(
|
||||||
|
`检测到 ${this.detectedUrls.length} 个商品链接,将自动批量创建 ${this.detectedUrls.length} 张 ${this.form.amount} 元礼金券并替换文案中的URL。是否继续?`,
|
||||||
|
'确认批量创建',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定创建',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
this.progress = 20
|
||||||
|
this.progressText = '正在处理...'
|
||||||
|
this.progressDetail = '后端正在为每个URL单独创建礼金券,请耐心等待...'
|
||||||
|
this.progressStatus = ''
|
||||||
|
this.result = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用后端接口,后端会为每个URL单独处理
|
||||||
|
const params = {
|
||||||
|
content: this.content,
|
||||||
|
amount: this.form.amount,
|
||||||
|
quantity: this.form.quantity,
|
||||||
|
owner: this.form.owner || 'g'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = 40
|
||||||
|
const res = await replaceUrlsWithGiftCoupons(params)
|
||||||
|
|
||||||
|
this.progress = 80
|
||||||
|
|
||||||
|
if (res && res.code === 200 && res.data) {
|
||||||
|
this.result = res.data
|
||||||
|
|
||||||
|
const successCount = this.result.replacedCount || 0
|
||||||
|
const totalCount = this.result.totalUrls || 0
|
||||||
|
|
||||||
|
this.progress = 100
|
||||||
|
this.progressStatus = successCount === totalCount ? 'success' : (successCount > 0 ? 'warning' : 'exception')
|
||||||
|
this.progressText = successCount === totalCount ? '完成!' : '部分成功'
|
||||||
|
this.progressDetail = `成功替换 ${successCount} / ${totalCount} 个URL`
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
this.$modal.msgSuccess(`✅ 批量替换完成!成功 ${successCount} / ${totalCount} 个`)
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError('批量替换失败,所有URL处理均失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.progress = 100
|
||||||
|
this.progressStatus = 'exception'
|
||||||
|
this.progressText = '失败'
|
||||||
|
this.progressDetail = res.msg || '未知错误'
|
||||||
|
this.$modal.msgError('批量替换失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('批量替换异常', e)
|
||||||
|
this.progress = 100
|
||||||
|
this.progressStatus = 'exception'
|
||||||
|
this.progressText = '失败'
|
||||||
|
this.progressDetail = e.message || '未知错误'
|
||||||
|
|
||||||
|
let errorMsg = '未知错误'
|
||||||
|
if (e.response && e.response.data) {
|
||||||
|
errorMsg = e.response.data.msg || e.response.data.message || JSON.stringify(e.response.data)
|
||||||
|
} else if (e.message) {
|
||||||
|
errorMsg = e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$modal.msgError('操作失败:' + errorMsg)
|
||||||
|
} finally {
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 复制结果 */
|
||||||
|
copyResult() {
|
||||||
|
if (this.result && this.result.replacedContent) {
|
||||||
|
this.copyToClipboard(this.result.replacedContent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 复制到剪贴板 */
|
||||||
|
copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$modal.msgSuccess('✅ 复制成功!可以直接发送给用户了')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopyToClipboard(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopyToClipboard(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 降级复制方法 */
|
||||||
|
fallbackCopyToClipboard(text) {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$modal.msgSuccess('✅ 复制成功!')
|
||||||
|
} catch (err) {
|
||||||
|
this.$modal.msgError('复制失败')
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-panel {
|
||||||
|
border: 2px solid #DCDFE6;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-input, .textarea-output {
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-input >>> textarea {
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-output >>> textarea {
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 550px;
|
||||||
|
padding: 40px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 550px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
999
src/views/system/giftcoupon/index.vue
Normal file
999
src/views/system/giftcoupon/index.vue
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<list-layout>
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<template #search>
|
||||||
|
<mobile-search-form
|
||||||
|
:model="queryParams"
|
||||||
|
@search="handleQuery"
|
||||||
|
@reset="resetQuery"
|
||||||
|
>
|
||||||
|
<template #form="{ expanded }">
|
||||||
|
<el-form
|
||||||
|
:model="queryParams"
|
||||||
|
:inline="true"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="礼金Key">
|
||||||
|
<el-input v-model="queryParams.giftCouponKey" placeholder="礼金批次ID" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品SKU">
|
||||||
|
<el-input v-model="queryParams.skuId" placeholder="商品SKU ID" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input v-model="queryParams.skuName" placeholder="商品名称" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select v-model="queryParams.owner" placeholder="请选择" clearable size="small">
|
||||||
|
<el-option label="自营" value="g" />
|
||||||
|
<el-option label="POP" value="pop" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="过期状态">
|
||||||
|
<el-select v-model="queryParams.isExpired" placeholder="请选择" clearable size="small">
|
||||||
|
<el-option label="未过期" :value="0" />
|
||||||
|
<el-option label="已过期" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
size="small"
|
||||||
|
range-separator="至"
|
||||||
|
@change="handleDateRangeChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 桌面端搜索按钮 -->
|
||||||
|
<el-form-item v-if="!expanded">
|
||||||
|
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button type="success" size="small" icon="el-icon-plus" @click="handleCreate">创建礼金</el-button>
|
||||||
|
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['system:giftcoupon:export']">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</mobile-search-form>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
|
<div class="action-buttons-section mobile-only">
|
||||||
|
<mobile-button-group
|
||||||
|
:buttons="actionButtons"
|
||||||
|
:primary-count="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<div class="desktop-action-buttons desktop-only">
|
||||||
|
<el-button type="success" size="small" icon="el-icon-plus" @click="handleCreate">创建礼金</el-button>
|
||||||
|
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['system:giftcoupon:export']">导出</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 统计信息卡片 -->
|
||||||
|
<template #statistics>
|
||||||
|
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #409EFF;">{{ statistics.totalCount || 0 }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px;">礼金批次总数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #67C23A;">{{ statistics.totalUseCount || 0 }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px;">总使用次数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 24px; font-weight: bold; color: #E6A23C;">¥{{ formatMoney(statistics.totalOcsAmount || 0) }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px;">总分摊金额</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<template #table>
|
||||||
|
<el-table :data="list" v-loading="loading" border stripe :default-sort="{prop: 'createTime', order: 'descending'}">
|
||||||
|
<el-table-column label="礼金Key" prop="giftCouponKey" width="180" sortable>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>
|
||||||
|
<span style="margin-right: 8px;">{{ scope.row.giftCouponKey }}</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-copy-document"
|
||||||
|
@click="copyToClipboard(scope.row.giftCouponKey)"
|
||||||
|
title="复制礼金Key"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品SKU" prop="skuId" width="120" />
|
||||||
|
<el-table-column label="商品名称" prop="skuName" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="类型" prop="owner" width="80">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.owner === 'g' ? 'success' : 'warning'">
|
||||||
|
{{ scope.row.owner === 'g' ? '自营' : 'POP' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用次数" prop="useCount" width="100" sortable />
|
||||||
|
<el-table-column label="总分摊金额" prop="totalOcsAmount" width="120" sortable>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
¥{{ formatMoney(scope.row.totalOcsAmount || 0) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="createTime" width="160" sortable>
|
||||||
|
<template slot-scope="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="过期时间" prop="expireTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.expireTime">{{ parseTime(scope.row.expireTime) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="过期状态" prop="isExpired" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.isExpired === 1 ? 'danger' : 'success'">
|
||||||
|
{{ scope.row.isExpired === 1 ? '已过期' : '未过期' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="handleDetail(scope.row)">
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 分页区域 -->
|
||||||
|
<template #pagination>
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</list-layout>
|
||||||
|
|
||||||
|
<!-- 创建礼金对话框 -->
|
||||||
|
<el-dialog title="创建礼金" :visible.sync="createDialogVisible" width="500px" append-to-body>
|
||||||
|
<el-form :model="createForm" :rules="createRules" ref="createForm" label-width="120px">
|
||||||
|
<el-form-item label="商品链接/SKU" prop="materialUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="createForm.materialUrl"
|
||||||
|
placeholder="请输入商品链接或SKU ID,必须先点击查询按钮获取商品信息"
|
||||||
|
@keyup.enter.native="queryProductInfo"
|
||||||
|
>
|
||||||
|
<el-button slot="append" icon="el-icon-search" @click="queryProductInfo" :loading="queryLoading">查询</el-button>
|
||||||
|
</el-input>
|
||||||
|
<div style="color: #E6A23C; font-size: 12px; margin-top: 5px;">
|
||||||
|
<i class="el-icon-warning"></i>
|
||||||
|
<strong>重要:</strong>请先点击"查询"按钮获取商品信息(特别是SKU ID),然后再创建礼金。直接使用短链可能创建失败。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称" prop="skuName">
|
||||||
|
<el-input v-model="createForm.skuName" placeholder="查询商品信息后自动填充" :disabled="true" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品类型" prop="owner">
|
||||||
|
<el-input :value="createForm.owner === 'g' ? '自营' : (createForm.owner === 'pop' ? 'POP' : '未查询')" disabled>
|
||||||
|
<template slot="prepend">
|
||||||
|
<i :class="createForm.owner === 'g' ? 'el-icon-success' : (createForm.owner === 'pop' ? 'el-icon-warning' : 'el-icon-question')"
|
||||||
|
:style="{color: createForm.owner === 'g' ? '#67C23A' : (createForm.owner === 'pop' ? '#409EFF' : '#909399')}"></i>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 5px;">查询商品信息后自动识别(自营/POP)</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="礼金金额(元)" prop="amount">
|
||||||
|
<el-input-number v-model="createForm.amount" :min="1" :max="50" :precision="2" :step="0.01" style="width: 100%" />
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 5px;">范围:1-50元(参考JD项目要求)</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="礼金数量" prop="quantity">
|
||||||
|
<el-input-number v-model="createForm.quantity" :min="1" :max="100" style="width: 100%" />
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 5px;">范围:1-100</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="createDialogVisible = false">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog title="礼金详情" :visible.sync="detailVisible" width="1000px" append-to-body>
|
||||||
|
<div v-if="detailData && detailData.giftCoupon">
|
||||||
|
<el-tabs v-model="activeDetailTab">
|
||||||
|
<el-tab-pane label="基本信息" name="info">
|
||||||
|
<el-descriptions :column="2" border style="margin-top: 20px;">
|
||||||
|
<el-descriptions-item label="礼金Key" :span="2">
|
||||||
|
<span style="margin-right: 10px;">{{ detailData.giftCoupon.giftCouponKey }}</span>
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-copy-document" @click="copyToClipboard(detailData.giftCoupon.giftCouponKey)">复制</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品SKU">{{ detailData.giftCoupon.skuId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称" :span="2">{{ detailData.giftCoupon.skuName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="类型">
|
||||||
|
<el-tag :type="detailData.giftCoupon.owner === 'g' ? 'success' : 'warning'">
|
||||||
|
{{ detailData.giftCoupon.owner === 'g' ? '自营' : 'POP' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用次数">{{ detailData.giftCoupon.useCount }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总分摊金额">¥{{ formatMoney(detailData.giftCoupon.totalOcsAmount || 0) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ parseTime(detailData.giftCoupon.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="过期时间">
|
||||||
|
<span v-if="detailData.giftCoupon.expireTime">{{ parseTime(detailData.giftCoupon.expireTime) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="过期状态" :span="2">
|
||||||
|
<el-tag :type="detailData.giftCoupon.isExpired === 1 ? 'danger' : 'success'">
|
||||||
|
{{ detailData.giftCoupon.isExpired === 1 ? '已过期' : '未过期' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="关联订单" name="orders">
|
||||||
|
<el-table :data="detailData.orders || []" border style="margin-top: 20px;" max-height="400">
|
||||||
|
<el-table-column label="订单号" prop="orderId" width="200" />
|
||||||
|
<el-table-column label="商品名称" prop="skuName" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="商品SKU" prop="skuId" width="120" />
|
||||||
|
<el-table-column label="订单金额" prop="price" width="100">
|
||||||
|
<template slot-scope="scope">¥{{ formatMoney(scope.row.price || 0) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="礼金分摊" prop="giftCouponOcsAmount" width="100">
|
||||||
|
<template slot-scope="scope">¥{{ formatMoney(scope.row.giftCouponOcsAmount || 0) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="下单时间" prop="orderTime" width="160">
|
||||||
|
<template slot-scope="scope">{{ parseTime(scope.row.orderTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="短链" width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div v-if="scope.row.positionId">
|
||||||
|
<a :href="getShortUrl(scope.row.positionId)" target="_blank" style="margin-right: 8px;">查看短链</a>
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-copy-document" @click="copyToClipboard(getShortUrl(scope.row.positionId))">复制</el-button>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="!detailData.orders || detailData.orders.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||||
|
暂无关联订单
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="detailVisible = false">关 闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listGiftCoupons, getGiftCoupon, getGiftCouponStatistics, exportGiftCoupons } from '@/api/system/giftcoupon'
|
||||||
|
import { createGiftCoupon, transferWithGift, generatePromotionContent } from '@/api/system/jdorder'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import ListLayout from '@/components/ListLayout'
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
import { parseTime } from '@/utils/ruoyi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GiftCoupon',
|
||||||
|
components: {
|
||||||
|
ListLayout,
|
||||||
|
MobileSearchForm,
|
||||||
|
MobileButtonGroup
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
total: 0,
|
||||||
|
dateRange: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
giftCouponKey: undefined,
|
||||||
|
skuId: undefined,
|
||||||
|
skuName: undefined,
|
||||||
|
owner: undefined,
|
||||||
|
isExpired: undefined,
|
||||||
|
beginTime: null,
|
||||||
|
endTime: null
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
totalCount: 0,
|
||||||
|
totalUseCount: 0,
|
||||||
|
totalOcsAmount: 0
|
||||||
|
},
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
activeDetailTab: 'info',
|
||||||
|
createDialogVisible: false,
|
||||||
|
createLoading: false,
|
||||||
|
createForm: {
|
||||||
|
materialUrl: '',
|
||||||
|
skuName: '',
|
||||||
|
owner: 'g',
|
||||||
|
amount: 5, // 默认5元
|
||||||
|
quantity: 10, // 默认10
|
||||||
|
queryResult: null // 保存查询到的商品信息,包含materialUrl等
|
||||||
|
},
|
||||||
|
queryLoading: false, // 查询商品信息加载状态
|
||||||
|
createRules: {
|
||||||
|
materialUrl: [{ required: true, message: '请输入商品链接或SKU', trigger: 'blur' }],
|
||||||
|
amount: [{ required: true, message: '请输入礼金金额', trigger: 'blur' }],
|
||||||
|
quantity: [{ required: true, message: '请输入礼金数量', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
this.loadStatistics()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询列表 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listGiftCoupons(this.queryParams).then(res => {
|
||||||
|
this.list = (res.rows || res.data || [])
|
||||||
|
this.total = res.total || 0
|
||||||
|
this.loading = false
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 加载统计信息 */
|
||||||
|
loadStatistics() {
|
||||||
|
const params = { ...this.queryParams }
|
||||||
|
// 统计信息不需要分页参数
|
||||||
|
delete params.pageNum
|
||||||
|
delete params.pageSize
|
||||||
|
getGiftCouponStatistics(params).then(res => {
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
this.statistics = {
|
||||||
|
totalCount: res.data.useCount || 0,
|
||||||
|
totalUseCount: res.data.useCount || 0,
|
||||||
|
totalOcsAmount: res.data.totalOcsAmount || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
this.loadStatistics()
|
||||||
|
},
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.queryParams = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
giftCouponKey: undefined,
|
||||||
|
skuId: undefined,
|
||||||
|
skuName: undefined,
|
||||||
|
owner: undefined,
|
||||||
|
isExpired: undefined,
|
||||||
|
beginTime: null,
|
||||||
|
endTime: null
|
||||||
|
}
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
/** 处理日期范围变化 */
|
||||||
|
handleDateRangeChange(val) {
|
||||||
|
if (val && val.length === 2) {
|
||||||
|
this.queryParams.beginTime = val[0]
|
||||||
|
this.queryParams.endTime = val[1]
|
||||||
|
} else {
|
||||||
|
this.queryParams.beginTime = null
|
||||||
|
this.queryParams.endTime = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
handleExport() {
|
||||||
|
const params = { ...this.queryParams }
|
||||||
|
this.$modal.confirm('是否确认导出所有礼金数据项?').then(() => {
|
||||||
|
this.exportLoading = true
|
||||||
|
return exportGiftCoupons(params)
|
||||||
|
}).then(response => {
|
||||||
|
this.$download.excel(response, '礼金数据.xlsx')
|
||||||
|
this.exportLoading = false
|
||||||
|
}).catch(() => {
|
||||||
|
this.exportLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 创建礼金 */
|
||||||
|
handleCreate() {
|
||||||
|
this.createForm = {
|
||||||
|
materialUrl: '',
|
||||||
|
skuName: '',
|
||||||
|
owner: 'g',
|
||||||
|
amount: 5, // 默认5元
|
||||||
|
quantity: 10, // 默认10
|
||||||
|
queryResult: null // 查询结果
|
||||||
|
}
|
||||||
|
this.createDialogVisible = true
|
||||||
|
},
|
||||||
|
/** 查询商品信息(参考xbmessage中的流程) */
|
||||||
|
async queryProductInfo() {
|
||||||
|
const materialUrl = this.createForm.materialUrl.trim()
|
||||||
|
if (!materialUrl) {
|
||||||
|
this.$modal.msgWarning('请输入商品链接或SKU ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queryLoading = true
|
||||||
|
try {
|
||||||
|
// 调用转链接口查询商品信息
|
||||||
|
const res = await generatePromotionContent({ promotionContent: materialUrl })
|
||||||
|
console.log('查询商品信息返回:', res)
|
||||||
|
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
let productInfo = null
|
||||||
|
// 解析返回结果(参考xbmessage中的处理)
|
||||||
|
try {
|
||||||
|
// 优先使用data,如果没有则使用msg(根据实际返回格式)
|
||||||
|
let resultStr = res.data || res.msg
|
||||||
|
|
||||||
|
// 如果是字符串,需要解析
|
||||||
|
let parsed = null
|
||||||
|
if (typeof resultStr === 'string') {
|
||||||
|
parsed = JSON.parse(resultStr)
|
||||||
|
} else {
|
||||||
|
parsed = resultStr
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的数据:', parsed)
|
||||||
|
|
||||||
|
// 提取第一个商品信息(支持多种返回格式)
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
// 格式1:直接是数组
|
||||||
|
productInfo = parsed[0]
|
||||||
|
} else if (parsed && typeof parsed === 'object') {
|
||||||
|
// 格式2:对象中包含list数组
|
||||||
|
if (parsed.list && Array.isArray(parsed.list) && parsed.list.length > 0) {
|
||||||
|
productInfo = parsed.list[0]
|
||||||
|
}
|
||||||
|
// 格式3:对象中包含data数组
|
||||||
|
else if (parsed.data && Array.isArray(parsed.data) && parsed.data.length > 0) {
|
||||||
|
productInfo = parsed.data[0]
|
||||||
|
}
|
||||||
|
// 格式4:对象本身包含商品信息
|
||||||
|
else if (parsed.materialUrl || parsed.owner || parsed.skuName) {
|
||||||
|
productInfo = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('提取的商品信息:', productInfo)
|
||||||
|
|
||||||
|
if (productInfo) {
|
||||||
|
// 保存完整的查询结果
|
||||||
|
this.createForm.queryResult = productInfo
|
||||||
|
|
||||||
|
// 自动填充商品信息
|
||||||
|
this.createForm.skuName = productInfo.skuName || productInfo.title || productInfo.productName || productInfo.cleanSkuName || ''
|
||||||
|
// owner字段:'p'表示POP,'g'表示自营,其他默认'g'
|
||||||
|
const ownerValue = productInfo.owner || (productInfo.popId ? 'pop' : 'g') || 'g'
|
||||||
|
this.createForm.owner = ownerValue === 'p' ? 'pop' : (ownerValue === 'pop' ? 'pop' : 'g')
|
||||||
|
|
||||||
|
// 保存查询到的materialUrl、skuId或spuid,用于后续创建礼金
|
||||||
|
// 注意:spuid是SPU ID(商品ID),skuId是SKU ID(库存单位ID),不是同一个东西
|
||||||
|
if (productInfo.materialUrl) {
|
||||||
|
// 保存materialUrl到查询结果中,创建礼金时使用
|
||||||
|
console.log('查询到的materialUrl:', productInfo.materialUrl)
|
||||||
|
}
|
||||||
|
if (productInfo.skuId || productInfo.skuid) {
|
||||||
|
// 保存skuId(SKU ID),用于创建礼金
|
||||||
|
const skuIdValue = productInfo.skuId || productInfo.skuid
|
||||||
|
console.log('查询到的skuId(SKU ID):', skuIdValue)
|
||||||
|
}
|
||||||
|
if (productInfo.spuid) {
|
||||||
|
// spuid是SPU ID(商品ID),不是SKU ID
|
||||||
|
console.log('查询到的spuid(SPU ID,不是SKU ID):', productInfo.spuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$modal.msgSuccess('商品信息查询成功:' + (this.createForm.owner === 'g' ? '自营' : 'POP'))
|
||||||
|
} else {
|
||||||
|
console.warn('未找到商品信息,完整返回:', parsed)
|
||||||
|
this.$modal.msgWarning('未找到商品信息,请检查链接是否正确')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析商品信息失败', e, '原始数据:', res)
|
||||||
|
this.$modal.msgWarning('返回数据格式异常:' + e.message + ',请手动填写商品信息')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError('查询商品信息失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('查询商品信息异常', e)
|
||||||
|
this.$modal.msgError('查询失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.queryLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 从链接中提取SKU ID */
|
||||||
|
extractSkuIdFromUrl(url) {
|
||||||
|
if (!url) return null
|
||||||
|
// 如果是纯数字,直接返回
|
||||||
|
if (/^\d+$/.test(url.trim())) {
|
||||||
|
return url.trim()
|
||||||
|
}
|
||||||
|
// 从URL中提取SKU ID
|
||||||
|
// 京东链接格式:https://item.jd.com/100012043978.html 或 jd.com/123456.html
|
||||||
|
const skuMatch = url.match(/(?:item\.jd\.com|jd\.com)\/(\d+)/i)
|
||||||
|
if (skuMatch && skuMatch[1]) {
|
||||||
|
return skuMatch[1]
|
||||||
|
}
|
||||||
|
// 如果包含skuId参数
|
||||||
|
const paramMatch = url.match(/[?&]skuId=(\d+)/i)
|
||||||
|
if (paramMatch && paramMatch[1]) {
|
||||||
|
return paramMatch[1]
|
||||||
|
}
|
||||||
|
// 无法提取,返回原链接(让后端处理)
|
||||||
|
return url.trim()
|
||||||
|
},
|
||||||
|
/** 提交创建礼金 */
|
||||||
|
async submitCreate() {
|
||||||
|
this.$refs.createForm.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
// 金额校验:1-50元(参考JD项目中的isValidAmount方法)
|
||||||
|
if (!this.createForm.amount || this.createForm.amount < 1 || this.createForm.amount > 50) {
|
||||||
|
this.$modal.msgError('礼金金额必须在1-50元之间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.createForm.quantity || this.createForm.quantity < 1 || this.createForm.quantity > 100) {
|
||||||
|
this.$modal.msgError('礼金数量必须在1-100之间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取SKU ID或使用materialUrl
|
||||||
|
const materialUrl = this.createForm.materialUrl.trim()
|
||||||
|
if (!materialUrl) {
|
||||||
|
this.$modal.msgError('请输入商品链接或SKU ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const skuId = this.extractSkuIdFromUrl(materialUrl)
|
||||||
|
const isUrl = skuId !== materialUrl
|
||||||
|
|
||||||
|
this.createLoading = true
|
||||||
|
try {
|
||||||
|
// 构建请求参数(参考JD项目的参数格式)
|
||||||
|
const params = {
|
||||||
|
amount: this.createForm.amount,
|
||||||
|
quantity: this.createForm.quantity,
|
||||||
|
owner: this.createForm.owner || 'g',
|
||||||
|
skuName: this.createForm.skuName || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据提取结果设置skuId或materialUrl
|
||||||
|
// 参考京东API文档:skuMaterialId支持SKU ID、商品落地页地址(如https://item.jd.com/11144230.html)或materialUrl
|
||||||
|
// 优先使用查询商品信息时获取的数据
|
||||||
|
if (this.createForm.queryResult) {
|
||||||
|
// 如果查询过商品信息,根据商品类型选择优先使用materialUrl或spuid
|
||||||
|
const queryResult = this.createForm.queryResult
|
||||||
|
const isPop = this.createForm.owner === 'pop'
|
||||||
|
|
||||||
|
// POP商品:优先使用materialUrl(jingfen链接),如果没有则使用SKU ID或落地页地址
|
||||||
|
// 自营商品:优先使用skuId(SKU ID),如果没有则使用materialUrl
|
||||||
|
// 注意:spuid是SPU ID(商品ID),skuId是SKU ID(库存单位ID),创建礼金应该用skuId而不是spuid
|
||||||
|
if (isPop) {
|
||||||
|
// POP商品:优先使用materialUrl
|
||||||
|
if (queryResult.materialUrl) {
|
||||||
|
params.materialUrl = queryResult.materialUrl
|
||||||
|
console.log('POP商品,使用查询到的materialUrl(jingfen链接):', queryResult.materialUrl)
|
||||||
|
} else if ((queryResult.skuId || queryResult.skuid) && /^\d+$/.test(String(queryResult.skuId || queryResult.skuid))) {
|
||||||
|
// 如果没有materialUrl,使用SKU ID(京东API支持)
|
||||||
|
const skuIdValue = queryResult.skuId || queryResult.skuid
|
||||||
|
params.skuId = String(skuIdValue)
|
||||||
|
console.log('POP商品,materialUrl不可用,使用查询到的skuId(SKU ID):', skuIdValue)
|
||||||
|
} else {
|
||||||
|
// 降级:从用户输入的URL中提取SKU ID或使用完整URL
|
||||||
|
if (isUrl && skuId && /^\d+$/.test(skuId)) {
|
||||||
|
// 可以使用SKU ID
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('POP商品,使用从URL提取的SKU ID:', skuId)
|
||||||
|
} else if (isUrl && materialUrl.includes('item.jd.com')) {
|
||||||
|
// 商品落地页地址(https://item.jd.com/11144230.html)也可以
|
||||||
|
params.materialUrl = materialUrl
|
||||||
|
console.log('POP商品,使用商品落地页地址:', materialUrl)
|
||||||
|
} else if (isUrl) {
|
||||||
|
params.materialUrl = materialUrl
|
||||||
|
console.log('POP商品,使用用户输入的URL:', materialUrl)
|
||||||
|
} else if (/^\d+$/.test(skuId)) {
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('POP商品,使用纯数字SKU ID:', skuId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 自营商品:优先使用skuId(SKU ID),注意不是spuid
|
||||||
|
const skuIdValue = queryResult.skuId || queryResult.skuid
|
||||||
|
if (skuIdValue && /^\d+$/.test(String(skuIdValue))) {
|
||||||
|
params.skuId = String(skuIdValue)
|
||||||
|
console.log('自营商品,使用查询到的skuId(SKU ID):', skuIdValue)
|
||||||
|
} else if (queryResult.materialUrl) {
|
||||||
|
// 如果没有spuid,使用materialUrl
|
||||||
|
params.materialUrl = queryResult.materialUrl
|
||||||
|
console.log('自营商品,spuid不可用,使用materialUrl:', queryResult.materialUrl)
|
||||||
|
} else {
|
||||||
|
// 降级:从用户输入的URL中提取SKU ID
|
||||||
|
if (isUrl && skuId && /^\d+$/.test(skuId)) {
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('自营商品,使用从URL提取的SKU ID:', skuId)
|
||||||
|
} else if (isUrl) {
|
||||||
|
params.materialUrl = materialUrl
|
||||||
|
console.log('自营商品,使用用户输入的URL:', materialUrl)
|
||||||
|
} else if (/^\d+$/.test(skuId)) {
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('自营商品,使用纯数字SKU ID:', skuId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有查询过商品信息,使用用户输入的
|
||||||
|
if (isUrl && skuId && /^\d+$/.test(skuId)) {
|
||||||
|
// 如果从URL中提取到了纯数字SKU ID,优先使用skuId
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('使用从URL提取的SKU ID:', skuId)
|
||||||
|
} else if (isUrl) {
|
||||||
|
// 如果提取不到纯数字SKU ID,使用原始URL作为materialUrl
|
||||||
|
// 注意:短链(u.jd.com)可能无法用于创建礼金
|
||||||
|
params.materialUrl = materialUrl
|
||||||
|
console.log('使用用户输入的URL(可能是短链,可能不工作):', materialUrl)
|
||||||
|
} else if (/^\d+$/.test(skuId)) {
|
||||||
|
// 纯数字,作为SKU ID
|
||||||
|
params.skuId = skuId
|
||||||
|
console.log('使用纯数字SKU ID:', skuId)
|
||||||
|
} else {
|
||||||
|
// 其他情况,作为materialUrl
|
||||||
|
params.materialUrl = materialUrl
|
||||||
|
console.log('使用其他格式的URL:', materialUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保必须有skuId或materialUrl之一
|
||||||
|
if (!params.skuId && !params.materialUrl) {
|
||||||
|
this.$modal.msgError('无法确定商品标识,请先查询商品信息或输入有效的SKU ID')
|
||||||
|
this.createLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用的是短链(u.jd.com),提示必须查询
|
||||||
|
if (params.materialUrl && params.materialUrl.includes('u.jd.com')) {
|
||||||
|
this.$modal.msgError('不能直接使用短链创建礼金!请先点击"查询"按钮获取商品信息(SKU ID或jingfen链接),然后再创建。')
|
||||||
|
this.createLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有查询过,且使用的是普通链接而不是SKU ID,也提示
|
||||||
|
if (!this.createForm.queryResult && params.materialUrl && !params.skuId) {
|
||||||
|
const confirmMsg = '检测到您未查询商品信息。建议先点击"查询"按钮获取准确的商品信息(SKU ID),这样可以提高创建成功率。是否继续使用当前链接创建?'
|
||||||
|
try {
|
||||||
|
await this.$confirm(confirmMsg, '提示', {
|
||||||
|
confirmButtonText: '继续创建',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this.createLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用创建礼金接口
|
||||||
|
console.log('创建礼金请求参数:', params)
|
||||||
|
const res = await createGiftCoupon(params)
|
||||||
|
console.log('创建礼金返回结果:', res)
|
||||||
|
|
||||||
|
// 检查返回结果
|
||||||
|
if (!res) {
|
||||||
|
this.$modal.msgError('接口返回为空,请检查网络连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断成功条件:code === 200 且 giftCouponKey 不为null
|
||||||
|
// 注意:即使code是200,如果giftCouponKey为null,也应该视为失败
|
||||||
|
const hasGiftKey = res.data &&
|
||||||
|
(res.data.giftCouponKey !== null && res.data.giftCouponKey !== undefined && res.data.giftCouponKey !== '') ||
|
||||||
|
(res.data.giftKey !== null && res.data.giftKey !== undefined && res.data.giftKey !== '')
|
||||||
|
|
||||||
|
const isSuccess = res.code === 200 && hasGiftKey
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
// 解析返回的giftCouponKey(参考xbmessage中的处理方式)
|
||||||
|
let giftKey = null
|
||||||
|
|
||||||
|
// 先打印data的详细信息
|
||||||
|
console.log('返回的data类型:', typeof res.data, 'data内容:', res.data)
|
||||||
|
|
||||||
|
if (typeof res.data === 'string') {
|
||||||
|
// 如果是字符串,尝试解析
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(res.data)
|
||||||
|
console.log('data字符串解析后:', parsed)
|
||||||
|
giftKey = parsed.giftCouponKey || parsed.giftKey
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解析失败,可能就是字符串本身
|
||||||
|
giftKey = res.data
|
||||||
|
}
|
||||||
|
} else if (res.data && typeof res.data === 'object') {
|
||||||
|
// 尝试多种可能的字段名(优先顺序很重要)
|
||||||
|
// 1. 直接获取 giftCouponKey(但如果是null则不使用)
|
||||||
|
if (res.data.giftCouponKey !== null && res.data.giftCouponKey !== undefined && res.data.giftCouponKey !== '') {
|
||||||
|
giftKey = res.data.giftCouponKey
|
||||||
|
}
|
||||||
|
// 2. 获取 giftKey(但如果是null则不使用)
|
||||||
|
else if (res.data.giftKey !== null && res.data.giftKey !== undefined && res.data.giftKey !== '') {
|
||||||
|
giftKey = res.data.giftKey
|
||||||
|
}
|
||||||
|
// 3. 如果data里面还有data对象(嵌套情况)
|
||||||
|
else if (res.data.data && typeof res.data.data === 'object') {
|
||||||
|
if (res.data.data.giftCouponKey !== null && res.data.data.giftCouponKey !== undefined && res.data.data.giftCouponKey !== '') {
|
||||||
|
giftKey = res.data.data.giftCouponKey
|
||||||
|
} else if (res.data.data.giftKey !== null && res.data.data.giftKey !== undefined && res.data.data.giftKey !== '') {
|
||||||
|
giftKey = res.data.data.giftKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. 如果data是数组,取第一个
|
||||||
|
else if (Array.isArray(res.data) && res.data.length > 0) {
|
||||||
|
const first = res.data[0]
|
||||||
|
if (first.giftCouponKey !== null && first.giftCouponKey !== undefined && first.giftCouponKey !== '') {
|
||||||
|
giftKey = first.giftCouponKey
|
||||||
|
} else if (first.giftKey !== null && first.giftKey !== undefined && first.giftKey !== '') {
|
||||||
|
giftKey = first.giftKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析到的礼金Key:', giftKey, '完整data结构:', JSON.stringify(res.data, null, 2))
|
||||||
|
|
||||||
|
// 如果giftCouponKey是null,说明创建可能失败了
|
||||||
|
if (res.data && res.data.giftCouponKey === null) {
|
||||||
|
console.error('礼金Key为null,创建可能失败。完整返回:', JSON.stringify(res, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (giftKey) {
|
||||||
|
// 可选:自动生成短链(参考JD项目的完整流程)
|
||||||
|
try {
|
||||||
|
const transferParams = {}
|
||||||
|
// 优先使用SKU ID作为materialUrl
|
||||||
|
if (skuId && isUrl) {
|
||||||
|
transferParams.materialUrl = skuId
|
||||||
|
} else {
|
||||||
|
transferParams.materialUrl = materialUrl
|
||||||
|
}
|
||||||
|
transferParams.giftCouponKey = giftKey
|
||||||
|
|
||||||
|
console.log('转链请求参数:', transferParams)
|
||||||
|
const tf = await transferWithGift(transferParams)
|
||||||
|
console.log('转链返回结果:', tf)
|
||||||
|
|
||||||
|
if (tf && tf.code === 200) {
|
||||||
|
let shortUrl = ''
|
||||||
|
if (typeof tf.data === 'string') {
|
||||||
|
shortUrl = tf.data
|
||||||
|
} else if (tf.data && tf.data.shortURL) {
|
||||||
|
shortUrl = tf.data.shortURL
|
||||||
|
} else if (tf.data && typeof tf.data === 'object') {
|
||||||
|
// 尝试其他可能的字段
|
||||||
|
shortUrl = tf.data.url || tf.data.link || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortUrl) {
|
||||||
|
this.$modal.msgSuccess('礼金创建成功!短链:' + shortUrl)
|
||||||
|
// 复制短链到剪贴板
|
||||||
|
this.copyToClipboard(shortUrl)
|
||||||
|
} else {
|
||||||
|
this.$modal.msgSuccess('礼金创建成功!礼金Key:' + giftKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$modal.msgSuccess('礼金创建成功!礼金Key:' + giftKey)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('生成短链失败', e)
|
||||||
|
this.$modal.msgSuccess('礼金创建成功!礼金Key:' + giftKey + '(短链生成失败)')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有礼金Key,但接口返回成功
|
||||||
|
console.warn('未找到礼金Key,完整返回:', res)
|
||||||
|
console.warn('data的完整内容:', JSON.stringify(res.data, null, 2))
|
||||||
|
|
||||||
|
// 检查是否是null的情况
|
||||||
|
if (res.data && res.data.giftCouponKey === null) {
|
||||||
|
this.$modal.msgError('礼金创建失败:返回的礼金Key为null。可能是商品链接不正确、商品不支持创建礼金,或内部服务异常。请检查商品链接或稍后重试。')
|
||||||
|
} else {
|
||||||
|
// 其他情况
|
||||||
|
this.$modal.msgWarning('礼金创建完成,但未在返回数据中找到礼金Key。请查看控制台日志或联系技术支持。')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 即使失败也刷新列表(可能之前创建的会显示)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.getList()
|
||||||
|
this.loadStatistics()
|
||||||
|
}, 500)
|
||||||
|
return // 不关闭对话框,让用户知道出问题了
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭对话框并刷新
|
||||||
|
this.createDialogVisible = false
|
||||||
|
// 延迟刷新,确保用户能看到提示
|
||||||
|
setTimeout(() => {
|
||||||
|
this.getList()
|
||||||
|
this.loadStatistics()
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 错误处理 - 更详细的错误信息
|
||||||
|
console.error('创建礼金失败,返回:', res)
|
||||||
|
let errorMsg = '创建失败'
|
||||||
|
|
||||||
|
// 优先显示后端返回的错误信息
|
||||||
|
if (res.msg) {
|
||||||
|
errorMsg = res.msg
|
||||||
|
} else if (res.data && res.data.msg) {
|
||||||
|
errorMsg = res.data.msg
|
||||||
|
}
|
||||||
|
// 如果是code:200但giftCouponKey为null的情况
|
||||||
|
else if (res.code === 200 && res.data && (res.data.giftCouponKey === null || res.data.giftCouponKey === undefined)) {
|
||||||
|
// 优先使用后端返回的错误信息
|
||||||
|
if (res.data.msg) {
|
||||||
|
errorMsg = res.data.msg
|
||||||
|
} else {
|
||||||
|
errorMsg = '礼金创建失败:返回的礼金Key为null。可能的原因:\n1. 商品不支持创建礼金\n2. 商品类型(自营/POP)判断错误(当前为:' + this.createForm.owner + ')\n3. 京东API调用失败\n4. 商品已下架或不存在\n5. SKU ID或商品链接不正确\n\n请检查:\n- 商品链接是否正确\n- 商品类型是否匹配(查询到的owner是否正确)\n- 查看JD项目日志获取详细错误信息\n- 尝试重新查询商品信息'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 其他错误情况
|
||||||
|
else if (res.message) {
|
||||||
|
errorMsg = res.message
|
||||||
|
} else if (typeof res === 'string') {
|
||||||
|
errorMsg = res
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$modal.msgError('创建失败:' + errorMsg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('创建礼金异常', e)
|
||||||
|
let errorMsg = '未知错误'
|
||||||
|
if (e.response && e.response.data) {
|
||||||
|
errorMsg = e.response.data.msg || e.response.data.message || JSON.stringify(e.response.data)
|
||||||
|
} else if (e.message) {
|
||||||
|
errorMsg = e.message
|
||||||
|
}
|
||||||
|
this.$modal.msgError('创建失败:' + errorMsg)
|
||||||
|
} finally {
|
||||||
|
this.createLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 查看详情 */
|
||||||
|
async handleDetail(row) {
|
||||||
|
this.activeDetailTab = 'info'
|
||||||
|
this.detailVisible = true
|
||||||
|
try {
|
||||||
|
const res = await getGiftCoupon(row.giftCouponKey)
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
if (res.data.giftCoupon) {
|
||||||
|
this.detailData = res.data
|
||||||
|
} else {
|
||||||
|
// 兼容直接返回GiftCoupon对象的情况
|
||||||
|
this.detailData = {
|
||||||
|
giftCoupon: res.data,
|
||||||
|
orders: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError('获取详情失败')
|
||||||
|
this.detailVisible = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$modal.msgError('获取详情失败')
|
||||||
|
this.detailVisible = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 生成短链URL */
|
||||||
|
getShortUrl(positionId) {
|
||||||
|
// 根据positionId生成短链,这里需要根据实际情况调整
|
||||||
|
if (!positionId) return ''
|
||||||
|
return `https://u.jd.com/${positionId}`
|
||||||
|
},
|
||||||
|
/** 复制到剪贴板 */
|
||||||
|
copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$modal.msgSuccess('复制成功')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopyToClipboard(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopyToClipboard(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 降级复制方法 */
|
||||||
|
fallbackCopyToClipboard(text) {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$modal.msgSuccess('复制成功')
|
||||||
|
} catch (err) {
|
||||||
|
this.$modal.msgError('复制失败')
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
},
|
||||||
|
/** 格式化金额 */
|
||||||
|
formatMoney(amount) {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toFixed(2)
|
||||||
|
},
|
||||||
|
/** 格式化时间 */
|
||||||
|
parseTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.action-buttons-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端和桌面端按钮组显示控制 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-section.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-action-buttons.desktop-only {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.el-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
1092
src/views/system/jd-instruction/index.vue
Normal file
1092
src/views/system/jd-instruction/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,729 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
top="5vh"
|
||||||
|
>
|
||||||
|
<!-- 自定义标题 -->
|
||||||
|
<div slot="title" class="dialog-title">
|
||||||
|
<i class="el-icon-user"></i>
|
||||||
|
<span>分销标识接收人配置</span>
|
||||||
|
<el-tag v-if="hasConfigured" type="success" size="mini" style="margin-left: 10px;">
|
||||||
|
<i class="el-icon-success"></i> 已配置 {{ configuredCount }} 项
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-container" v-loading="loading">
|
||||||
|
<!-- 左侧:配置表单 -->
|
||||||
|
<div class="config-left">
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<i class="el-icon-setting"></i>
|
||||||
|
<span>分销标识接收人映射</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
@click="handleAddConfig"
|
||||||
|
style="margin-left: auto;"
|
||||||
|
>
|
||||||
|
添加配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in configList"
|
||||||
|
:key="`config-${item.configId || item.configKey || index}-${index}`"
|
||||||
|
class="config-item"
|
||||||
|
>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="field-label">分销标识</label>
|
||||||
|
<div class="field-content">
|
||||||
|
<el-input
|
||||||
|
v-model="item.distributionMark"
|
||||||
|
placeholder="请输入分销标识(如:F、PDD、H-TF)"
|
||||||
|
size="small"
|
||||||
|
style="width: 200px;"
|
||||||
|
@blur="handleDistributionMarkChange(item, index)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleRemoveConfig(index)"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="field-label">接收人列表</label>
|
||||||
|
<div class="field-content">
|
||||||
|
<el-input
|
||||||
|
v-model="item.touser"
|
||||||
|
placeholder="请输入接收人列表,多个用逗号分隔(如:abc,bcd,efg)"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<div class="item-hint">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
配置说明:当订单的分销标识为"{{ item.distributionMark || '分销标识' }}"时,将发送给这些接收人
|
||||||
|
</div>
|
||||||
|
<div class="item-hint" style="margin-top: 4px;">
|
||||||
|
<i class="el-icon-key"></i>
|
||||||
|
配置键名:logistics.push.touser.{{ item.distributionMark || '分销标识' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="configList.length === 0" class="empty-state">
|
||||||
|
<i class="el-icon-document-add"></i>
|
||||||
|
<p>暂无配置,点击"添加配置"开始设置</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:状态信息 -->
|
||||||
|
<div class="config-right">
|
||||||
|
<!-- 配置状态提示 -->
|
||||||
|
<div class="status-card">
|
||||||
|
<div class="status-icon" :class="hasConfigured ? 'success' : 'warning'">
|
||||||
|
<i :class="hasConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-text">
|
||||||
|
<div class="status-title">{{ hasConfigured ? '配置完成' : '配置未完成' }}</div>
|
||||||
|
<div class="status-desc">
|
||||||
|
{{ hasConfigured ? `已配置 ${configuredCount} 个分销标识的接收人` : '请至少配置一个分销标识的接收人' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速帮助 -->
|
||||||
|
<div class="help-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="el-icon-question"></i>
|
||||||
|
<span>配置说明</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-content">
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span><strong>分销标识</strong>:必须与订单中的分销标识完全匹配(区分大小写)</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span><strong>接收人列表</strong>:企业微信用户ID,多个用逗号分隔,例如:abc,bcd,efg</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>配置会自动保存到系统配置表中</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>推送时会根据订单的分销标识自动匹配对应的接收人列表</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 常见分销标识 -->
|
||||||
|
<div class="help-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="el-icon-collection-tag"></i>
|
||||||
|
<span>常见分销标识</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-content">
|
||||||
|
<div class="tag-list">
|
||||||
|
<el-tag
|
||||||
|
v-for="mark in commonDistributionMarks"
|
||||||
|
:key="mark"
|
||||||
|
size="small"
|
||||||
|
@click="handleQuickAdd(mark)"
|
||||||
|
style="cursor: pointer; margin: 4px;"
|
||||||
|
>
|
||||||
|
{{ mark }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div slot="footer" class="footer-buttons">
|
||||||
|
<div class="footer-left">
|
||||||
|
<el-button @click="handleLoadFromOrders" :loading="loadLoading" icon="el-icon-refresh" size="small">
|
||||||
|
从订单加载分销标识
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button @click="handleClose" size="small">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave" :loading="saveLoading" size="small">
|
||||||
|
<i class="el-icon-check"></i> 保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listConfig, getConfigKey, addConfig, updateConfig } from '@/api/system/config'
|
||||||
|
import { listJDOrders } from '@/api/system/jdorder'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DistributionMarkTouserConfig',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
configList: [],
|
||||||
|
saveLoading: false,
|
||||||
|
loadLoading: false,
|
||||||
|
loading: false,
|
||||||
|
commonDistributionMarks: ['F', 'PDD', 'H-TF', 'H', 'TF'],
|
||||||
|
configKeyPrefix: 'logistics.push.touser.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasConfigured() {
|
||||||
|
return this.configList.some(item =>
|
||||||
|
item.distributionMark && item.touser && item.distributionMark.trim() && item.touser.trim()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
configuredCount() {
|
||||||
|
return this.configList.filter(item =>
|
||||||
|
item.distributionMark && item.touser && item.distributionMark.trim() && item.touser.trim()
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
this.visible = val
|
||||||
|
if (val) {
|
||||||
|
// 延迟一下确保对话框已经打开
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.loadConfig()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 关闭时清空列表,避免下次打开时显示旧数据
|
||||||
|
this.configList = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visible(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 加载当前配置 */
|
||||||
|
async loadConfig() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
// 先尝试从系统配置中加载已有的配置
|
||||||
|
// 由于我们不知道有哪些分销标识,先加载所有以 logistics.push.touser. 开头的配置
|
||||||
|
const res = await listConfig({
|
||||||
|
configKey: 'logistics.push.touser.',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('加载配置响应:', res)
|
||||||
|
|
||||||
|
// 重置配置列表
|
||||||
|
this.configList.splice(0, this.configList.length)
|
||||||
|
|
||||||
|
if (res && res.code === 200 && res.rows && res.rows.length > 0) {
|
||||||
|
console.log('原始配置数据:', res.rows)
|
||||||
|
|
||||||
|
// 过滤并转换配置项
|
||||||
|
const configs = res.rows
|
||||||
|
.filter(item => {
|
||||||
|
// 确保 configKey 存在且以配置前缀开头,并且不是正好等于前缀(即必须有分销标识后缀)
|
||||||
|
const isValid = item && item.configKey &&
|
||||||
|
item.configKey.startsWith(this.configKeyPrefix) &&
|
||||||
|
item.configKey.length > this.configKeyPrefix.length
|
||||||
|
if (!isValid && item) {
|
||||||
|
console.warn('过滤掉的无效配置项:', item)
|
||||||
|
}
|
||||||
|
return isValid
|
||||||
|
})
|
||||||
|
.map(item => {
|
||||||
|
const distributionMark = item.configKey.replace(this.configKeyPrefix, '').trim()
|
||||||
|
const configItem = {
|
||||||
|
distributionMark: distributionMark,
|
||||||
|
touser: item.configValue || '',
|
||||||
|
configId: item.configId,
|
||||||
|
configKey: item.configKey,
|
||||||
|
configName: item.configName || `${distributionMark}分销标识接收人`
|
||||||
|
}
|
||||||
|
console.log('解析配置项:', item.configKey, '->', configItem)
|
||||||
|
return configItem
|
||||||
|
})
|
||||||
|
.filter(item => {
|
||||||
|
// 再次过滤,确保分销标识不为空
|
||||||
|
const isValid = item.distributionMark && item.distributionMark.length > 0
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn('过滤掉的分销标识为空的配置项:', item)
|
||||||
|
}
|
||||||
|
return isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用 splice 确保 Vue 2 的响应式更新
|
||||||
|
this.configList.splice(0, 0, ...configs)
|
||||||
|
console.log('解析后的配置列表:', this.configList)
|
||||||
|
console.log('配置列表长度:', this.configList.length)
|
||||||
|
|
||||||
|
if (configs.length > 0) {
|
||||||
|
this.$message.success(`成功加载 ${configs.length} 条配置`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有配置,至少添加一个空项
|
||||||
|
if (this.configList.length === 0) {
|
||||||
|
this.configList.push({
|
||||||
|
distributionMark: '',
|
||||||
|
touser: '',
|
||||||
|
configId: null,
|
||||||
|
configKey: '',
|
||||||
|
configName: ''
|
||||||
|
})
|
||||||
|
console.log('未找到配置,添加空项')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载配置失败:', e)
|
||||||
|
this.$message.error('加载配置失败:' + (e.message || '未知错误'))
|
||||||
|
// 即使加载失败,也至少添加一个空项
|
||||||
|
if (this.configList.length === 0) {
|
||||||
|
this.configList.push({
|
||||||
|
distributionMark: '',
|
||||||
|
touser: '',
|
||||||
|
configId: null,
|
||||||
|
configKey: '',
|
||||||
|
configName: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从订单中加载分销标识 */
|
||||||
|
async handleLoadFromOrders() {
|
||||||
|
this.loadLoading = true
|
||||||
|
try {
|
||||||
|
const res = await listJDOrders({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200 && res.rows) {
|
||||||
|
// 提取所有不重复的分销标识
|
||||||
|
const distributionMarks = [...new Set(
|
||||||
|
res.rows
|
||||||
|
.map(order => order.distributionMark)
|
||||||
|
.filter(mark => mark && mark.trim())
|
||||||
|
)].sort()
|
||||||
|
|
||||||
|
// 为每个分销标识添加配置项(如果不存在)
|
||||||
|
distributionMarks.forEach(mark => {
|
||||||
|
const exists = this.configList.some(item => item.distributionMark === mark)
|
||||||
|
if (!exists) {
|
||||||
|
this.configList.push({
|
||||||
|
distributionMark: mark,
|
||||||
|
touser: '',
|
||||||
|
configId: null,
|
||||||
|
configKey: this.configKeyPrefix + mark,
|
||||||
|
configName: `${mark}分销标识接收人`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$message.success(`已加载 ${distributionMarks.length} 个分销标识`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载分销标识失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.loadLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 快速添加分销标识 */
|
||||||
|
handleQuickAdd(mark) {
|
||||||
|
const exists = this.configList.some(item => item.distributionMark === mark)
|
||||||
|
if (!exists) {
|
||||||
|
this.configList.push({
|
||||||
|
distributionMark: mark,
|
||||||
|
touser: '',
|
||||||
|
configId: null,
|
||||||
|
configKey: this.configKeyPrefix + mark,
|
||||||
|
configName: `${mark}分销标识接收人`
|
||||||
|
})
|
||||||
|
this.$message.success(`已添加分销标识:${mark}`)
|
||||||
|
} else {
|
||||||
|
this.$message.info(`分销标识 ${mark} 已存在`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 添加配置项 */
|
||||||
|
handleAddConfig() {
|
||||||
|
this.configList.push({
|
||||||
|
distributionMark: '',
|
||||||
|
touser: '',
|
||||||
|
configId: null,
|
||||||
|
configKey: '',
|
||||||
|
configName: ''
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 删除配置项 */
|
||||||
|
handleRemoveConfig(index) {
|
||||||
|
this.configList.splice(index, 1)
|
||||||
|
// 如果删除后列表为空,至少保留一个空项
|
||||||
|
if (this.configList.length === 0) {
|
||||||
|
this.handleAddConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 分销标识变化时更新配置键名 */
|
||||||
|
handleDistributionMarkChange(item, index) {
|
||||||
|
if (item.distributionMark && item.distributionMark.trim()) {
|
||||||
|
item.configKey = this.configKeyPrefix + item.distributionMark.trim()
|
||||||
|
item.configName = `${item.distributionMark.trim()}分销标识接收人`
|
||||||
|
} else {
|
||||||
|
item.configKey = ''
|
||||||
|
item.configName = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存配置 */
|
||||||
|
async handleSave() {
|
||||||
|
// 验证配置
|
||||||
|
const validConfigs = this.configList.filter(item =>
|
||||||
|
item.distributionMark && item.distributionMark.trim() &&
|
||||||
|
item.touser && item.touser.trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (validConfigs.length === 0) {
|
||||||
|
this.$message.warning('请至少配置一个有效的分销标识和接收人')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有重复的分销标识
|
||||||
|
const marks = validConfigs.map(item => item.distributionMark.trim())
|
||||||
|
const uniqueMarks = [...new Set(marks)]
|
||||||
|
if (marks.length !== uniqueMarks.length) {
|
||||||
|
this.$message.warning('存在重复的分销标识,请检查')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveLoading = true
|
||||||
|
try {
|
||||||
|
// 保存每个配置项
|
||||||
|
const savePromises = validConfigs.map(async (item) => {
|
||||||
|
const configData = {
|
||||||
|
configKey: this.configKeyPrefix + item.distributionMark.trim(),
|
||||||
|
configName: `${item.distributionMark.trim()}分销标识接收人`,
|
||||||
|
configValue: item.touser.trim(),
|
||||||
|
configType: 'N',
|
||||||
|
remark: `分销标识 ${item.distributionMark.trim()} 对应的企业微信接收人列表`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.configId) {
|
||||||
|
// 更新现有配置
|
||||||
|
configData.configId = item.configId
|
||||||
|
return updateConfig(configData)
|
||||||
|
} else {
|
||||||
|
// 新增配置
|
||||||
|
return addConfig(configData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(savePromises)
|
||||||
|
this.$message.success(`配置保存成功!共保存 ${validConfigs.length} 项配置`)
|
||||||
|
|
||||||
|
// 重新加载配置
|
||||||
|
await this.loadConfig()
|
||||||
|
this.$emit('config-updated')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('保存失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 关闭对话框 */
|
||||||
|
handleClose() {
|
||||||
|
this.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 标题样式 */
|
||||||
|
.dialog-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 容器布局 */
|
||||||
|
.config-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置区块 */
|
||||||
|
.config-section {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置列表 */
|
||||||
|
.config-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-content {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-hint i {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态卡片 */
|
||||||
|
.status-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.success {
|
||||||
|
background: rgba(103, 194, 58, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.warning {
|
||||||
|
background: rgba(230, 162, 60, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 帮助卡片 */
|
||||||
|
.help-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item i {
|
||||||
|
color: #67c23a;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮 */
|
||||||
|
.footer-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left,
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.config-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,739 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
top="5vh"
|
||||||
|
>
|
||||||
|
<!-- 自定义标题 -->
|
||||||
|
<div slot="title" class="dialog-title">
|
||||||
|
<i class="el-icon-setting"></i>
|
||||||
|
<span>H-TF订单自动写入配置</span>
|
||||||
|
<el-tag v-if="config.isConfigured" type="success" size="mini" style="margin-left: 10px;">
|
||||||
|
<i class="el-icon-success"></i> 已配置
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="warning" size="mini" style="margin-left: 10px;">
|
||||||
|
<i class="el-icon-warning"></i> 未配置
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-container">
|
||||||
|
<!-- 左侧:配置表单 -->
|
||||||
|
<div class="config-left">
|
||||||
|
<!-- 授权状态 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<i class="el-icon-key"></i>
|
||||||
|
<span>授权状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-status">
|
||||||
|
<el-tag v-if="config.hasAccessToken" type="success" size="medium">
|
||||||
|
<i class="el-icon-circle-check"></i> {{ config.accessTokenStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="medium">
|
||||||
|
<i class="el-icon-circle-close"></i> {{ config.accessTokenStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-if="!config.hasAccessToken"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-unlock"
|
||||||
|
@click="handleAuth"
|
||||||
|
>
|
||||||
|
立即授权
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档配置表单 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span>目标文档</span>
|
||||||
|
</div>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px" size="small">
|
||||||
|
<el-form-item label="文件ID" prop="fileId">
|
||||||
|
<el-input
|
||||||
|
v-model="form.fileId"
|
||||||
|
placeholder="例如:DUW50RUprWXh2TGJK"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
slot="append"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleFetchSheets"
|
||||||
|
:disabled="!form.fileId"
|
||||||
|
>
|
||||||
|
获取工作表
|
||||||
|
</el-button>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="工作表ID" prop="sheetId">
|
||||||
|
<el-select
|
||||||
|
v-if="sheetList.length > 0"
|
||||||
|
v-model="form.sheetId"
|
||||||
|
placeholder="请选择工作表"
|
||||||
|
style="width: 100%;"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="sheet in sheetList"
|
||||||
|
:key="sheet.sheetId"
|
||||||
|
:label="sheet.title"
|
||||||
|
:value="sheet.sheetId"
|
||||||
|
>
|
||||||
|
<span style="float: left">{{ sheet.title }}</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 12px;">{{ sheet.sheetId }}</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="form.sheetId"
|
||||||
|
placeholder="例如:BB08J2"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="表头行号" prop="headerRow">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.headerRow"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="起始行号" prop="startRow">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.startRow"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:状态信息 -->
|
||||||
|
<div class="config-right">
|
||||||
|
<!-- 配置状态提示 -->
|
||||||
|
<div class="status-card">
|
||||||
|
<div class="status-icon" :class="config.isConfigured ? 'success' : 'warning'">
|
||||||
|
<i :class="config.isConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-text">
|
||||||
|
<div class="status-title">{{ config.isConfigured ? '配置完成' : '配置未完成' }}</div>
|
||||||
|
<div class="status-desc">{{ config.hint }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格行数(从接口获取,用于决定同步范围) -->
|
||||||
|
<div v-if="config.progressHint || config.currentProgress" class="progress-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="el-icon-data-line"></i>
|
||||||
|
<span>表格行数</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-content">
|
||||||
|
<div v-if="config.currentProgress" class="progress-detail">
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="label">当前有数据行数</span>
|
||||||
|
<span class="value">第 {{ config.currentProgress }} 行(接口获取)</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="label">下次同步起始</span>
|
||||||
|
<span class="value">第 {{ config.nextStartRow != null ? config.nextStartRow : form.startRow }} 行</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-hint">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
由接口实时获取表格行数,不再使用本地保存的进度
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-progress">
|
||||||
|
{{ config.progressHint || '暂无表格行数(请先授权并配置)' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速帮助 -->
|
||||||
|
<div class="help-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="el-icon-question"></i>
|
||||||
|
<span>配置说明</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-content">
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>文件ID从腾讯文档URL中获取</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>点击"获取工作表"自动加载</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>表头行号默认为第2行</span>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<i class="el-icon-check"></i>
|
||||||
|
<span>数据起始行默认为第3行</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div slot="footer" class="footer-buttons">
|
||||||
|
<div class="footer-left">
|
||||||
|
<el-button @click="showOperationLogs = true" icon="el-icon-document" size="small">
|
||||||
|
操作日志
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleTest" :loading="testLoading" icon="el-icon-setting" size="small">
|
||||||
|
测试配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button @click="handleClear" :loading="clearLoading" type="danger" plain size="small">
|
||||||
|
清除配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleClose" size="small">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave" :loading="saveLoading" size="small">
|
||||||
|
<i class="el-icon-check"></i> 保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作日志查看对话框 -->
|
||||||
|
<tencent-doc-operation-logs
|
||||||
|
v-model="showOperationLogs"
|
||||||
|
:file-id="form.fileId"
|
||||||
|
:sheet-id="form.sheetId"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getAutoWriteConfig,
|
||||||
|
updateAutoWriteConfig,
|
||||||
|
testAutoWriteConfig,
|
||||||
|
clearAutoWriteConfig,
|
||||||
|
getDocSheetList,
|
||||||
|
getTencentDocAuthUrl
|
||||||
|
} from '@/api/jarvis/tendoc'
|
||||||
|
import TencentDocOperationLogs from './TencentDocOperationLogs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TencentDocAutoWriteConfig',
|
||||||
|
components: {
|
||||||
|
TencentDocOperationLogs
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showOperationLogs: false,
|
||||||
|
visible: false,
|
||||||
|
config: {
|
||||||
|
hasAccessToken: false,
|
||||||
|
accessTokenStatus: '未授权',
|
||||||
|
fileId: '',
|
||||||
|
sheetId: '',
|
||||||
|
appId: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
isConfigured: false,
|
||||||
|
hint: ''
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
fileId: '',
|
||||||
|
sheetId: '',
|
||||||
|
headerRow: 2,
|
||||||
|
startRow: 3
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
fileId: [
|
||||||
|
{ required: true, message: '请输入文件ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
sheetId: [
|
||||||
|
{ required: true, message: '请输入工作表ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
headerRow: [
|
||||||
|
{ required: true, message: '请输入表头行号', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '表头行号必须大于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
startRow: [
|
||||||
|
{ required: true, message: '请输入数据起始行', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '数据起始行必须大于0', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sheetList: [],
|
||||||
|
saveLoading: false,
|
||||||
|
testLoading: false,
|
||||||
|
clearLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
this.visible = val
|
||||||
|
if (val) {
|
||||||
|
this.loadConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visible(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 加载当前配置 */
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const res = await getAutoWriteConfig()
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
this.config = res.data
|
||||||
|
this.form.fileId = res.data.fileId || ''
|
||||||
|
this.form.sheetId = res.data.sheetId || ''
|
||||||
|
// 确保 headerRow 和 startRow 是数字类型
|
||||||
|
this.form.headerRow = parseInt(res.data.headerRow) || 2
|
||||||
|
this.form.startRow = parseInt(res.data.startRow) || 3
|
||||||
|
console.log('配置加载成功 - headerRow:', this.form.headerRow, 'startRow:', this.form.startRow)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载配置失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 打开授权页面 */
|
||||||
|
async handleAuth() {
|
||||||
|
try {
|
||||||
|
const res = await getTencentDocAuthUrl()
|
||||||
|
if (res.code !== 200 || !res.data) {
|
||||||
|
this.$message.error('获取授权URL失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUrl = res.data
|
||||||
|
const width = 600
|
||||||
|
const height = 700
|
||||||
|
const left = (window.screen.width - width) / 2
|
||||||
|
const top = (window.screen.height - height) / 2
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
authUrl,
|
||||||
|
'腾讯文档授权',
|
||||||
|
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$message.success('授权页面已打开,请在新窗口中完成授权')
|
||||||
|
|
||||||
|
// 1秒后刷新配置状态
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadConfig()
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('打开授权页面失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取工作表列表 */
|
||||||
|
async handleFetchSheets() {
|
||||||
|
if (!this.form.fileId) {
|
||||||
|
this.$message.warning('请先输入文件ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$message.info('正在获取工作表列表...')
|
||||||
|
const res = await getDocSheetList(this.form.fileId)
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data && res.data.sheets) {
|
||||||
|
this.sheetList = res.data.sheets
|
||||||
|
this.$message.success(`获取成功,共 ${this.sheetList.length} 个工作表`)
|
||||||
|
} else {
|
||||||
|
this.$message.error('获取工作表列表失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('获取工作表列表失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存配置 */
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(async valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveLoading = true
|
||||||
|
try {
|
||||||
|
const res = await updateAutoWriteConfig({
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
sheetId: this.form.sheetId,
|
||||||
|
headerRow: this.form.headerRow,
|
||||||
|
startRow: this.form.startRow
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success(`配置保存成功!表头第${this.form.headerRow}行,数据从第${this.form.startRow}行开始`)
|
||||||
|
console.log('配置保存成功 - 保存的值:', {
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
sheetId: this.form.sheetId,
|
||||||
|
headerRow: this.form.headerRow,
|
||||||
|
startRow: this.form.startRow
|
||||||
|
})
|
||||||
|
// 延迟重新加载配置,确保后端已保存
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadConfig()
|
||||||
|
}, 500)
|
||||||
|
this.$emit('config-updated')
|
||||||
|
} else {
|
||||||
|
this.$message.error('保存失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('保存失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 测试配置 */
|
||||||
|
async handleTest() {
|
||||||
|
this.testLoading = true
|
||||||
|
try {
|
||||||
|
const res = await testAutoWriteConfig()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$alert(
|
||||||
|
'<pre style="text-align: left; max-height: 400px; overflow: auto;">' +
|
||||||
|
JSON.stringify(res.data, null, 2) +
|
||||||
|
'</pre>',
|
||||||
|
'测试成功',
|
||||||
|
{
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.$message.error('测试失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('测试失败:' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.testLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清除配置 */
|
||||||
|
async handleClear() {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要清除配置吗?这不会清除授权令牌。', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.clearLoading = true
|
||||||
|
const res = await clearAutoWriteConfig()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('配置已清除')
|
||||||
|
this.form.fileId = ''
|
||||||
|
this.form.sheetId = ''
|
||||||
|
this.form.startRow = 3
|
||||||
|
this.sheetList = []
|
||||||
|
this.loadConfig()
|
||||||
|
this.$emit('config-updated')
|
||||||
|
} else {
|
||||||
|
this.$message.error('清除失败:' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
this.$message.error('清除失败:' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.clearLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 关闭对话框 */
|
||||||
|
handleClose() {
|
||||||
|
this.visible = false
|
||||||
|
this.sheetList = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 标题样式 */
|
||||||
|
.dialog-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 容器布局 */
|
||||||
|
.config-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置区块 */
|
||||||
|
.config-section {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 授权状态 */
|
||||||
|
.auth-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态卡片 */
|
||||||
|
.status-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.success {
|
||||||
|
background: rgba(103, 194, 58, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.warning {
|
||||||
|
background: rgba(230, 162, 60, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度卡片 */
|
||||||
|
.progress-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header i {
|
||||||
|
margin-right: 6px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fef0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #f56c6c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-progress {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 帮助卡片 */
|
||||||
|
.help-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-item i {
|
||||||
|
color: #67c23a;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮 */
|
||||||
|
.footer-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left,
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.config-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element UI 覆盖样式 */
|
||||||
|
.config-section >>> .el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section >>> .el-form-item__label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section >>> .el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
316
src/views/system/jdorder/components/TencentDocOperationLogs.vue
Normal file
316
src/views/system/jdorder/components/TencentDocOperationLogs.vue
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
title="腾讯文档操作日志"
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="90%"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="加载中..."
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.9)"
|
||||||
|
>
|
||||||
|
<!-- 搜索条件 -->
|
||||||
|
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="80px">
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.orderNo"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作类型">
|
||||||
|
<el-select v-model="queryParams.operationType" placeholder="请选择" clearable size="small" style="width: 150px">
|
||||||
|
<el-option label="批量同步" value="BATCH_SYNC" />
|
||||||
|
<el-option label="单个写入" value="WRITE_SINGLE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作状态">
|
||||||
|
<el-select v-model="queryParams.operationStatus" placeholder="请选择" clearable size="small" style="width: 150px">
|
||||||
|
<el-option label="成功" value="SUCCESS" />
|
||||||
|
<el-option label="失败" value="FAILED" />
|
||||||
|
<el-option label="跳过" value="SKIPPED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 统计信息卡片 -->
|
||||||
|
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<div style="font-size: 24px; color: #67C23A; font-weight: bold">{{ statistics.success }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px">成功</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<div style="font-size: 24px; color: #E6A23C; font-weight: bold">{{ statistics.skipped }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px">跳过</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<div style="font-size: 24px; color: #F56C6C; font-weight: bold">{{ statistics.failed }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px">失败</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<div style="font-size: 24px; color: #409EFF; font-weight: bold">{{ statistics.total }}</div>
|
||||||
|
<div style="color: #909399; margin-top: 8px">总计</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 日志表格 -->
|
||||||
|
<el-table
|
||||||
|
:data="logList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
max-height="500"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
|
|
||||||
|
<el-table-column label="操作类型" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.operationType === 'BATCH_SYNC'" type="primary" size="small">批量同步</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.operationType === 'WRITE_SINGLE'" type="success" size="small">单个写入</el-tag>
|
||||||
|
<el-tag v-else size="small">{{ scope.row.operationType }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="订单号" prop="orderNo" width="180" />
|
||||||
|
|
||||||
|
<el-table-column label="目标行" prop="targetRow" width="80" align="center" />
|
||||||
|
|
||||||
|
<el-table-column label="物流链接" prop="logisticsLink" min-width="200" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<a v-if="scope.row.logisticsLink" :href="scope.row.logisticsLink" target="_blank" style="color: #409EFF">
|
||||||
|
{{ scope.row.logisticsLink }}
|
||||||
|
</a>
|
||||||
|
<span v-else style="color: #C0C4CC">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作状态" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.operationStatus === 'SUCCESS'" type="success" size="small">成功</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.operationStatus === 'FAILED'" type="danger" size="small">失败</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.operationStatus === 'SKIPPED'" type="warning" size="small">跳过</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">{{ scope.row.operationStatus }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="错误信息" prop="errorMessage" min-width="200" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.errorMessage" style="color: #F56C6C">{{ scope.row.errorMessage }}</span>
|
||||||
|
<span v-else style="color: #C0C4CC">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作人" prop="operator" width="100" align="center" />
|
||||||
|
|
||||||
|
<el-table-column label="操作时间" prop="createTime" width="160" align="center" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div style="text-align: right; margin-top: 20px">
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="queryParams.pageNum"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
|
<el-button type="primary" icon="el-icon-refresh" @click="handleQuery">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getOperationLogs } from '@/api/jarvis/tendoc'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TencentDocOperationLogs',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
fileId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
sheetId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
logList: [],
|
||||||
|
total: 0,
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
fileId: '',
|
||||||
|
sheetId: '',
|
||||||
|
orderNo: '',
|
||||||
|
operationType: '',
|
||||||
|
operationStatus: ''
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
if (val) {
|
||||||
|
this.loading = false // 重置loading状态
|
||||||
|
this.initData()
|
||||||
|
this.getList()
|
||||||
|
} else {
|
||||||
|
this.loading = false // 关闭时也重置loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initData() {
|
||||||
|
this.queryParams.fileId = this.fileId
|
||||||
|
this.queryParams.sheetId = this.sheetId
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 查询日志列表 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
getOperationLogs(this.queryParams).then(res => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.logList = res.data || []
|
||||||
|
this.total = this.logList.length
|
||||||
|
this.calculateStatistics()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询失败')
|
||||||
|
this.logList = []
|
||||||
|
this.total = 0
|
||||||
|
this.calculateStatistics()
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
this.$message.error('查询失败: ' + (e.message || '未知错误'))
|
||||||
|
console.error('查询操作日志失败', e)
|
||||||
|
this.logList = []
|
||||||
|
this.total = 0
|
||||||
|
this.calculateStatistics()
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 计算统计数据 */
|
||||||
|
calculateStatistics() {
|
||||||
|
this.statistics = {
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
|
total: this.logList.length
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logList.forEach(log => {
|
||||||
|
if (log.operationStatus === 'SUCCESS') {
|
||||||
|
this.statistics.success++
|
||||||
|
} else if (log.operationStatus === 'FAILED') {
|
||||||
|
this.statistics.failed++
|
||||||
|
} else if (log.operationStatus === 'SKIPPED') {
|
||||||
|
this.statistics.skipped++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.queryParams = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
fileId: this.fileId,
|
||||||
|
sheetId: this.sheetId,
|
||||||
|
orderNo: '',
|
||||||
|
operationType: '',
|
||||||
|
operationStatus: ''
|
||||||
|
}
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 分页 */
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.queryParams.pageSize = val
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.queryParams.pageNum = val
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 关闭对话框 */
|
||||||
|
handleClose() {
|
||||||
|
this.loading = false // 确保关闭loading状态
|
||||||
|
this.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-card {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
976
src/views/system/jdorder/components/TencentDocPushMonitor.vue
Normal file
976
src/views/system/jdorder/components/TencentDocPushMonitor.vue
Normal file
@@ -0,0 +1,976 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
title="腾讯文档推送监控"
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="handleVisibleChange"
|
||||||
|
:width="isMobile ? '100%' : '1200px'"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
:top="isMobile ? '0' : '5vh'"
|
||||||
|
:fullscreen="isMobile"
|
||||||
|
:custom-class="isMobile ? 'mobile-push-monitor-dialog' : ''"
|
||||||
|
:modal-append-to-body="true"
|
||||||
|
:append-to-body="true"
|
||||||
|
>
|
||||||
|
<div class="push-monitor">
|
||||||
|
<!-- 倒计时和状态卡片 -->
|
||||||
|
<el-card class="countdown-card" shadow="hover">
|
||||||
|
<div class="countdown-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<i class="el-icon-timer"></i>
|
||||||
|
<span class="title">自动推送倒计时</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-tag v-if="pushStatus.isScheduled" type="warning" size="medium">
|
||||||
|
<i class="el-icon-loading"></i> 等待推送中
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="info" size="medium">
|
||||||
|
<i class="el-icon-circle-check"></i> 无待推送任务
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="countdown-content">
|
||||||
|
<div class="countdown-display" :class="{active: pushStatus.isScheduled}">
|
||||||
|
<div class="time-box">
|
||||||
|
<span class="time-value">{{ countdownDisplay.minutes }}</span>
|
||||||
|
<span class="time-label">分</span>
|
||||||
|
</div>
|
||||||
|
<span class="time-separator">:</span>
|
||||||
|
<div class="time-box">
|
||||||
|
<span class="time-value">{{ countdownDisplay.seconds }}</span>
|
||||||
|
<span class="time-label">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="countdown-info">
|
||||||
|
<div v-if="pushStatus.scheduledTime" class="info-item">
|
||||||
|
<i class="el-icon-time"></i>
|
||||||
|
<span>预计推送时间:{{ formatDateTime(pushStatus.scheduledTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="pushStatus.lastSuccessRecord" class="info-item">
|
||||||
|
<i class="el-icon-success"></i>
|
||||||
|
<span>上次推送:{{ formatDateTime(pushStatus.lastSuccessRecord.endTime) }}</span>
|
||||||
|
<el-tag size="mini" type="success" style="margin-left: 10px;">
|
||||||
|
成功 {{ pushStatus.lastSuccessRecord.successCount }} 条
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="countdown-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
:loading="pushing"
|
||||||
|
:size="isMobile ? 'small' : 'default'"
|
||||||
|
@click="handleTriggerPushNow"
|
||||||
|
>
|
||||||
|
立即推送
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
icon="el-icon-close"
|
||||||
|
:disabled="!pushStatus.isScheduled"
|
||||||
|
:size="isMobile ? 'small' : 'default'"
|
||||||
|
@click="handleCancelPush"
|
||||||
|
>
|
||||||
|
取消推送
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
:size="isMobile ? 'small' : 'default'"
|
||||||
|
@click="loadPushStatus"
|
||||||
|
>
|
||||||
|
刷新状态
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 推送记录列表 -->
|
||||||
|
<el-card class="records-card" shadow="hover">
|
||||||
|
<div slot="header" class="records-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<i class="el-icon-document-copy"></i>
|
||||||
|
<span class="title">推送记录</span>
|
||||||
|
<el-tag size="mini" type="info" style="margin-left: 10px;">
|
||||||
|
共 {{ batchRecords.length }} 条
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-button type="text" icon="el-icon-refresh" @click="loadBatchRecords">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-timeline v-if="batchRecords.length > 0">
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="record in batchRecords"
|
||||||
|
:key="record.batchId"
|
||||||
|
:timestamp="formatDateTime(record.createTime)"
|
||||||
|
placement="top"
|
||||||
|
:type="getRecordType(record.status)"
|
||||||
|
:icon="getRecordIcon(record.status)"
|
||||||
|
>
|
||||||
|
<el-card class="record-item" shadow="hover">
|
||||||
|
<div class="record-summary" @click="toggleRecordDetail(record.batchId)">
|
||||||
|
<div class="summary-left">
|
||||||
|
<el-tag :type="getStatusTagType(record.status)" size="small">
|
||||||
|
{{ getStatusText(record.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="trigger-source">
|
||||||
|
{{ getTriggerSourceText(record.triggerSource) }}
|
||||||
|
</span>
|
||||||
|
<span class="record-stats">
|
||||||
|
<i class="el-icon-check" style="color: #67c23a;"></i> {{ record.successCount }}
|
||||||
|
<i class="el-icon-remove-outline" style="color: #e6a23c; margin-left: 10px;"></i> {{ record.skipCount }}
|
||||||
|
<i class="el-icon-close" style="color: #f56c6c; margin-left: 10px;"></i> {{ record.errorCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-right">
|
||||||
|
<span class="record-range">行 {{ record.startRow }} - {{ record.endRow }}</span>
|
||||||
|
<span v-if="record.durationMs" class="record-duration">
|
||||||
|
耗时 {{ formatDuration(record.durationMs) }}
|
||||||
|
</span>
|
||||||
|
<i :class="expandedRecords.includes(record.batchId) ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情展开区域 -->
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-if="expandedRecords.includes(record.batchId)" class="record-detail">
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="record.loadingDetail" class="loading-detail" v-loading="true" element-loading-text="正在加载详情...">
|
||||||
|
<div style="height: 100px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="record.resultMessage" class="detail-message">
|
||||||
|
<div class="message-label">结果消息:</div>
|
||||||
|
<div class="message-content">{{ record.resultMessage }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="record.errorMessage" class="detail-error">
|
||||||
|
<div class="error-label">错误信息:</div>
|
||||||
|
<div class="error-content">{{ record.errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作日志列表 -->
|
||||||
|
<div v-if="record.operationLogs && record.operationLogs.length > 0" class="operation-logs">
|
||||||
|
<div class="logs-header">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span>操作日志({{ record.operationLogs.length }} 条)</span>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="record.operationLogs"
|
||||||
|
size="mini"
|
||||||
|
max-height="300"
|
||||||
|
stripe
|
||||||
|
>
|
||||||
|
<el-table-column prop="orderNo" label="订单号" width="150" />
|
||||||
|
<el-table-column prop="operationType" label="操作类型" width="100" />
|
||||||
|
<el-table-column prop="targetRow" label="目标行" width="80" />
|
||||||
|
<el-table-column prop="logisticsLink" label="物流链接" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="operationStatus" label="状态" width="80">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.operationStatus === 'SUCCESS' ? 'success' : 'danger'" size="mini">
|
||||||
|
{{ scope.row.operationStatus }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="errorMessage" label="错误信息" min-width="150" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 暂无操作日志 -->
|
||||||
|
<div v-else class="no-logs">
|
||||||
|
<i class="el-icon-info"></i>
|
||||||
|
<span>暂无操作日志</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
</el-card>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无推送记录"></el-empty>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getPushStatus,
|
||||||
|
getBatchPushRecords,
|
||||||
|
getBatchPushRecordDetail,
|
||||||
|
triggerPushNow,
|
||||||
|
cancelPendingPush
|
||||||
|
} from '@/api/jarvis/tendoc'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TencentDocPushMonitor',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
pushing: false,
|
||||||
|
pushStatus: {
|
||||||
|
isScheduled: false,
|
||||||
|
scheduledTime: null,
|
||||||
|
remainingSeconds: 0,
|
||||||
|
remainingMs: 0,
|
||||||
|
countdownText: '无定时任务',
|
||||||
|
lastSuccessRecord: null
|
||||||
|
},
|
||||||
|
countdownDisplay: {
|
||||||
|
minutes: '00',
|
||||||
|
seconds: '00'
|
||||||
|
},
|
||||||
|
batchRecords: [],
|
||||||
|
expandedRecords: [],
|
||||||
|
countdownTimer: null,
|
||||||
|
refreshTimer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMobile() {
|
||||||
|
if (this.$store?.getters?.device === 'mobile') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
if (this.visible !== val) {
|
||||||
|
this.visible = val
|
||||||
|
if (val) {
|
||||||
|
this.init()
|
||||||
|
} else {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
await this.loadPushStatus()
|
||||||
|
await this.loadBatchRecords()
|
||||||
|
this.startCountdown()
|
||||||
|
this.startAutoRefresh()
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopCountdown()
|
||||||
|
this.stopAutoRefresh()
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadPushStatus() {
|
||||||
|
try {
|
||||||
|
const res = await getPushStatus()
|
||||||
|
console.log('=== 推送状态响应 ===', res)
|
||||||
|
if (res.code === 200) {
|
||||||
|
console.log('推送状态数据:', res.data)
|
||||||
|
console.log('isScheduled:', res.data.isScheduled)
|
||||||
|
console.log('remainingSeconds:', res.data.remainingSeconds)
|
||||||
|
console.log('scheduledTime:', res.data.scheduledTime)
|
||||||
|
|
||||||
|
// 重要:使用解构赋值,确保 remainingSeconds 被正确赋值
|
||||||
|
this.pushStatus = {
|
||||||
|
...res.data,
|
||||||
|
remainingSeconds: parseInt(res.data.remainingSeconds) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCountdownDisplay()
|
||||||
|
console.log('倒计时显示:', this.countdownDisplay)
|
||||||
|
console.log('pushStatus.remainingSeconds 已更新为:', this.pushStatus.remainingSeconds)
|
||||||
|
} else {
|
||||||
|
console.error('API返回错误:', res)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载推送状态失败', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadBatchRecords() {
|
||||||
|
try {
|
||||||
|
const res = await getBatchPushRecords({ limit: 20 })
|
||||||
|
console.log('加载推送记录响应:', res)
|
||||||
|
if (res.code === 200) {
|
||||||
|
const records = res.data || []
|
||||||
|
// 确保每条记录都有 operationLogs 字段(即使为空数组)
|
||||||
|
records.forEach(record => {
|
||||||
|
if (!record.hasOwnProperty('operationLogs')) {
|
||||||
|
this.$set(record, 'operationLogs', [])
|
||||||
|
}
|
||||||
|
// 重置详情加载标记,允许重新加载
|
||||||
|
this.$set(record, 'detailLoaded', false)
|
||||||
|
})
|
||||||
|
this.batchRecords = records
|
||||||
|
console.log('推送记录数量:', records.length)
|
||||||
|
records.forEach(r => {
|
||||||
|
console.log(`记录 ${r.batchId}: operationLogs数量=${r.operationLogs ? r.operationLogs.length : 'undefined'}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载推送记录失败', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleTriggerPushNow() {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要立即执行推送吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pushing = true
|
||||||
|
const res = await triggerPushNow()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('推送已触发')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadPushStatus()
|
||||||
|
this.loadBatchRecords()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '触发推送失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
this.$message.error('触发推送失败: ' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.pushing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleCancelPush() {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要取消待推送任务吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await cancelPendingPush()
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('已取消待推送任务')
|
||||||
|
this.loadPushStatus()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '取消失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
this.$message.error('取消失败: ' + (e.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleRecordDetail(batchId) {
|
||||||
|
const index = this.expandedRecords.indexOf(batchId)
|
||||||
|
if (index > -1) {
|
||||||
|
// 收起
|
||||||
|
this.expandedRecords.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
// 展开 - 加载详情
|
||||||
|
this.expandedRecords.push(batchId)
|
||||||
|
await this.loadRecordDetail(batchId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadRecordDetail(batchId) {
|
||||||
|
try {
|
||||||
|
const record = this.batchRecords.find(r => r.batchId === batchId)
|
||||||
|
if (!record) return
|
||||||
|
|
||||||
|
// 如果已经明确加载过详情(有 loadingDetail 标记且已完成),则不再重复加载
|
||||||
|
// 注意:即使 operationLogs 为空数组,也可能是数据确实为空,需要重新加载确认
|
||||||
|
if (record.detailLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
this.$set(record, 'loadingDetail', true)
|
||||||
|
|
||||||
|
const res = await getBatchPushRecordDetail(batchId)
|
||||||
|
console.log('加载推送详情响应:', res)
|
||||||
|
console.log('响应数据:', JSON.stringify(res.data, null, 2))
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
// 更新记录的详细信息
|
||||||
|
const operationLogs = res.data.operationLogs || []
|
||||||
|
console.log('操作日志数量:', operationLogs.length, 'batchId:', batchId)
|
||||||
|
console.log('操作日志详情:', operationLogs)
|
||||||
|
this.$set(record, 'operationLogs', operationLogs)
|
||||||
|
this.$set(record, 'errorMessage', res.data.errorMessage)
|
||||||
|
// 使用 resultMessage 字段,如果没有则使用 remark
|
||||||
|
this.$set(record, 'resultMessage', res.data.resultMessage || res.data.remark)
|
||||||
|
// 标记已加载详情
|
||||||
|
this.$set(record, 'detailLoaded', true)
|
||||||
|
|
||||||
|
// 如果操作日志为空,输出警告信息用于调试
|
||||||
|
if (operationLogs.length === 0) {
|
||||||
|
console.warn('操作日志为空 - batchId:', batchId, '记录数据:', res.data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.warning('加载详情失败: ' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载推送详情失败', e)
|
||||||
|
this.$message.error('加载详情失败: ' + (e.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
const record = this.batchRecords.find(r => r.batchId === batchId)
|
||||||
|
if (record) {
|
||||||
|
this.$set(record, 'loadingDetail', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startCountdown() {
|
||||||
|
this.stopCountdown()
|
||||||
|
|
||||||
|
// 立即更新一次显示
|
||||||
|
this.updateCountdownDisplay()
|
||||||
|
|
||||||
|
this.countdownTimer = setInterval(() => {
|
||||||
|
if (this.pushStatus.remainingSeconds > 0) {
|
||||||
|
this.pushStatus.remainingSeconds--
|
||||||
|
this.pushStatus.remainingMs = this.pushStatus.remainingSeconds * 1000
|
||||||
|
this.updateCountdownDisplay()
|
||||||
|
} else if (this.pushStatus.isScheduled) {
|
||||||
|
// 倒计时结束,刷新状态
|
||||||
|
this.loadPushStatus()
|
||||||
|
this.loadBatchRecords()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
stopCountdown() {
|
||||||
|
if (this.countdownTimer) {
|
||||||
|
clearInterval(this.countdownTimer)
|
||||||
|
this.countdownTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startAutoRefresh() {
|
||||||
|
this.stopAutoRefresh()
|
||||||
|
// 每30秒自动刷新一次状态
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
this.loadPushStatus()
|
||||||
|
this.loadBatchRecords()
|
||||||
|
}, 30000)
|
||||||
|
},
|
||||||
|
|
||||||
|
stopAutoRefresh() {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer)
|
||||||
|
this.refreshTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCountdownDisplay() {
|
||||||
|
const seconds = this.pushStatus.remainingSeconds || 0
|
||||||
|
console.log('更新倒计时显示 - remainingSeconds:', seconds)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
this.countdownDisplay.minutes = String(minutes).padStart(2, '0')
|
||||||
|
this.countdownDisplay.seconds = String(secs).padStart(2, '0')
|
||||||
|
console.log('倒计时显示更新为:', this.countdownDisplay.minutes + ':' + this.countdownDisplay.seconds)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDateTime(dateTime) {
|
||||||
|
if (!dateTime) return '-'
|
||||||
|
try {
|
||||||
|
// 处理多种时间格式
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
if (isNaN(date.getTime())) return dateTime
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch (e) {
|
||||||
|
console.error('格式化时间失败:', e, dateTime)
|
||||||
|
return dateTime
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration(ms) {
|
||||||
|
if (!ms) return '-'
|
||||||
|
const seconds = Math.floor(ms / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
return minutes > 0 ? `${minutes}分${secs}秒` : `${secs}秒`
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusText(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'RUNNING': '执行中',
|
||||||
|
'SUCCESS': '成功',
|
||||||
|
'PARTIAL': '部分成功',
|
||||||
|
'FAILED': '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusTagType(status) {
|
||||||
|
const typeMap = {
|
||||||
|
'RUNNING': 'warning',
|
||||||
|
'SUCCESS': 'success',
|
||||||
|
'PARTIAL': 'warning',
|
||||||
|
'FAILED': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecordType(status) {
|
||||||
|
const typeMap = {
|
||||||
|
'SUCCESS': 'success',
|
||||||
|
'PARTIAL': 'warning',
|
||||||
|
'FAILED': 'danger',
|
||||||
|
'RUNNING': 'primary'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecordIcon(status) {
|
||||||
|
const iconMap = {
|
||||||
|
'SUCCESS': 'el-icon-success',
|
||||||
|
'PARTIAL': 'el-icon-warning',
|
||||||
|
'FAILED': 'el-icon-error',
|
||||||
|
'RUNNING': 'el-icon-loading'
|
||||||
|
}
|
||||||
|
return iconMap[status] || 'el-icon-info'
|
||||||
|
},
|
||||||
|
|
||||||
|
getTriggerSourceText(source) {
|
||||||
|
const sourceMap = {
|
||||||
|
'DELAYED_TIMER': '延迟定时器',
|
||||||
|
'USER': '用户手动',
|
||||||
|
'SYSTEM': '系统'
|
||||||
|
}
|
||||||
|
return sourceMap[source] || source
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.visible = false
|
||||||
|
this.expandedRecords = []
|
||||||
|
this.$emit('input', false)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleVisibleChange(val) {
|
||||||
|
this.visible = val
|
||||||
|
this.$emit('input', val)
|
||||||
|
if (!val) {
|
||||||
|
this.expandedRecords = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.push-monitor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 倒计时卡片 */
|
||||||
|
.countdown-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-card >>> .el-card__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header .header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header .header-left i {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-content {
|
||||||
|
padding: 30px 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-display.active {
|
||||||
|
opacity: 1;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-separator {
|
||||||
|
font-size: 36px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 推送记录卡片 */
|
||||||
|
.records-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header .header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header .header-left i {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-left,
|
||||||
|
.summary-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-source {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-range,
|
||||||
|
.record-duration {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-detail {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message,
|
||||||
|
.detail-error {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-label,
|
||||||
|
.error-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content {
|
||||||
|
padding: 10px;
|
||||||
|
background: #fef0f0;
|
||||||
|
border-left: 3px solid #f56c6c;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-logs {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header i {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-detail {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-logs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-logs i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.push-monitor {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header .header-left {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-header .header-left i {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-content {
|
||||||
|
padding: 20px 12px 12px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-display {
|
||||||
|
font-size: 32px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-box {
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-separator {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-info {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header .header-left {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-left,
|
||||||
|
.summary-right {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-source,
|
||||||
|
.record-range,
|
||||||
|
.record-duration {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端全屏弹窗样式 */
|
||||||
|
::v-deep .mobile-push-monitor-dialog {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .mobile-push-monitor-dialog .el-dialog {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .mobile-push-monitor-dialog .el-dialog__body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .mobile-push-monitor-dialog .el-dialog__header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .mobile-push-monitor-dialog .el-dialog__title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -5,24 +5,45 @@
|
|||||||
<span>一键转链</span>
|
<span>一键转链</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form :model="form" label-width="120px">
|
<el-row :gutter="20">
|
||||||
<el-form-item label="输入内容">
|
<el-col :span="12">
|
||||||
<el-input
|
<el-form :model="form" label-width="120px" label-position="top">
|
||||||
v-model="form.inputContent"
|
<el-form-item label="输入内容">
|
||||||
type="textarea"
|
<el-input
|
||||||
:rows="6"
|
v-model="form.inputContent"
|
||||||
placeholder="请输入需要转链的内容,如商品链接、商品名称等"
|
type="textarea"
|
||||||
style="width: 100%"
|
:rows="10"
|
||||||
/>
|
placeholder="请输入需要转链的内容,如商品链接、商品名称等"
|
||||||
</el-form-item>
|
style="width: 100%"
|
||||||
|
/>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="handleGenerate" :loading="loading">
|
|
||||||
生成转链内容
|
<el-form-item>
|
||||||
</el-button>
|
<el-button type="primary" @click="handleGenerate" :loading="loading">
|
||||||
<el-button @click="handleClear">清空</el-button>
|
生成转链内容
|
||||||
</el-form-item>
|
</el-button>
|
||||||
</el-form>
|
<el-button @click="handleClear">清空</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="通用文案">
|
||||||
|
<el-input
|
||||||
|
:value="generalCopy"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
readonly
|
||||||
|
placeholder="暂无通用文案"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<el-button type="success" @click="handleCopyText(generalCopy)" :disabled="!generalCopy">复制通用文案</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<div v-if="result" style="margin-top: 20px;">
|
<div v-if="result" style="margin-top: 20px;">
|
||||||
<h4>转链结果:</h4>
|
<h4>转链结果:</h4>
|
||||||
@@ -69,6 +90,7 @@
|
|||||||
/>
|
/>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<el-button type="success" @click="handleCopyText(wenan.content || '')">复制此版本</el-button>
|
<el-button type="success" @click="handleCopyText(wenan.content || '')">复制此版本</el-button>
|
||||||
|
<el-button type="primary" style="margin-left: 8px;" @click="openPublish(product, productIndex)">发品</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -95,6 +117,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 发品操作 -->
|
||||||
|
<div style="margin-top: 10px; display:flex; gap:8px;">
|
||||||
|
<el-button type="primary" @click="openPublish(product, productIndex)">发品</el-button>
|
||||||
|
<el-button @click="handleAddToFavorites(product)">加入常用</el-button>
|
||||||
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,14 +143,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 公共发品对话框组件 -->
|
||||||
|
<PublishDialog :visible.sync="publishDialogVisible" :initial-data="publishInitialData" @success="handlePublishSuccess" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { generatePromotionContent } from "@/api/system/jdorder";
|
import { generatePromotionContent } from "@/api/system/jdorder";
|
||||||
|
import { addToFavorites, getBySkuid } from "@/api/system/favoriteProduct";
|
||||||
|
import { addToFavoritesAfterPublishFromTransfer } from "@/utils/publishHelper";
|
||||||
|
import PublishDialog from '@/components/PublishDialog.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Jdorder",
|
name: "Jdorder",
|
||||||
|
components: { PublishDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -134,10 +169,134 @@ export default {
|
|||||||
// 当前选中的商品
|
// 当前选中的商品
|
||||||
activeProductTab: 0,
|
activeProductTab: 0,
|
||||||
// 每个商品的文案标签页状态
|
// 每个商品的文案标签页状态
|
||||||
activeWenanTab: {}
|
activeWenanTab: {},
|
||||||
|
publishDialogVisible: false,
|
||||||
|
publishInitialData: {},
|
||||||
|
// 记录正在发品的商品,用于发品成功后自动加入常用
|
||||||
|
currentPublishProduct: null,
|
||||||
|
regionOptions: {
|
||||||
|
provinces: [],
|
||||||
|
cities: [],
|
||||||
|
areas: []
|
||||||
|
},
|
||||||
|
categoryOptions: [],
|
||||||
|
categoryLoading: false,
|
||||||
|
userNameOptions: [],
|
||||||
|
userNameLoading: false,
|
||||||
|
erpAccountsOptions: [],
|
||||||
|
erpAccountLoading: false,
|
||||||
|
pvOptions: [],
|
||||||
|
selectedPv: {},
|
||||||
|
itemBizTypeOptions: [
|
||||||
|
{ label: '普通商品', value: 2 },
|
||||||
|
{ label: '已验货', value: 0 },
|
||||||
|
{ label: '验货宝', value: 10 },
|
||||||
|
{ label: '闲鱼优品', value: 19 },
|
||||||
|
{ label: '闲鱼特卖', value: 24 },
|
||||||
|
{ label: '品牌捡漏', value: 26 }
|
||||||
|
],
|
||||||
|
spBizTypeOptions: [
|
||||||
|
{ label: '手机', value: 1 },
|
||||||
|
{ label: '时尚', value: 2 },
|
||||||
|
{ label: '家电', value: 3 },
|
||||||
|
{ label: '乐器', value: 8 },
|
||||||
|
{ label: '数码3C', value: 9 },
|
||||||
|
{ label: '奢品', value: 16 },
|
||||||
|
{ label: '母婴', value: 17 },
|
||||||
|
{ label: '美妆', value: 18 },
|
||||||
|
{ label: '珠宝', value: 19 },
|
||||||
|
{ label: '游戏', value: 20 },
|
||||||
|
{ label: '家居', value: 21 },
|
||||||
|
{ label: '虚拟', value: 22 },
|
||||||
|
{ label: '图书', value: 24 },
|
||||||
|
{ label: '食品', value: 27 },
|
||||||
|
{ label: '玩具', value: 28 },
|
||||||
|
{ label: '其他', value: 99 }
|
||||||
|
],
|
||||||
|
stuffStatusOptions: [
|
||||||
|
{ label: '不传', value: null },
|
||||||
|
{ label: '全新', value: 100 },
|
||||||
|
{ label: '99新', value: 99 },
|
||||||
|
{ label: '95新', value: 95 },
|
||||||
|
{ label: '9成新', value: 90 },
|
||||||
|
{ label: '8成新', value: 80 },
|
||||||
|
{ label: '7成新', value: 70 },
|
||||||
|
{ label: '6成新', value: 60 },
|
||||||
|
{ label: '5成新', value: 50 }
|
||||||
|
],
|
||||||
|
serviceSupportOptions: [
|
||||||
|
{ label: '七天无理由退货', value: 'SDR' },
|
||||||
|
{ label: '描述不符包邮退', value: 'NFR' },
|
||||||
|
{ label: '描述不符全额退(虚拟)', value: 'VNR' },
|
||||||
|
{ label: '10分钟极速发货(虚拟)', value: 'FD_10MS' },
|
||||||
|
{ label: '24小时极速发货', value: 'FD_24HS' },
|
||||||
|
{ label: '48小时极速发货', value: 'FD_48HS' },
|
||||||
|
{ label: '正品保障', value: 'FD_GPA' }
|
||||||
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {},
|
||||||
|
computed: {
|
||||||
|
// 提取通用文案:优先取第一个商品中类型包含“通用”的文案;否则取第一个文案
|
||||||
|
generalCopy() {
|
||||||
|
const data = this.parsedResult;
|
||||||
|
try {
|
||||||
|
if (Array.isArray(data) && data.length) {
|
||||||
|
const productIndex = typeof this.activeProductTab === 'number' ? this.activeProductTab : 0;
|
||||||
|
const product = data[productIndex] || data[0];
|
||||||
|
const wenanList = Array.isArray(product && product.wenan) ? product.wenan : [];
|
||||||
|
if (wenanList.length) {
|
||||||
|
const found = wenanList.find(w => (w && (w.type || '').includes('通用')));
|
||||||
|
const chosen = found || wenanList[0];
|
||||||
|
return (chosen && chosen.content) ? chosen.content : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data && data.generalCopy) {
|
||||||
|
return String(data.generalCopy || '');
|
||||||
|
}
|
||||||
|
} catch (e) { /* 忽略提取异常 */ }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'publishDialog.form.itemBizType'(val) {
|
||||||
|
// 冗余联动,防止@change未触发的情况
|
||||||
|
this.onItemBizTypeChange();
|
||||||
|
},
|
||||||
|
'publishDialog.form.spBizType'(val) {
|
||||||
|
this.onSpBizTypeChange();
|
||||||
|
},
|
||||||
|
'publishDialog.form.appid'(val) {
|
||||||
|
this.onAppidChange();
|
||||||
|
},
|
||||||
|
'publishDialog.form.channelCatId'(val) {
|
||||||
|
// 兜底:类目变更(包括编程方式设置)时,自动拉取属性
|
||||||
|
this.loadProperties();
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async handleAddToFavorites(product) {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
skuid: product.skuid || product.skuId || product.spuid || '',
|
||||||
|
productName: product.skuName || product.title || '',
|
||||||
|
shopName: product.shopName || '',
|
||||||
|
productUrl: product.materialUrl || product.url || '',
|
||||||
|
productImage: Array.isArray(product.images) && product.images.length ? product.images[0] : '',
|
||||||
|
price: product.price != null ? String(product.price) : (product.lowestCouponPrice != null ? String(product.lowestCouponPrice) : ''),
|
||||||
|
commissionInfo: product.commission != null ? String(product.commission) : '',
|
||||||
|
remark: '来自一键转链'
|
||||||
|
}
|
||||||
|
const res = await addToFavorites(payload)
|
||||||
|
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||||
|
this.$modal.msgSuccess('已加入常用');
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(res && res.msg ? res.msg : '加入常用失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$modal.msgError('加入常用失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
handleGenerate() {
|
handleGenerate() {
|
||||||
if (!this.form.inputContent.trim()) {
|
if (!this.form.inputContent.trim()) {
|
||||||
this.$modal.msgError("请输入需要转链的内容");
|
this.$modal.msgError("请输入需要转链的内容");
|
||||||
@@ -260,6 +419,269 @@ export default {
|
|||||||
|
|
||||||
handlePreviewImage(imageUrl) {
|
handlePreviewImage(imageUrl) {
|
||||||
window.open(imageUrl, '_blank');
|
window.open(imageUrl, '_blank');
|
||||||
|
},
|
||||||
|
|
||||||
|
openPublish(product, productIndex) {
|
||||||
|
const wenanIndex = this.activeWenanTab[productIndex] || 0;
|
||||||
|
const wenanOptions = Array.isArray(product.wenan) ? product.wenan.map((w, i) => ({ label: w.type || `版本${i+1}`, content: w.content || '' })) : [];
|
||||||
|
// 记录当前发品的商品
|
||||||
|
this.currentPublishProduct = product;
|
||||||
|
this.publishInitialData = {
|
||||||
|
title: product.skuName || '',
|
||||||
|
content: (product && product.wenan && product.wenan[wenanIndex] ? (product.wenan[wenanIndex].content || '') : ''),
|
||||||
|
images: Array.isArray(product.images) ? product.images : [],
|
||||||
|
originalPrice: this.guessYuanPrice(product),
|
||||||
|
wenanOptions
|
||||||
|
};
|
||||||
|
this.publishDialogVisible = true;
|
||||||
|
},
|
||||||
|
async handlePublishSuccess(res) {
|
||||||
|
try {
|
||||||
|
const p = this.currentPublishProduct || {};
|
||||||
|
await addToFavoritesAfterPublishFromTransfer(p, res);
|
||||||
|
this.$store && this.$store.dispatch && this.$store.dispatch('app/triggerFavoriteProductRefresh');
|
||||||
|
} catch (e) { }
|
||||||
|
},
|
||||||
|
|
||||||
|
onWenanChange(val) {
|
||||||
|
if (this.publishDialog.wenanOptions && this.publishDialog.wenanOptions[val]) {
|
||||||
|
this.publishDialog.form.content = this.publishDialog.wenanOptions[val].content || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAllImages(flag) {
|
||||||
|
(this.publishDialog.productImages || []).forEach(it => { it.selected = !!flag; });
|
||||||
|
},
|
||||||
|
invertSelection() {
|
||||||
|
(this.publishDialog.productImages || []).forEach(it => { it.selected = !it.selected; });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从解析到的商品信息中推测原价(元)
|
||||||
|
guessYuanPrice(product) {
|
||||||
|
// 常见字段尝试:price、oriPrice、originPrice、jdPrice、marketPrice 等(单位可能为元)
|
||||||
|
const candidates = [
|
||||||
|
product && product.price,
|
||||||
|
product && product.oriPrice,
|
||||||
|
product && product.originPrice,
|
||||||
|
product && product.jdPrice,
|
||||||
|
product && product.marketPrice,
|
||||||
|
product && product.opPrice,
|
||||||
|
].filter(v => v != null);
|
||||||
|
for (const v of candidates) {
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isNaN(n) && n > 0) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadProvinces(echo = true) {
|
||||||
|
try {
|
||||||
|
const res = await getProvinces();
|
||||||
|
if (res.code === 200) this.regionOptions.provinces = res.data || []; else this.$modal.msgError(res.msg || '加载省份失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载省份失败'); }
|
||||||
|
if (echo && this.publishDialog.form.province) {
|
||||||
|
await this.loadCities(this.publishDialog.form.province, true);
|
||||||
|
} else {
|
||||||
|
this.regionOptions.cities = []; this.regionOptions.areas = [];
|
||||||
|
this.publishDialog.form.city = null; this.publishDialog.form.district = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onProvinceChange() {
|
||||||
|
const provId = this.publishDialog.form.province;
|
||||||
|
await this.loadCities(provId, false);
|
||||||
|
},
|
||||||
|
async onCityChange() {
|
||||||
|
const provId = this.publishDialog.form.province; const cityId = this.publishDialog.form.city;
|
||||||
|
await this.loadAreas(provId, cityId, false);
|
||||||
|
},
|
||||||
|
async loadCities(provId, echo = false) {
|
||||||
|
if (!provId) {
|
||||||
|
this.regionOptions.cities = []; this.regionOptions.areas = [];
|
||||||
|
this.publishDialog.form.city = null; this.publishDialog.form.district = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getCities(provId);
|
||||||
|
if (res.code === 200) this.regionOptions.cities = res.data || []; else this.$modal.msgError(res.msg || '加载城市失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载城市失败'); }
|
||||||
|
if (echo && this.publishDialog.form.city) {
|
||||||
|
await this.loadAreas(provId, this.publishDialog.form.city, true);
|
||||||
|
} else {
|
||||||
|
this.regionOptions.areas = []; this.publishDialog.form.district = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAreas(provId, cityId, echo = false) {
|
||||||
|
if (!provId || !cityId) {
|
||||||
|
this.regionOptions.areas = []; this.publishDialog.form.district = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getAreas(provId, cityId);
|
||||||
|
if (res.code === 200) this.regionOptions.areas = res.data || []; else this.$modal.msgError(res.msg || '加载区县失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载区县失败'); }
|
||||||
|
if (!echo) {
|
||||||
|
this.publishDialog.form.district = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onItemBizTypeChange() {
|
||||||
|
this.categoryOptions = [];
|
||||||
|
this.publishDialog.form.channelCatId = '';
|
||||||
|
await this.loadCategories();
|
||||||
|
},
|
||||||
|
async onSpBizTypeChange() {
|
||||||
|
this.categoryOptions = [];
|
||||||
|
this.publishDialog.form.channelCatId = '';
|
||||||
|
await this.loadCategories();
|
||||||
|
},
|
||||||
|
async loadCategories() {
|
||||||
|
const itemBizType = this.publishDialog.form.itemBizType;
|
||||||
|
const spBizType = this.publishDialog.form.spBizType;
|
||||||
|
const appid = this.publishDialog.form.appid;
|
||||||
|
if (!itemBizType) return;
|
||||||
|
this.categoryLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getCategories({ itemBizType, spBizType, appid });
|
||||||
|
if (res.code === 200) this.categoryOptions = res.data || []; else this.$modal.msgError(res.msg || '加载类目失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载类目失败'); }
|
||||||
|
this.categoryLoading = false;
|
||||||
|
// 若已有选中的类目,或列表首项存在,则尝试自动拉取属性
|
||||||
|
if (this.publishDialog.form.channelCatId) {
|
||||||
|
this.loadProperties();
|
||||||
|
} else if (this.categoryOptions.length) {
|
||||||
|
this.publishDialog.form.channelCatId = this.categoryOptions[0].value;
|
||||||
|
this.loadProperties();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadUsernames() {
|
||||||
|
this.userNameLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getUsernames({ pageNum: 1, pageSize: 200, appid: this.publishDialog.form.appid });
|
||||||
|
if (res.code === 200) this.userNameOptions = res.data || []; else this.$modal.msgError(res.msg || '加载会员名失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载会员名失败'); }
|
||||||
|
this.userNameLoading = false;
|
||||||
|
if (!this.publishDialog.form.userName && this.userNameOptions.length) {
|
||||||
|
// 如未选择默认填第一个
|
||||||
|
this.publishDialog.form.userName = this.userNameOptions[0].value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadERPAccounts() {
|
||||||
|
this.erpAccountLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getERPAccounts();
|
||||||
|
if (res.code === 200) this.erpAccountsOptions = res.data || []; else this.$modal.msgError(res.msg || '加载应用失败');
|
||||||
|
} catch (e) { this.$modal.msgError('加载应用失败'); }
|
||||||
|
this.erpAccountLoading = false;
|
||||||
|
if (!this.publishDialog.form.appid && this.erpAccountsOptions.length) {
|
||||||
|
this.publishDialog.form.appid = this.erpAccountsOptions[0].value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAppidChange() {
|
||||||
|
// 切换账号后,重新拉取与账号相关的下拉
|
||||||
|
this.publishDialog.form.userName = '';
|
||||||
|
this.loadUsernames();
|
||||||
|
this.loadCategories();
|
||||||
|
this.loadProperties();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadProperties() {
|
||||||
|
const f = this.publishDialog.form;
|
||||||
|
if (!f.itemBizType || !f.spBizType || !f.channelCatId) {
|
||||||
|
this.pvOptions = []; this.selectedPv = {}; return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getProperties({ itemBizType: f.itemBizType, spBizType: f.spBizType, channelCatId: f.channelCatId, appid: f.appid });
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.pvOptions = res.data || [];
|
||||||
|
const keep = { ...this.selectedPv };
|
||||||
|
this.selectedPv = {};
|
||||||
|
(this.pvOptions || []).forEach(p => { if (keep[p.propertyId]) this.selectedPv[p.propertyId] = keep[p.propertyId]; });
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(res.msg || '加载属性失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$modal.msgError('加载属性失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitPublish() {
|
||||||
|
this.$refs.publishForm.validate(valid => {
|
||||||
|
if (!valid) return;
|
||||||
|
const f = this.publishDialog.form;
|
||||||
|
const selectedImages = (this.publishDialog.productImages || [])
|
||||||
|
.filter(it => it.selected)
|
||||||
|
.map(it => it.url)
|
||||||
|
.filter(Boolean);
|
||||||
|
const extraImages = String(f.extraImagesText || '')
|
||||||
|
.split(/\n+/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const images = [...selectedImages, ...extraImages];
|
||||||
|
if (!images.length) {
|
||||||
|
this.$modal.msgError('请至少选择或填写一张图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let channelPv = undefined;
|
||||||
|
if (f.channelPvJson && f.channelPvJson.trim()) {
|
||||||
|
try { channelPv = JSON.parse(f.channelPvJson); } catch (e) { this.$modal.msgError('属性JSON格式不正确'); return; }
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
appid: f.appid || undefined,
|
||||||
|
title: f.title,
|
||||||
|
content: f.content,
|
||||||
|
images: images,
|
||||||
|
whiteImages: f.whiteImages || undefined,
|
||||||
|
userName: f.userName,
|
||||||
|
province: f.province,
|
||||||
|
city: f.city,
|
||||||
|
district: f.district,
|
||||||
|
serviceSupport: (f.serviceSupport && f.serviceSupport.length) ? f.serviceSupport.join(',') : undefined,
|
||||||
|
price: cents(f.price),
|
||||||
|
originalPrice: f.originalPrice != null ? cents(f.originalPrice) : undefined,
|
||||||
|
expressFee: cents(f.expressFee),
|
||||||
|
stock: f.stock,
|
||||||
|
outerId: f.outerId || undefined,
|
||||||
|
itemBizType: f.itemBizType,
|
||||||
|
spBizType: f.spBizType,
|
||||||
|
channelCatId: f.channelCatId,
|
||||||
|
stuffStatus: f.stuffStatus || undefined,
|
||||||
|
channelPv: channelPv
|
||||||
|
};
|
||||||
|
function cents(yuan) {
|
||||||
|
const n = Number(yuan);
|
||||||
|
if (Number.isNaN(n)) return undefined;
|
||||||
|
return Math.round(n * 100);
|
||||||
|
}
|
||||||
|
this.publishDialog.loading = true;
|
||||||
|
createProductByPromotion(payload).then(res => {
|
||||||
|
this.publishDialog.loading = false;
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 成功反馈:包含生成的outerId
|
||||||
|
try {
|
||||||
|
const outerId = res.data && (res.data.outerId || (res.data.data && res.data.data.outerId))
|
||||||
|
if (outerId) {
|
||||||
|
this.$modal.msgSuccess(`发品成功,商家编码:${outerId}`)
|
||||||
|
} else {
|
||||||
|
this.$modal.msgSuccess('发品提交成功')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$modal.msgSuccess('发品提交成功')
|
||||||
|
}
|
||||||
|
this.publishDialog.visible = false;
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(res.msg || '发品失败');
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
this.publishDialog.loading = false;
|
||||||
|
console.error('发品失败', err);
|
||||||
|
this.$modal.msgError('发品失败,请稍后重试');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -278,4 +700,28 @@ export default {
|
|||||||
.clearfix:after {
|
.clearfix:after {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.img-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.img-item {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.img-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.img-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
3079
src/views/system/jdorder/orderList.vue
Normal file
3079
src/views/system/jdorder/orderList.vue
Normal file
File diff suppressed because it is too large
Load Diff
1067
src/views/system/marketing-image/index.vue
Normal file
1067
src/views/system/marketing-image/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,109 +1,213 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
<!-- 顶部搜索区域 -->
|
||||||
<el-form-item label="京粉账号" prop="unionId">
|
<div class="search-section">
|
||||||
<el-select v-model="queryParams.unionId" placeholder="请选择京粉账号" clearable style="width: 240px">
|
<mobile-search-form
|
||||||
<el-option
|
:model="queryParams"
|
||||||
v-for="admin in adminList"
|
@search="handleQuery"
|
||||||
:key="admin.value || admin.id"
|
@reset="resetQuery"
|
||||||
:label="admin.label || (admin.name + ' (' + admin.wxid + ')')"
|
>
|
||||||
:value="admin.value || admin.id"
|
<template #form="{ expanded }">
|
||||||
/>
|
<el-form
|
||||||
</el-select>
|
:model="queryParams"
|
||||||
</el-form-item>
|
ref="queryForm"
|
||||||
<el-form-item label="订单号" prop="orderId">
|
size="small"
|
||||||
<el-input v-model="queryParams.orderId" placeholder="请输入订单号" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
:inline="true"
|
||||||
</el-form-item>
|
v-show="showSearch"
|
||||||
<el-form-item label="商品名称" prop="skuName">
|
label-width="68px"
|
||||||
<el-input v-model="queryParams.skuName" placeholder="请输入商品名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
>
|
||||||
</el-form-item>
|
<el-form-item label="京粉账号" prop="unionId">
|
||||||
<el-form-item label="订单状态" prop="statusGroup">
|
<el-select v-model="queryParams.unionId" placeholder="请选择京粉账号" clearable style="width: 240px">
|
||||||
<el-select v-model="queryParams.statusGroup" placeholder="订单状态" clearable style="width: 240px">
|
<el-option
|
||||||
<el-option v-for="status in mergedStatusList" :key="status.value" :label="status.label" :value="status.value" />
|
v-for="admin in adminList"
|
||||||
</el-select>
|
:key="admin.value || admin.id"
|
||||||
</el-form-item>
|
:label="admin.label || (admin.name + ' (' + admin.wxid + ')')"
|
||||||
<el-form-item label="订单时间">
|
:value="admin.value || admin.id"
|
||||||
<el-date-picker v-model="dateRange" style="width: 240px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-form-item label="订单号" prop="orderId">
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-input v-model="queryParams.orderId" placeholder="请输入订单号" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<el-form-item label="商品名称" prop="skuName">
|
||||||
|
<el-input v-model="queryParams.skuName" placeholder="请输入商品名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单状态" prop="statusGroup">
|
||||||
|
<el-select v-model="queryParams.statusGroup" placeholder="订单状态" clearable style="width: 240px">
|
||||||
|
<el-option v-for="status in mergedStatusList" :key="status.value" :label="status.label" :value="status.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单时间">
|
||||||
|
<el-date-picker v-model="dateRange" style="width: 240px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 桌面端搜索按钮 -->
|
||||||
|
<el-form-item v-if="!expanded">
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</mobile-search-form>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
<!-- 统计悬浮模块 -->
|
||||||
<el-col :span="1.5">
|
<el-card class="statistics-card" shadow="hover" v-if="orderrowsList.length > 0">
|
||||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:orderrows:add']">新增</el-button>
|
<div slot="header" class="clearfix">
|
||||||
</el-col>
|
<span><i class="el-icon-data-analysis"></i> 佣金统计</span>
|
||||||
<el-col :span="1.5">
|
<el-button style="float: right; padding: 3px 0" type="text" @click="toggleStatistics">
|
||||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:orderrows:edit']">修改</el-button>
|
{{ showStatistics ? '收起' : '展开' }}
|
||||||
</el-col>
|
</el-button>
|
||||||
<el-col :span="1.5">
|
</div>
|
||||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:orderrows:remove']">删除</el-button>
|
<div v-show="showStatistics" class="statistics-content">
|
||||||
</el-col>
|
<el-row :gutter="20">
|
||||||
<el-col :span="1.5">
|
<el-col :span="6">
|
||||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:orderrows:export']">导出</el-button>
|
<div class="stat-item">
|
||||||
</el-col>
|
<div class="stat-label">总订单数</div>
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<div class="stat-value">{{ statistics.totalOrders }}</div>
|
||||||
</el-row>
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">总计佣金额</div>
|
||||||
|
<div class="stat-value">¥{{ statistics.totalCosPrice.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">预估佣金</div>
|
||||||
|
<div class="stat-value">¥{{ statistics.totalEstimateFee.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">实际佣金</div>
|
||||||
|
<div class="stat-value">¥{{ statistics.totalActualFee.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="status-chart-row">
|
||||||
|
<el-col :xs="24" :sm="24" :md="10" :lg="9">
|
||||||
|
<div class="status-pie-wrap">
|
||||||
|
<h4>订单状态分布</h4>
|
||||||
|
<div ref="statusPieRef" class="status-pie-chart"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="14" :lg="15">
|
||||||
|
<div class="status-stats-expanded">
|
||||||
|
<h4>按状态统计</h4>
|
||||||
|
<div class="status-list status-list-expanded">
|
||||||
|
<div
|
||||||
|
v-for="{ key, stat } in orderedStatusStatRows"
|
||||||
|
:key="key"
|
||||||
|
class="status-item status-item-expanded"
|
||||||
|
>
|
||||||
|
<div class="status-item-left">
|
||||||
|
<el-tag :type="getStatusTypeByKey(key)" size="small">{{ stat.label }}</el-tag>
|
||||||
|
<span class="status-count">{{ stat.count }}单</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item-amounts">
|
||||||
|
<span class="status-amt-est" title="与顶部「预估佣金」同口径">预估 ¥{{ stat.estimateAmount.toFixed(2) }}</span>
|
||||||
|
<span class="status-amt-act" title="与顶部「实际佣金」同口径">实际 ¥{{ stat.actualAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="orderrowsList" @selection-change="handleSelectionChange">
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
<el-table-column type="selection" width="50" align="center" />
|
<div class="action-buttons-section mobile-only">
|
||||||
<el-table-column label="账号" align="center" prop="unionId" width="50">
|
<mobile-button-group
|
||||||
<template slot-scope="scope">
|
:buttons="actionButtons"
|
||||||
<span>{{ getAdminName(scope.row.unionId) }}</span>
|
:primary-count="2"
|
||||||
</template>
|
/>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column label="订单号" align="center" prop="orderId" width="120" />
|
|
||||||
<el-table-column label="商品名称" align="center" prop="skuName" :show-overflow-tooltip="true" min-width="200" />
|
|
||||||
<el-table-column label="计佣金额" align="center" prop="estimateCosPrice" width="100">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>¥{{ scope.row.estimateCosPrice }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="商品数量" align="center" prop="skuNum" width="80" />
|
|
||||||
<el-table-column label="订单状态" align="center" prop="validCode" width="240">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-tag :type="getStatusType(scope.row.validCode)">
|
|
||||||
{{ getStatusText(scope.row.validCode) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="佣金比例" align="center" prop="commissionRate" width="100">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.commissionRate }}%</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="预估佣金" align="center" prop="estimateFee" width="100">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>¥{{ scope.row.estimateFee }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="实际佣金" align="center" prop="actualFee" width="100">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>¥{{ scope.row.actualFee }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="订单时间" align="center" prop="orderTime" width="160">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ parseTime(scope.row.orderTime) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="完成时间" align="center" prop="finishTime" width="160">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.finishTime ? parseTime(scope.row.finishTime) : '-' }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
|
|
||||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:orderrows:edit']">修改</el-button>
|
|
||||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:orderrows:remove']">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
<!-- 桌面端按钮组 -->
|
||||||
|
<el-row :gutter="10" class="mb8 desktop-only">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:orderrows:add']">新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:orderrows:edit']">修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:orderrows:remove']">删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:orderrows:export']">导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 - 可滚动 -->
|
||||||
|
<div class="table-section">
|
||||||
|
<el-table v-loading="loading" :data="orderrowsList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column label="账号" align="center" prop="unionId" width="50">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ getAdminName(scope.row.unionId) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="订单号" align="center" prop="orderId" width="120" />
|
||||||
|
<el-table-column label="商品名称" align="center" prop="skuName" :show-overflow-tooltip="true" min-width="200" />
|
||||||
|
<el-table-column label="计佣金额" align="center" prop="estimateCosPrice" width="100" sortable="custom" @sort-change="handleSortChange">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>¥{{ scope.row.estimateCosPrice }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品数量" align="center" prop="skuNum" width="80" />
|
||||||
|
<el-table-column label="订单状态" align="center" prop="validCode" width="240">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.validCode)">
|
||||||
|
{{ getStatusText(scope.row.validCode) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="佣金比例" align="center" prop="commissionRate" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.commissionRate }}%</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="预估佣金" align="center" prop="estimateFee" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>¥{{ scope.row.estimateFee }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="实际佣金" align="center" prop="actualFee" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>¥{{ scope.row.actualFee }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="订单时间" align="center" prop="orderTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.orderTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="完成时间" align="center" prop="finishTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.finishTime ? parseTime(scope.row.finishTime) : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:orderrows:edit']">修改</el-button>
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:orderrows:remove']">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 固定分页区域 -->
|
||||||
|
<div class="pagination-section">
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 查看订单详情对话框 -->
|
<!-- 查看订单详情对话框 -->
|
||||||
<el-dialog :title="'订单详情 - ' + currentOrder.orderId" :visible.sync="viewDialogVisible" width="900px" append-to-body>
|
<el-dialog :title="'订单详情 - ' + currentOrder.orderId" :visible.sync="viewDialogVisible" width="900px" append-to-body>
|
||||||
@@ -219,9 +323,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData } from "@/api/system/orderrows";
|
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData } from "@/api/system/orderrows";
|
||||||
import { getAdminSelectData } from "@/api/system/superadmin";
|
import { getAdminSelectData } from "@/api/system/superadmin";
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { debounce } from '@/utils'
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
|
||||||
|
/** 与后端分组顺序一致,便于阅读 */
|
||||||
|
const STATUS_STAT_ORDER = ['pending', 'paid', 'deposit', 'finished', 'cancel', 'invalid', 'illegal']
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Orderrows",
|
name: "Orderrows",
|
||||||
|
components: {
|
||||||
|
MobileSearchForm,
|
||||||
|
MobileButtonGroup
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
@@ -252,7 +368,9 @@ export default {
|
|||||||
orderId: null,
|
orderId: null,
|
||||||
skuName: null,
|
skuName: null,
|
||||||
validCode: null,
|
validCode: null,
|
||||||
statusGroup: null // 新增
|
statusGroup: null, // 新增
|
||||||
|
orderBy: null, // 排序字段
|
||||||
|
orderSort: null // 排序方向:asc/desc
|
||||||
},
|
},
|
||||||
// 管理员列表
|
// 管理员列表
|
||||||
adminList: [],
|
adminList: [],
|
||||||
@@ -263,14 +381,80 @@ export default {
|
|||||||
// 查看详情对话框
|
// 查看详情对话框
|
||||||
viewDialogVisible: false,
|
viewDialogVisible: false,
|
||||||
// 当前查看的订单
|
// 当前查看的订单
|
||||||
currentOrder: {}
|
currentOrder: {},
|
||||||
|
// 统计相关
|
||||||
|
showStatistics: true,
|
||||||
|
statistics: {
|
||||||
|
totalOrders: 0,
|
||||||
|
totalCosPrice: 0,
|
||||||
|
totalEstimateFee: 0,
|
||||||
|
totalActualFee: 0,
|
||||||
|
statusStats: {}
|
||||||
|
},
|
||||||
|
statusPieInstance: null,
|
||||||
|
_statusPieResizeHandler: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
orderedStatusStatRows() {
|
||||||
|
return STATUS_STAT_ORDER.filter(key => this.statistics.statusStats[key]).map(key => ({
|
||||||
|
key,
|
||||||
|
stat: this.statistics.statusStats[key]
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
isMobile() {
|
||||||
|
if (this.device === 'mobile') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
actionButtons() {
|
||||||
|
return [
|
||||||
|
{ key: 'add', label: '新增', type: 'primary', icon: 'el-icon-plus', handler: () => this.handleAdd(), disabled: false },
|
||||||
|
{ key: 'update', label: '修改', type: 'success', icon: 'el-icon-edit', handler: () => this.handleUpdate(), disabled: this.single },
|
||||||
|
{ key: 'delete', label: '删除', type: 'danger', icon: 'el-icon-delete', handler: () => this.handleDelete(), disabled: this.multiple },
|
||||||
|
{ key: 'export', label: '导出', type: 'warning', icon: 'el-icon-download', handler: () => this.handleExport(), disabled: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList();
|
this.getList();
|
||||||
this.getAdminList();
|
this.getAdminList();
|
||||||
this.getStatusList();
|
this.getStatusList();
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.disposeStatusPieChart();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听日期范围变化,自动调整分页条数
|
||||||
|
dateRange: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal && newVal.length === 2) {
|
||||||
|
// 选择了日期范围,设置分页条数为1000
|
||||||
|
this.queryParams.pageSize = 1000;
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
} else {
|
||||||
|
// 清空日期范围,恢复默认分页条数
|
||||||
|
this.queryParams.pageSize = 10;
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.$nextTick(() => this.renderStatusPieChart());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showStatistics(val) {
|
||||||
|
if (val) this.$nextTick(() => this.renderStatusPieChart());
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 查询京粉订单列表 */
|
/** 查询京粉订单列表 */
|
||||||
getList() {
|
getList() {
|
||||||
@@ -279,12 +463,108 @@ export default {
|
|||||||
this.orderrowsList = response.rows;
|
this.orderrowsList = response.rows;
|
||||||
this.total = response.total;
|
this.total = response.total;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
// 统计已合并到列表接口返回的 statistics,与列表同条件
|
||||||
|
this.applyStatistics(response.statistics);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('获取订单列表失败:', error);
|
console.error('获取订单列表失败:', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.$message.error('获取订单列表失败');
|
this.$message.error('获取订单列表失败');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** 使用列表接口返回的 statistics 填充统计(与列表同一次请求、同条件) */
|
||||||
|
applyStatistics(data) {
|
||||||
|
if (!data) {
|
||||||
|
if (this.orderrowsList.length > 0) this.calculateStatistics();
|
||||||
|
else this.statistics = { totalOrders: 0, totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, statusStats: {} };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupStats = data.groupStats || {};
|
||||||
|
this.statistics = {
|
||||||
|
totalOrders: data.totalOrders || 0,
|
||||||
|
totalCosPrice: data.totalCosPrice != null ? data.totalCosPrice : 0,
|
||||||
|
totalEstimateFee: data.totalCommission || 0,
|
||||||
|
totalActualFee: data.totalActualFee || 0,
|
||||||
|
statusStats: {
|
||||||
|
cancel: this.convertGroupStat(groupStats.cancel),
|
||||||
|
invalid: this.convertGroupStat(groupStats.invalid),
|
||||||
|
pending: this.convertGroupStat(groupStats.pending),
|
||||||
|
paid: this.convertGroupStat(groupStats.paid),
|
||||||
|
finished: this.convertGroupStat(groupStats.finished),
|
||||||
|
deposit: this.convertGroupStat(groupStats.deposit),
|
||||||
|
illegal: this.convertGroupStat(groupStats.illegal)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** 与 OrderRowsController.buildStatistics 一致:commission→预估口径,actualFee→实际口径 */
|
||||||
|
convertGroupStat(groupStat) {
|
||||||
|
if (!groupStat) return { label: '', count: 0, estimateAmount: 0, actualAmount: 0 };
|
||||||
|
const est = groupStat.commission != null ? Number(groupStat.commission) : 0;
|
||||||
|
const act = groupStat.actualFee != null ? Number(groupStat.actualFee) : 0;
|
||||||
|
return {
|
||||||
|
label: groupStat.label || '',
|
||||||
|
count: groupStat.count || 0,
|
||||||
|
estimateAmount: est,
|
||||||
|
actualAmount: act
|
||||||
|
};
|
||||||
|
},
|
||||||
|
disposeStatusPieChart() {
|
||||||
|
if (this._statusPieResizeHandler) {
|
||||||
|
window.removeEventListener('resize', this._statusPieResizeHandler);
|
||||||
|
this._statusPieResizeHandler = null;
|
||||||
|
}
|
||||||
|
if (this.statusPieInstance) {
|
||||||
|
this.statusPieInstance.dispose();
|
||||||
|
this.statusPieInstance = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderStatusPieChart() {
|
||||||
|
if (!this.showStatistics || !this.$refs.statusPieRef) return;
|
||||||
|
const pieData = [];
|
||||||
|
STATUS_STAT_ORDER.forEach(key => {
|
||||||
|
const s = this.statistics.statusStats[key];
|
||||||
|
if (s && s.count > 0) pieData.push({ name: s.label, value: s.count });
|
||||||
|
});
|
||||||
|
const el = this.$refs.statusPieRef;
|
||||||
|
if (!this.statusPieInstance) {
|
||||||
|
this.statusPieInstance = echarts.init(el);
|
||||||
|
this._statusPieResizeHandler = debounce(() => {
|
||||||
|
this.statusPieInstance && this.statusPieInstance.resize();
|
||||||
|
}, 100);
|
||||||
|
window.addEventListener('resize', this._statusPieResizeHandler);
|
||||||
|
}
|
||||||
|
if (!pieData.length) {
|
||||||
|
this.statusPieInstance.setOption({
|
||||||
|
tooltip: { show: false },
|
||||||
|
graphic: [{
|
||||||
|
type: 'text',
|
||||||
|
left: 'center',
|
||||||
|
top: 'middle',
|
||||||
|
style: { text: '暂无分布数据', fill: '#909399', fontSize: 14 }
|
||||||
|
}],
|
||||||
|
series: []
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.statusPieInstance.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{b}<br/>{c} 单 ({d}%)'
|
||||||
|
},
|
||||||
|
graphic: [],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '订单数',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '68%'],
|
||||||
|
center: ['50%', '52%'],
|
||||||
|
data: pieData,
|
||||||
|
label: { formatter: '{b}\n{d}%' },
|
||||||
|
emphasis: { itemStyle: { shadowBlur: 8, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.15)' } }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
color: ['#e6a23c', '#409eff', '#909399', '#67c23a', '#f56c6c', '#c0c4cc', '#f78989']
|
||||||
|
}, true);
|
||||||
|
},
|
||||||
/** 获取管理员列表 */
|
/** 获取管理员列表 */
|
||||||
getAdminList() {
|
getAdminList() {
|
||||||
getAdminSelectData().then(response => {
|
getAdminSelectData().then(response => {
|
||||||
@@ -446,6 +726,7 @@ export default {
|
|||||||
handleQuery() {
|
handleQuery() {
|
||||||
this.queryParams.pageNum = 1;
|
this.queryParams.pageNum = 1;
|
||||||
console.log(this.queryParams.validCode);
|
console.log(this.queryParams.validCode);
|
||||||
|
|
||||||
// 合并项转为原始code数组
|
// 合并项转为原始code数组
|
||||||
if (this.queryParams.statusGroup) {
|
if (this.queryParams.statusGroup) {
|
||||||
this.queryParams.validCodes = this.statusValueMap[this.queryParams.statusGroup].map(code => Number(code));
|
this.queryParams.validCodes = this.statusValueMap[this.queryParams.statusGroup].map(code => Number(code));
|
||||||
@@ -467,7 +748,31 @@ export default {
|
|||||||
this.dateRange = [];
|
this.dateRange = [];
|
||||||
this.resetForm("queryForm");
|
this.resetForm("queryForm");
|
||||||
this.queryParams.validCodes = [];
|
this.queryParams.validCodes = [];
|
||||||
this.handleQuery();
|
// 重置时恢复默认分页条数
|
||||||
|
this.queryParams.pageSize = 10;
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
// 重置排序
|
||||||
|
this.queryParams.orderBy = null;
|
||||||
|
this.queryParams.orderSort = null;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
/** 排序变化处理 */
|
||||||
|
handleSortChange(column) {
|
||||||
|
if (column.prop === 'estimateCosPrice') {
|
||||||
|
if (column.order === 'ascending') {
|
||||||
|
this.queryParams.orderBy = 'estimateCosPrice';
|
||||||
|
this.queryParams.orderSort = 'asc';
|
||||||
|
} else if (column.order === 'descending') {
|
||||||
|
this.queryParams.orderBy = 'estimateCosPrice';
|
||||||
|
this.queryParams.orderSort = 'desc';
|
||||||
|
} else {
|
||||||
|
// 取消排序
|
||||||
|
this.queryParams.orderBy = null;
|
||||||
|
this.queryParams.orderSort = null;
|
||||||
|
}
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 多选框选中数据
|
// 多选框选中数据
|
||||||
handleSelectionChange(selection) {
|
handleSelectionChange(selection) {
|
||||||
@@ -531,7 +836,422 @@ export default {
|
|||||||
this.download('/jarvis/orderrows/export', {
|
this.download('/jarvis/orderrows/export', {
|
||||||
...this.queryParams
|
...this.queryParams
|
||||||
}, `京粉订单数据_${new Date().getTime()}.xlsx`)
|
}, `京粉订单数据_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
/** 切换统计显示 */
|
||||||
|
toggleStatistics() {
|
||||||
|
this.showStatistics = !this.showStatistics;
|
||||||
|
},
|
||||||
|
/** 无后端 statistics 时按当前页推算(与 OrderRowsController.buildStatistics 口径一致) */
|
||||||
|
calculateStatistics() {
|
||||||
|
const stats = {
|
||||||
|
totalOrders: this.orderrowsList.length,
|
||||||
|
totalCosPrice: 0,
|
||||||
|
totalEstimateFee: 0,
|
||||||
|
totalActualFee: 0,
|
||||||
|
statusStats: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusGroups = {
|
||||||
|
cancel: { label: '取消', codes: ['3'] },
|
||||||
|
invalid: { label: '无效', codes: ['2', '4', '5', '6', '7', '8', '9', '10', '11', '14', '19', '20', '21', '22', '23', '29', '30', '31', '32', '33', '34'] },
|
||||||
|
pending: { label: '待付款', codes: ['15'] },
|
||||||
|
paid: { label: '已付款', codes: ['16'] },
|
||||||
|
finished: { label: '已完成', codes: ['17'] },
|
||||||
|
deposit: { label: '已付定金', codes: ['24'] },
|
||||||
|
illegal: { label: '违规', codes: ['25', '26', '27', '28'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(statusGroups).forEach(key => {
|
||||||
|
stats.statusStats[key] = {
|
||||||
|
label: statusGroups[key].label,
|
||||||
|
count: 0,
|
||||||
|
estimateAmount: 0,
|
||||||
|
actualAmount: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.orderrowsList.forEach(order => {
|
||||||
|
if (order.estimateCosPrice != null && order.estimateCosPrice !== '') {
|
||||||
|
stats.totalCosPrice += parseFloat(order.estimateCosPrice) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validCode = String(order.validCode);
|
||||||
|
const isCancel = validCode === '3';
|
||||||
|
const isIllegal = ['25', '26', '27', '28'].includes(validCode);
|
||||||
|
const estCos = parseFloat(order.estimateCosPrice) || 0;
|
||||||
|
const rate = parseFloat(order.commissionRate) || 0;
|
||||||
|
const estFee = parseFloat(order.estimateFee) || 0;
|
||||||
|
const actFee = parseFloat(order.actualFee) || 0;
|
||||||
|
|
||||||
|
let commissionAmount = 0;
|
||||||
|
let actualFeeAmount = 0;
|
||||||
|
|
||||||
|
if (isIllegal) {
|
||||||
|
if (estCos > 0 && rate > 0) {
|
||||||
|
commissionAmount = estCos * rate / 100;
|
||||||
|
actualFeeAmount = commissionAmount;
|
||||||
|
} else if (estFee) {
|
||||||
|
commissionAmount = estFee;
|
||||||
|
actualFeeAmount = estFee;
|
||||||
|
}
|
||||||
|
} else if (isCancel) {
|
||||||
|
if (actFee > 0) {
|
||||||
|
actualFeeAmount = actFee;
|
||||||
|
commissionAmount = estFee;
|
||||||
|
} else if (estCos > 0 && rate > 0) {
|
||||||
|
commissionAmount = estCos * rate / 100;
|
||||||
|
actualFeeAmount = commissionAmount;
|
||||||
|
} else {
|
||||||
|
commissionAmount = estFee;
|
||||||
|
actualFeeAmount = actFee;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commissionAmount = estFee;
|
||||||
|
actualFeeAmount = actFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.totalEstimateFee += commissionAmount;
|
||||||
|
stats.totalActualFee += actualFeeAmount;
|
||||||
|
|
||||||
|
let statusKey = 'invalid';
|
||||||
|
for (const [key, group] of Object.entries(statusGroups)) {
|
||||||
|
if (group.codes.includes(validCode)) {
|
||||||
|
statusKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats.statusStats[statusKey].count++;
|
||||||
|
stats.statusStats[statusKey].estimateAmount += commissionAmount;
|
||||||
|
stats.statusStats[statusKey].actualAmount += actualFeeAmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.statistics = stats;
|
||||||
|
},
|
||||||
|
/** 根据状态键获取标签类型 */
|
||||||
|
getStatusTypeByKey(key) {
|
||||||
|
const typeMap = {
|
||||||
|
'cancel': 'danger',
|
||||||
|
'invalid': 'info',
|
||||||
|
'pending': 'warning',
|
||||||
|
'paid': 'primary',
|
||||||
|
'finished': 'success',
|
||||||
|
'deposit': 'warning',
|
||||||
|
'illegal': 'danger'
|
||||||
|
};
|
||||||
|
return typeMap[key] || 'info';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 主容器布局 */
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 84px); /* 减去头部导航高度 */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域 - 固定在顶部 */
|
||||||
|
.search-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格区域 - 可滚动 */
|
||||||
|
.table-section {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定分页区域 */
|
||||||
|
.pagination-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片样式 */
|
||||||
|
.statistics-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-card .el-card__header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-card .el-card__header span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-card .el-card__header .el-button {
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-card .el-card__header .el-button:hover {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-content {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chart-row {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pie-wrap,
|
||||||
|
.status-stats-expanded {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pie-wrap h4,
|
||||||
|
.status-stats-expanded h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pie-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list-expanded {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item-expanded {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item-amounts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.status-item-expanded {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.status-item-amounts {
|
||||||
|
width: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-amt-est {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-amt-act {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #67c23a;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.status-list-expanded::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list-expanded::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list-expanded::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list-expanded::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格区域滚动条样式 */
|
||||||
|
.table-section::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-thumb {
|
||||||
|
background: #c0c0c0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
height: calc(100vh - 50px); /* 移动端调整高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-section {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-content .el-col {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保表格在容器内正确显示 */
|
||||||
|
.table-section .el-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页组件样式优化 */
|
||||||
|
.pagination-section .pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.action-buttons-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端和桌面端按钮组显示控制 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-section.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,159 +1,194 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div>
|
||||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
<list-layout>
|
||||||
<el-form-item label="角色名称" prop="roleName">
|
<!-- 搜索区域 -->
|
||||||
<el-input
|
<template #search>
|
||||||
v-model="queryParams.roleName"
|
<mobile-search-form
|
||||||
placeholder="请输入角色名称"
|
:model="queryParams"
|
||||||
clearable
|
@search="handleQuery"
|
||||||
style="width: 240px"
|
@reset="resetQuery"
|
||||||
@keyup.enter.native="handleQuery"
|
>
|
||||||
/>
|
<template #form="{ expanded }">
|
||||||
</el-form-item>
|
<el-form
|
||||||
<el-form-item label="权限字符" prop="roleKey">
|
:model="queryParams"
|
||||||
<el-input
|
ref="queryForm"
|
||||||
v-model="queryParams.roleKey"
|
size="small"
|
||||||
placeholder="请输入权限字符"
|
:inline="true"
|
||||||
clearable
|
label-width="68px"
|
||||||
style="width: 240px"
|
>
|
||||||
@keyup.enter.native="handleQuery"
|
<el-form-item label="角色名称" prop="roleName">
|
||||||
/>
|
<el-input
|
||||||
</el-form-item>
|
v-model="queryParams.roleName"
|
||||||
<el-form-item label="状态" prop="status">
|
placeholder="请输入角色名称"
|
||||||
<el-select
|
clearable
|
||||||
v-model="queryParams.status"
|
style="width: 240px"
|
||||||
placeholder="角色状态"
|
@keyup.enter.native="handleQuery"
|
||||||
clearable
|
/>
|
||||||
style="width: 240px"
|
</el-form-item>
|
||||||
>
|
<el-form-item label="权限字符" prop="roleKey">
|
||||||
<el-option
|
<el-input
|
||||||
v-for="dict in dict.type.sys_normal_disable"
|
v-model="queryParams.roleKey"
|
||||||
:key="dict.value"
|
placeholder="请输入权限字符"
|
||||||
:label="dict.label"
|
clearable
|
||||||
:value="dict.value"
|
style="width: 240px"
|
||||||
/>
|
@keyup.enter.native="handleQuery"
|
||||||
</el-select>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="创建时间">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-date-picker
|
<el-select
|
||||||
v-model="dateRange"
|
v-model="queryParams.status"
|
||||||
style="width: 240px"
|
placeholder="角色状态"
|
||||||
value-format="yyyy-MM-dd"
|
clearable
|
||||||
type="daterange"
|
style="width: 240px"
|
||||||
range-separator="-"
|
>
|
||||||
start-placeholder="开始日期"
|
<el-option
|
||||||
end-placeholder="结束日期"
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
></el-date-picker>
|
:key="dict.value"
|
||||||
</el-form-item>
|
:label="dict.label"
|
||||||
<el-form-item>
|
:value="dict.value"
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
/>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
<el-row :gutter="10" class="mb8">
|
v-model="dateRange"
|
||||||
<el-col :span="1.5">
|
style="width: 240px"
|
||||||
<el-button
|
value-format="yyyy-MM-dd"
|
||||||
type="primary"
|
type="daterange"
|
||||||
plain
|
range-separator="-"
|
||||||
icon="el-icon-plus"
|
start-placeholder="开始日期"
|
||||||
size="mini"
|
end-placeholder="结束日期"
|
||||||
@click="handleAdd"
|
></el-date-picker>
|
||||||
v-hasPermi="['system:role:add']"
|
</el-form-item>
|
||||||
>新增</el-button>
|
<!-- 桌面端搜索按钮 -->
|
||||||
</el-col>
|
<el-form-item v-if="!expanded">
|
||||||
<el-col :span="1.5">
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
type="success"
|
</el-form-item>
|
||||||
plain
|
</el-form>
|
||||||
icon="el-icon-edit"
|
|
||||||
size="mini"
|
|
||||||
:disabled="single"
|
|
||||||
@click="handleUpdate"
|
|
||||||
v-hasPermi="['system:role:edit']"
|
|
||||||
>修改</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
icon="el-icon-delete"
|
|
||||||
size="mini"
|
|
||||||
:disabled="multiple"
|
|
||||||
@click="handleDelete"
|
|
||||||
v-hasPermi="['system:role:remove']"
|
|
||||||
>删除</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
|
||||||
type="warning"
|
|
||||||
plain
|
|
||||||
icon="el-icon-download"
|
|
||||||
size="mini"
|
|
||||||
@click="handleExport"
|
|
||||||
v-hasPermi="['system:role:export']"
|
|
||||||
>导出</el-button>
|
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
|
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
|
||||||
<el-table-column label="角色编号" prop="roleId" width="120" />
|
|
||||||
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
|
|
||||||
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
|
|
||||||
<el-table-column label="显示顺序" prop="roleSort" width="100" />
|
|
||||||
<el-table-column label="状态" align="center" width="100">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-switch
|
|
||||||
v-model="scope.row.status"
|
|
||||||
active-value="0"
|
|
||||||
inactive-value="1"
|
|
||||||
@change="handleStatusChange(scope.row)"
|
|
||||||
></el-switch>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</mobile-search-form>
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
|
||||||
<template slot-scope="scope">
|
<!-- 操作按钮区域(移动端单独显示) -->
|
||||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
<div class="action-buttons-section mobile-only">
|
||||||
</template>
|
<mobile-button-group
|
||||||
</el-table-column>
|
:buttons="actionButtons"
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
:primary-count="2"
|
||||||
<template slot-scope="scope" v-if="scope.row.roleId !== 1">
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端按钮组 -->
|
||||||
|
<el-row :gutter="10" class="mb8 desktop-only">
|
||||||
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['system:role:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="handleUpdate(scope.row)"
|
size="mini"
|
||||||
|
:disabled="single"
|
||||||
|
@click="handleUpdate"
|
||||||
v-hasPermi="['system:role:edit']"
|
v-hasPermi="['system:role:edit']"
|
||||||
>修改</el-button>
|
>修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
type="danger"
|
||||||
type="text"
|
plain
|
||||||
icon="el-icon-delete"
|
icon="el-icon-delete"
|
||||||
@click="handleDelete(scope.row)"
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
v-hasPermi="['system:role:remove']"
|
v-hasPermi="['system:role:remove']"
|
||||||
>删除</el-button>
|
>删除</el-button>
|
||||||
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:role:edit']">
|
</el-col>
|
||||||
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
|
<el-col :span="1.5">
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-button
|
||||||
<el-dropdown-item command="handleDataScope" icon="el-icon-circle-check"
|
type="warning"
|
||||||
v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
|
plain
|
||||||
<el-dropdown-item command="handleAuthUser" icon="el-icon-user"
|
icon="el-icon-download"
|
||||||
v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
|
size="mini"
|
||||||
</el-dropdown-menu>
|
@click="handleExport"
|
||||||
</el-dropdown>
|
v-hasPermi="['system:role:export']"
|
||||||
</template>
|
>导出</el-button>
|
||||||
</el-table-column>
|
</el-col>
|
||||||
</el-table>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
<pagination
|
<!-- 表格区域 -->
|
||||||
v-show="total>0"
|
<template #table>
|
||||||
:total="total"
|
<el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
|
||||||
:page.sync="queryParams.pageNum"
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
:limit.sync="queryParams.pageSize"
|
<el-table-column label="角色编号" prop="roleId" width="120" />
|
||||||
@pagination="getList"
|
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
|
||||||
/>
|
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
|
||||||
|
<el-table-column label="显示顺序" prop="roleSort" width="100" />
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch
|
||||||
|
v-model="scope.row.status"
|
||||||
|
active-value="0"
|
||||||
|
inactive-value="1"
|
||||||
|
@change="handleStatusChange(scope.row)"
|
||||||
|
></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope" v-if="scope.row.roleId !== 1">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['system:role:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['system:role:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:role:edit']">
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item command="handleDataScope" icon="el-icon-circle-check"
|
||||||
|
v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="handleAuthUser" icon="el-icon-user"
|
||||||
|
v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 分页区域 -->
|
||||||
|
<template #pagination>
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</list-layout>
|
||||||
|
|
||||||
<!-- 添加或修改角色配置对话框 -->
|
<!-- 添加或修改角色配置对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||||
@@ -254,9 +289,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus, deptTreeSelect } from "@/api/system/role"
|
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus, deptTreeSelect } from "@/api/system/role"
|
||||||
import { treeselect as menuTreeselect, roleMenuTreeselect } from "@/api/system/menu"
|
import { treeselect as menuTreeselect, roleMenuTreeselect } from "@/api/system/menu"
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import ListLayout from "@/components/ListLayout"
|
||||||
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
|
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Role",
|
name: "Role",
|
||||||
|
components: {
|
||||||
|
ListLayout,
|
||||||
|
MobileSearchForm,
|
||||||
|
MobileButtonGroup
|
||||||
|
},
|
||||||
dicts: ['sys_normal_disable'],
|
dicts: ['sys_normal_disable'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -341,6 +385,26 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['device']),
|
||||||
|
isMobile() {
|
||||||
|
if (this.device === 'mobile') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
actionButtons() {
|
||||||
|
return [
|
||||||
|
{ key: 'add', label: '新增', type: 'primary', icon: 'el-icon-plus', handler: () => this.handleAdd(), disabled: false },
|
||||||
|
{ key: 'update', label: '修改', type: 'success', icon: 'el-icon-edit', handler: () => this.handleUpdate(), disabled: this.single },
|
||||||
|
{ key: 'delete', label: '删除', type: 'danger', icon: 'el-icon-delete', handler: () => this.handleDelete(), disabled: this.multiple },
|
||||||
|
{ key: 'export', label: '导出', type: 'warning', icon: 'el-icon-download', handler: () => this.handleExport(), disabled: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList()
|
this.getList()
|
||||||
},
|
},
|
||||||
@@ -602,4 +666,33 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.action-buttons-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端和桌面端按钮组显示控制 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-section.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
917
src/views/system/social-media/index.vue
Normal file
917
src/views/system/social-media/index.vue
Normal file
@@ -0,0 +1,917 @@
|
|||||||
|
<template>
|
||||||
|
<div class="social-media-container">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span class="card-title">
|
||||||
|
<i class="el-icon-star-on"></i>
|
||||||
|
小红书/抖音内容生成工具
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 0"
|
||||||
|
type="text"
|
||||||
|
@click="showHelp = !showHelp">
|
||||||
|
{{ showHelp ? '隐藏帮助' : '显示帮助' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 帮助说明 -->
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-show="showHelp" class="help-section">
|
||||||
|
<el-alert
|
||||||
|
title="使用说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon>
|
||||||
|
<div slot="default">
|
||||||
|
<p><strong>功能说明:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>一键生成小红书/抖音推广内容(关键词、文案、营销图片)</li>
|
||||||
|
<li>AI智能提取商品关键词</li>
|
||||||
|
<li>AI生成多种风格的推广文案</li>
|
||||||
|
<li>自动合成营销对比图片(1080x1080)</li>
|
||||||
|
<li>支持快捷复制图片和文案</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>使用步骤:</strong></p>
|
||||||
|
<ol>
|
||||||
|
<li>输入商品信息(图片URL、名称、价格)</li>
|
||||||
|
<li>选择文案风格(小红书/抖音/通用)</li>
|
||||||
|
<li>点击"一键生成"或分步生成</li>
|
||||||
|
<li>在结果区域复制图片和文案</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
|
||||||
|
<!-- 商品信息输入区域 -->
|
||||||
|
<div class="input-section">
|
||||||
|
<el-form :model="form" label-width="120px" class="main-form">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品主图URL" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.productImageUrl"
|
||||||
|
placeholder="请输入商品主图URL"
|
||||||
|
clearable>
|
||||||
|
<el-button slot="append" @click="handlePreviewImage(form.productImageUrl)" :disabled="!form.productImageUrl">
|
||||||
|
预览
|
||||||
|
</el-button>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.productName"
|
||||||
|
placeholder="请输入完整商品名称"
|
||||||
|
clearable>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="官网价">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.originalPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="1"
|
||||||
|
placeholder="官网原价"
|
||||||
|
style="width: 100%">
|
||||||
|
</el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="到手价" required>
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.finalPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="1"
|
||||||
|
placeholder="到手价"
|
||||||
|
style="width: 100%">
|
||||||
|
</el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="文案风格">
|
||||||
|
<el-select v-model="form.style" placeholder="选择风格" style="width: 100%">
|
||||||
|
<el-option label="小红书风格" value="xhs">
|
||||||
|
<span style="float: left">小红书风格</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 13px">真实、种草</span>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="抖音风格" value="douyin">
|
||||||
|
<span style="float: left">抖音风格</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 13px">直接、有冲击力</span>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="通用风格" value="both">
|
||||||
|
<span style="float: left">通用风格</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 13px">适合多平台</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-magic-stick"
|
||||||
|
size="medium"
|
||||||
|
@click="handleGenerateComplete"
|
||||||
|
:loading="generating">
|
||||||
|
一键生成全部
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleExtractKeywords"
|
||||||
|
:loading="extractingKeywords">
|
||||||
|
提取关键词
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleGenerateContent"
|
||||||
|
:loading="generatingContent">
|
||||||
|
生成文案
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="handleReset">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
icon="el-icon-connection"
|
||||||
|
@click="showParseDialog = true">
|
||||||
|
从解析接口导入
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关键词展示区域 -->
|
||||||
|
<div v-if="result.keywords && result.keywords.length > 0" class="keywords-section">
|
||||||
|
<el-divider content-position="left">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">
|
||||||
|
<i class="el-icon-collection-tag"></i>
|
||||||
|
提取的关键词
|
||||||
|
</span>
|
||||||
|
</el-divider>
|
||||||
|
<div class="keywords-display">
|
||||||
|
<el-tag
|
||||||
|
v-for="(keyword, index) in result.keywords"
|
||||||
|
:key="index"
|
||||||
|
type="primary"
|
||||||
|
effect="dark"
|
||||||
|
size="medium"
|
||||||
|
style="margin: 5px; cursor: pointer;"
|
||||||
|
@click="handleCopyText(keyword)">
|
||||||
|
{{ keyword }}
|
||||||
|
<i class="el-icon-document-copy" style="margin-left: 5px;"></i>
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="handleCopyText(result.keywordsText)"
|
||||||
|
style="margin-left: 10px;">
|
||||||
|
复制全部关键词
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文案展示区域 -->
|
||||||
|
<div v-if="result.content" class="content-section">
|
||||||
|
<el-divider content-position="left">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">
|
||||||
|
<i class="el-icon-edit-outline"></i>
|
||||||
|
生成的文案
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="handleCopyText(result.content)"
|
||||||
|
style="float: right; margin-top: -5px;">
|
||||||
|
复制文案
|
||||||
|
</el-button>
|
||||||
|
</el-divider>
|
||||||
|
<div class="content-display">
|
||||||
|
<el-input
|
||||||
|
v-model="result.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
readonly
|
||||||
|
class="content-textarea">
|
||||||
|
</el-input>
|
||||||
|
<div class="content-actions">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="handleCopyText(result.content)">
|
||||||
|
复制文案
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleRegenerateContent">
|
||||||
|
重新生成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片展示区域 -->
|
||||||
|
<div v-if="result.imageBase64" class="image-section">
|
||||||
|
<el-divider content-position="left">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">
|
||||||
|
<i class="el-icon-picture"></i>
|
||||||
|
生成的营销图片
|
||||||
|
</span>
|
||||||
|
</el-divider>
|
||||||
|
<div class="image-display">
|
||||||
|
<div class="image-wrapper">
|
||||||
|
<img
|
||||||
|
:src="result.imageBase64"
|
||||||
|
alt="营销图片"
|
||||||
|
class="result-image"
|
||||||
|
@click="handlePreviewLargeImage(result.imageBase64)">
|
||||||
|
<div class="image-overlay">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-download"
|
||||||
|
@click="handleDownloadImage(result.imageBase64, `营销图片_${form.productName || '商品'}_${Date.now()}.jpg`)">
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="handleCopyImage(result.imageBase64)">
|
||||||
|
复制图片
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-zoom-in"
|
||||||
|
@click="handlePreviewLargeImage(result.imageBase64)">
|
||||||
|
查看大图
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷操作区域 -->
|
||||||
|
<div v-if="result.content || result.imageBase64" class="quick-actions-section">
|
||||||
|
<el-divider content-position="left">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">
|
||||||
|
<i class="el-icon-s-operation"></i>
|
||||||
|
快捷操作
|
||||||
|
</span>
|
||||||
|
</el-divider>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-document-copy"
|
||||||
|
@click="handleCopyAll">
|
||||||
|
复制全部(文案+图片链接)
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
icon="el-icon-download"
|
||||||
|
@click="handleDownloadAll">
|
||||||
|
下载图片
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleClearResult">
|
||||||
|
清空结果
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 大图预览对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
title="图片预览"
|
||||||
|
:visible.sync="previewDialogVisible"
|
||||||
|
width="80%"
|
||||||
|
:before-close="handleClosePreview">
|
||||||
|
<div class="large-preview">
|
||||||
|
<img
|
||||||
|
:src="previewLargeImageUrl"
|
||||||
|
alt="预览"
|
||||||
|
style="max-width: 100%; height: auto;">
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 从解析接口导入对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
title="从解析接口导入"
|
||||||
|
:visible.sync="showParseDialog"
|
||||||
|
width="70%"
|
||||||
|
:close-on-click-modal="false">
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="输入格式:每行一个商品,格式为:京东短链接 + Tab/空格 + 到手价"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px;">
|
||||||
|
<div slot="default">
|
||||||
|
<p>示例:</p>
|
||||||
|
<pre style="background: #f5f7fa; padding: 10px; border-radius: 4px; margin-top: 10px;">https://u.jd.com/W17zHk2 199
|
||||||
|
https://u.jd.com/W17zcSF 349</pre>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="parseText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="请输入商品链接和价格,每行一个..."
|
||||||
|
style="margin-bottom: 20px;">
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="showParseDialog = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleCallParse"
|
||||||
|
:loading="parsing">
|
||||||
|
解析
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { extractKeywords, generateContent, generateComplete } from '@/api/jarvis/socialMedia'
|
||||||
|
import { parseLineReport } from '@/api/jarvis/batchPublish'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SocialMedia',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHelp: false,
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
form: {
|
||||||
|
productImageUrl: '',
|
||||||
|
productName: '',
|
||||||
|
originalPrice: null,
|
||||||
|
finalPrice: null,
|
||||||
|
style: 'xhs'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 结果数据
|
||||||
|
result: {
|
||||||
|
keywords: [],
|
||||||
|
keywordsText: '',
|
||||||
|
content: '',
|
||||||
|
imageBase64: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
generating: false,
|
||||||
|
extractingKeywords: false,
|
||||||
|
generatingContent: false,
|
||||||
|
parsing: false,
|
||||||
|
|
||||||
|
// 预览对话框
|
||||||
|
previewDialogVisible: false,
|
||||||
|
previewLargeImageUrl: '',
|
||||||
|
|
||||||
|
// 解析对话框
|
||||||
|
showParseDialog: false,
|
||||||
|
parseText: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 尝试从localStorage加载上次的数据
|
||||||
|
this.loadFromStorage()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 一键生成全部 */
|
||||||
|
async handleGenerateComplete() {
|
||||||
|
// 验证表单
|
||||||
|
if (!this.form.productName) {
|
||||||
|
this.$message.warning('请输入商品名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form.finalPrice === null || this.form.finalPrice <= 0) {
|
||||||
|
this.$message.warning('请输入有效的到手价')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generating = true
|
||||||
|
try {
|
||||||
|
const res = await generateComplete({
|
||||||
|
productImageUrl: this.form.productImageUrl || undefined,
|
||||||
|
productName: this.form.productName,
|
||||||
|
originalPrice: this.form.originalPrice || undefined,
|
||||||
|
finalPrice: this.form.finalPrice,
|
||||||
|
style: this.form.style
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
this.result = {
|
||||||
|
keywords: data.keywords || [],
|
||||||
|
keywordsText: data.keywordsText || '',
|
||||||
|
content: data.content || '',
|
||||||
|
imageBase64: data.imageBase64 || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到localStorage
|
||||||
|
this.saveToStorage()
|
||||||
|
|
||||||
|
this.$message.success('生成成功!')
|
||||||
|
|
||||||
|
// 滚动到结果区域
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.result.keywords && this.result.keywords.length > 0) {
|
||||||
|
const keywordsSection = document.querySelector('.keywords-section')
|
||||||
|
if (keywordsSection) {
|
||||||
|
keywordsSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.error || '生成失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '生成失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成失败', error)
|
||||||
|
this.$message.error('生成失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.generating = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 提取关键词 */
|
||||||
|
async handleExtractKeywords() {
|
||||||
|
if (!this.form.productName) {
|
||||||
|
this.$message.warning('请输入商品名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extractingKeywords = true
|
||||||
|
try {
|
||||||
|
const res = await extractKeywords({
|
||||||
|
productName: this.form.productName
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
if (data.success) {
|
||||||
|
this.result.keywords = data.keywords || []
|
||||||
|
this.result.keywordsText = data.keywordsText || ''
|
||||||
|
this.$message.success('关键词提取成功!')
|
||||||
|
} else {
|
||||||
|
this.$message.warning(data.error || '提取失败,已使用降级方案')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '提取失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提取关键词失败', error)
|
||||||
|
this.$message.error('提取失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.extractingKeywords = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 生成文案 */
|
||||||
|
async handleGenerateContent() {
|
||||||
|
if (!this.form.productName) {
|
||||||
|
this.$message.warning('请输入商品名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generatingContent = true
|
||||||
|
try {
|
||||||
|
const res = await generateContent({
|
||||||
|
productName: this.form.productName,
|
||||||
|
originalPrice: this.form.originalPrice || undefined,
|
||||||
|
finalPrice: this.form.finalPrice || undefined,
|
||||||
|
keywords: this.result.keywordsText || undefined,
|
||||||
|
style: this.form.style
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
if (data.success) {
|
||||||
|
this.result.content = data.content || ''
|
||||||
|
this.$message.success('文案生成成功!')
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.error || '生成失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '生成失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成文案失败', error)
|
||||||
|
this.$message.error('生成失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.generatingContent = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 重新生成文案 */
|
||||||
|
async handleRegenerateContent() {
|
||||||
|
await this.handleGenerateContent()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 复制文本 */
|
||||||
|
handleCopyText(text) {
|
||||||
|
if (!text) {
|
||||||
|
this.$message.warning('没有可复制的内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文本域
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('复制成功!')
|
||||||
|
} catch (error) {
|
||||||
|
// 降级方案
|
||||||
|
textarea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('复制成功!')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 复制图片 */
|
||||||
|
async handleCopyImage(base64) {
|
||||||
|
try {
|
||||||
|
const base64Data = base64.split(',')[1] || base64
|
||||||
|
const byteCharacters = atob(base64Data)
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
const blob = new Blob([byteArray], { type: 'image/jpeg' })
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.write) {
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ 'image/jpeg': blob })
|
||||||
|
])
|
||||||
|
this.$message.success('图片已复制到剪贴板')
|
||||||
|
} else {
|
||||||
|
this.$message.info('浏览器不支持直接复制图片,请使用下载功能')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败', error)
|
||||||
|
this.$message.error('复制失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 下载图片 */
|
||||||
|
handleDownloadImage(base64, filename) {
|
||||||
|
try {
|
||||||
|
const base64Data = base64.split(',')[1] || base64
|
||||||
|
const byteCharacters = atob(base64Data)
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
const blob = new Blob([byteArray], { type: 'image/jpeg' })
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
this.$message.success('下载成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败', error)
|
||||||
|
this.$message.error('下载失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 复制全部 */
|
||||||
|
handleCopyAll() {
|
||||||
|
let allText = ''
|
||||||
|
|
||||||
|
if (this.result.keywordsText) {
|
||||||
|
allText += '关键词:' + this.result.keywordsText + '\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.result.content) {
|
||||||
|
allText += this.result.content + '\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.result.imageBase64) {
|
||||||
|
allText += '[营销图片已生成,请查看图片]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allText) {
|
||||||
|
this.handleCopyText(allText)
|
||||||
|
} else {
|
||||||
|
this.$message.warning('没有可复制的内容')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 下载全部 */
|
||||||
|
handleDownloadAll() {
|
||||||
|
if (this.result.imageBase64) {
|
||||||
|
this.handleDownloadImage(
|
||||||
|
this.result.imageBase64,
|
||||||
|
`营销图片_${this.form.productName || '商品'}_${Date.now()}.jpg`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.$message.warning('没有可下载的图片')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清空结果 */
|
||||||
|
handleClearResult() {
|
||||||
|
this.$confirm('确定要清空所有结果吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.result = {
|
||||||
|
keywords: [],
|
||||||
|
keywordsText: '',
|
||||||
|
content: '',
|
||||||
|
imageBase64: ''
|
||||||
|
}
|
||||||
|
this.$message.success('已清空')
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 预览图片 */
|
||||||
|
handlePreviewImage(url) {
|
||||||
|
if (!url) return
|
||||||
|
this.previewLargeImageUrl = url
|
||||||
|
this.previewDialogVisible = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 预览大图 */
|
||||||
|
handlePreviewLargeImage(base64) {
|
||||||
|
this.previewLargeImageUrl = base64
|
||||||
|
this.previewDialogVisible = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 关闭预览 */
|
||||||
|
handleClosePreview() {
|
||||||
|
this.previewDialogVisible = false
|
||||||
|
this.previewLargeImageUrl = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
handleReset() {
|
||||||
|
this.form = {
|
||||||
|
productImageUrl: '',
|
||||||
|
productName: '',
|
||||||
|
originalPrice: null,
|
||||||
|
finalPrice: null,
|
||||||
|
style: 'xhs'
|
||||||
|
}
|
||||||
|
this.result = {
|
||||||
|
keywords: [],
|
||||||
|
keywordsText: '',
|
||||||
|
content: '',
|
||||||
|
imageBase64: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 调用解析接口 */
|
||||||
|
async handleCallParse() {
|
||||||
|
if (!this.parseText || !this.parseText.trim()) {
|
||||||
|
this.$message.warning('请输入要解析的内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parsing = true
|
||||||
|
try {
|
||||||
|
const res = await parseLineReport({
|
||||||
|
message: this.parseText.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data && Array.isArray(res.data) && res.data.length > 0) {
|
||||||
|
// 导入第一个商品
|
||||||
|
const item = res.data[0]
|
||||||
|
this.form = {
|
||||||
|
productImageUrl: item.productImage || '',
|
||||||
|
productName: item.productName || '',
|
||||||
|
originalPrice: item.price || null,
|
||||||
|
finalPrice: null // 需要手动输入
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从输入文本中提取价格
|
||||||
|
const lines = this.parseText.trim().split('\n')
|
||||||
|
lines.forEach(line => {
|
||||||
|
const parts = line.trim().split(/\s+/)
|
||||||
|
if (parts.length >= 2 && parts[0].includes(item._raw?.originalUrl || '')) {
|
||||||
|
const price = parseFloat(parts[1])
|
||||||
|
if (!isNaN(price)) {
|
||||||
|
this.form.finalPrice = price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$message.success('导入成功!请检查信息后生成内容')
|
||||||
|
this.showParseDialog = false
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '解析失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('调用解析接口失败', error)
|
||||||
|
this.$message.error('解析失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.parsing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存到localStorage */
|
||||||
|
saveToStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('socialMediaForm', JSON.stringify(this.form))
|
||||||
|
localStorage.setItem('socialMediaResult', JSON.stringify(this.result))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从localStorage加载 */
|
||||||
|
loadFromStorage() {
|
||||||
|
try {
|
||||||
|
const formStr = localStorage.getItem('socialMediaForm')
|
||||||
|
const resultStr = localStorage.getItem('socialMediaResult')
|
||||||
|
|
||||||
|
if (formStr) {
|
||||||
|
this.form = { ...this.form, ...JSON.parse(formStr) }
|
||||||
|
}
|
||||||
|
if (resultStr) {
|
||||||
|
this.result = { ...this.result, ...JSON.parse(resultStr) }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.social-media-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section ul,
|
||||||
|
.help-section ol {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-section,
|
||||||
|
.content-section,
|
||||||
|
.image-section,
|
||||||
|
.quick-actions-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #EBEEF5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-display {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-display {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-textarea {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-display {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
max-width: 500px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper:hover .image-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions .el-button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-preview {
|
||||||
|
text-align: center;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
289
src/views/system/social-media/prompt-config.vue
Normal file
289
src/views/system/social-media/prompt-config.vue
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div class="prompt-config-container">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span class="card-title">
|
||||||
|
<i class="el-icon-setting"></i>
|
||||||
|
DS提示词模板配置
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 0"
|
||||||
|
type="text"
|
||||||
|
@click="showHelp = !showHelp">
|
||||||
|
{{ showHelp ? '隐藏帮助' : '显示帮助' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 帮助说明 -->
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-show="showHelp" class="help-section">
|
||||||
|
<el-alert
|
||||||
|
title="使用说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon>
|
||||||
|
<div slot="default">
|
||||||
|
<p><strong>功能说明:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>配置DS(DeepSeek)AI的提示词模板,用于生成关键词和文案</li>
|
||||||
|
<li>模板存储在Redis中,修改后立即生效</li>
|
||||||
|
<li>支持占位符:%s 用于替换实际内容</li>
|
||||||
|
<li>删除模板将恢复为系统默认模板</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>模板类型:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>关键词提取模板</strong>:用于提取商品关键词,占位符:%s = 商品名称</li>
|
||||||
|
<li><strong>小红书文案模板</strong>:用于生成小红书风格文案,占位符:%s = 商品名称,%s = 价格信息,%s = 关键词</li>
|
||||||
|
<li><strong>抖音文案模板</strong>:用于生成抖音风格文案,占位符:%s = 商品名称,%s = 价格信息,%s = 关键词</li>
|
||||||
|
<li><strong>通用文案模板</strong>:用于生成通用风格文案,占位符:%s = 商品名称,%s = 价格信息,%s = 关键词</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
|
||||||
|
<!-- 模板列表 -->
|
||||||
|
<div class="template-list">
|
||||||
|
<el-tabs v-model="activeTab" type="border-card">
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="(template, key) in templates"
|
||||||
|
:key="key"
|
||||||
|
:label="getTemplateLabel(key)"
|
||||||
|
:name="key">
|
||||||
|
<div class="template-editor">
|
||||||
|
<div class="template-info">
|
||||||
|
<el-alert
|
||||||
|
:title="template.description"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 15px;">
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div class="template-status">
|
||||||
|
<el-tag :type="template.isDefault ? 'info' : 'success'" size="small">
|
||||||
|
{{ template.isDefault ? '使用默认模板' : '使用自定义模板' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-if="!template.isDefault"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="handleResetTemplate(key)"
|
||||||
|
style="margin-left: 10px;">
|
||||||
|
恢复默认
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="template.template"
|
||||||
|
type="textarea"
|
||||||
|
:rows="12"
|
||||||
|
placeholder="请输入提示词模板..."
|
||||||
|
class="template-textarea">
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div class="template-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-check"
|
||||||
|
@click="handleSaveTemplate(key)"
|
||||||
|
:loading="saving[key]">
|
||||||
|
保存模板
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleLoadTemplate(key)">
|
||||||
|
重新加载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listPromptTemplates, getPromptTemplate, savePromptTemplate, deletePromptTemplate } from '@/api/jarvis/socialMediaPrompt'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SocialMediaPromptConfig',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHelp: false,
|
||||||
|
activeTab: 'keywords',
|
||||||
|
templates: {},
|
||||||
|
saving: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadTemplates()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 加载所有模板 */
|
||||||
|
async loadTemplates() {
|
||||||
|
try {
|
||||||
|
const res = await listPromptTemplates()
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
this.templates = res.data
|
||||||
|
// 初始化保存状态
|
||||||
|
Object.keys(this.templates).forEach(key => {
|
||||||
|
this.$set(this.saving, key, false)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '加载模板失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载模板失败', error)
|
||||||
|
this.$message.error('加载模板失败:' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 加载单个模板 */
|
||||||
|
async handleLoadTemplate(key) {
|
||||||
|
try {
|
||||||
|
const res = await getPromptTemplate(key)
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
this.$set(this.templates, key, res.data)
|
||||||
|
this.$message.success('重新加载成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '加载失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载模板失败', error)
|
||||||
|
this.$message.error('加载失败:' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存模板 */
|
||||||
|
async handleSaveTemplate(key) {
|
||||||
|
const template = this.templates[key]
|
||||||
|
if (!template || !template.template || template.template.trim() === '') {
|
||||||
|
this.$message.warning('模板内容不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$set(this.saving, key, true)
|
||||||
|
try {
|
||||||
|
const res = await savePromptTemplate({
|
||||||
|
key: key,
|
||||||
|
template: template.template.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('保存成功!')
|
||||||
|
// 更新状态
|
||||||
|
this.$set(template, 'isDefault', false)
|
||||||
|
// 重新加载以确认
|
||||||
|
await this.handleLoadTemplate(key)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存模板失败', error)
|
||||||
|
this.$message.error('保存失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.$set(this.saving, key, false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 恢复默认模板 */
|
||||||
|
async handleResetTemplate(key) {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要恢复默认模板吗?自定义模板将被删除。', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await deletePromptTemplate(key)
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('已恢复默认模板')
|
||||||
|
// 重新加载
|
||||||
|
await this.handleLoadTemplate(key)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '恢复失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('恢复默认模板失败', error)
|
||||||
|
this.$message.error('恢复失败:' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取模板标签 */
|
||||||
|
getTemplateLabel(key) {
|
||||||
|
const labels = {
|
||||||
|
'keywords': '关键词提取',
|
||||||
|
'content:xhs': '小红书文案',
|
||||||
|
'content:douyin': '抖音文案',
|
||||||
|
'content:both': '通用文案'
|
||||||
|
}
|
||||||
|
return labels[key] || key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.prompt-config-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-editor {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-textarea {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-textarea >>> .el-textarea__inner {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-actions .el-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
364
src/views/system/social-media/xianyu-wenan.vue
Normal file
364
src/views/system/social-media/xianyu-wenan.vue
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div class="xianyu-wenan-container">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span class="card-title">
|
||||||
|
<i class="el-icon-edit-outline"></i>
|
||||||
|
闲鱼文案(手动)
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 0"
|
||||||
|
type="text"
|
||||||
|
@click="showHelp = !showHelp">
|
||||||
|
{{ showHelp ? '隐藏帮助' : '显示帮助' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-show="showHelp" class="help-section">
|
||||||
|
<el-alert
|
||||||
|
title="使用说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon>
|
||||||
|
<div slot="default">
|
||||||
|
<p><strong>功能说明:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>不依赖 JD 接口,手动输入标题即可生成闲鱼文案,用于接口限流时的应急</li>
|
||||||
|
<li>一键生成「代下单」和「教你下单」两种文案</li>
|
||||||
|
<li>标题和型号会自动清洗敏感词(以旧、政府、换新等)</li>
|
||||||
|
<li>支持可选备注(型号),会拼在标题后参与生成</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
|
||||||
|
<div class="input-section">
|
||||||
|
<el-form
|
||||||
|
:model="form"
|
||||||
|
:label-width="labelWidth"
|
||||||
|
:label-position="labelPosition"
|
||||||
|
class="main-form">
|
||||||
|
<el-form-item label="商品标题" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
type="textarea"
|
||||||
|
:rows="mobile ? 3 : 2"
|
||||||
|
placeholder="请输入商品标题(必填)"
|
||||||
|
clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="型号/备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
placeholder="选填,如型号、规格等,会拼在标题后"
|
||||||
|
clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="btn-generate"
|
||||||
|
:loading="generating"
|
||||||
|
@click="handleGenerate">
|
||||||
|
生成闲鱼文案
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="btn-clear"
|
||||||
|
:disabled="!form.title && !form.remark"
|
||||||
|
@click="handleClearInput">
|
||||||
|
<i class="el-icon-delete"></i> 清空输入
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result.daixiadan || result.jiaonixiadan" class="result-section">
|
||||||
|
<el-row :gutter="gutter">
|
||||||
|
<el-col :xs="24" :sm="24" :md="12">
|
||||||
|
<el-card shadow="hover" class="result-card">
|
||||||
|
<div slot="header" class="result-card-header">
|
||||||
|
<span class="result-card-title">代下单(一键代下)</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="btn-copy"
|
||||||
|
@click="copyResult('daixiadan')">
|
||||||
|
<i class="el-icon-document-copy"></i> 复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">{{ result.daixiadan }}</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="12">
|
||||||
|
<el-card shadow="hover" class="result-card">
|
||||||
|
<div slot="header" class="result-card-header">
|
||||||
|
<span class="result-card-title">教你下单</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="btn-copy"
|
||||||
|
@click="copyResult('jiaonixiadan')">
|
||||||
|
<i class="el-icon-document-copy"></i> 复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">{{ result.jiaonixiadan }}</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateXianyuWenan } from '@/api/jarvis/socialMedia'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'XianyuWenan',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHelp: false,
|
||||||
|
form: {
|
||||||
|
title: '',
|
||||||
|
remark: ''
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
daixiadan: '',
|
||||||
|
jiaonixiadan: ''
|
||||||
|
},
|
||||||
|
generating: false,
|
||||||
|
mobile: false,
|
||||||
|
resizeTimer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelWidth() {
|
||||||
|
return this.mobile ? '80px' : '100px'
|
||||||
|
},
|
||||||
|
labelPosition() {
|
||||||
|
return this.mobile ? 'top' : 'right'
|
||||||
|
},
|
||||||
|
gutter() {
|
||||||
|
return this.mobile ? 12 : 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.checkMobile()
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
if (this.resizeTimer) clearTimeout(this.resizeTimer)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkMobile() {
|
||||||
|
this.mobile = document.documentElement.clientWidth < 768
|
||||||
|
},
|
||||||
|
onResize() {
|
||||||
|
if (this.resizeTimer) clearTimeout(this.resizeTimer)
|
||||||
|
this.resizeTimer = setTimeout(this.checkMobile, 150)
|
||||||
|
},
|
||||||
|
handleClearInput() {
|
||||||
|
this.form.title = ''
|
||||||
|
this.form.remark = ''
|
||||||
|
this.$message.success('已清空输入')
|
||||||
|
},
|
||||||
|
async handleGenerate() {
|
||||||
|
const title = (this.form.title || '').trim()
|
||||||
|
if (!title) {
|
||||||
|
this.$message.warning('请输入商品标题')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.generating = true
|
||||||
|
try {
|
||||||
|
const res = await generateXianyuWenan({
|
||||||
|
title: title,
|
||||||
|
remark: (this.form.remark || '').trim() || undefined
|
||||||
|
})
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
if (data.success) {
|
||||||
|
this.result.daixiadan = data.daixiadan || ''
|
||||||
|
this.result.jiaonixiadan = data.jiaonixiadan || ''
|
||||||
|
this.$message.success('生成成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.error || '生成失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '生成失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成失败', error)
|
||||||
|
this.$message.error('生成失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.generating = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyResult(type) {
|
||||||
|
const text = this.result[type]
|
||||||
|
if (!text) {
|
||||||
|
this.$message.warning('暂无内容可复制')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$message.success('已复制到剪贴板')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopy(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopy(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.position = 'fixed'
|
||||||
|
ta.style.left = '-9999px'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('已复制到剪贴板')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('复制失败,请手动选择复制')
|
||||||
|
}
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.xianyu-wenan-container {
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.box-card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.result-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.result-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.result-card-title {
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.btn-copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.result-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.btn-clear {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.xianyu-wenan-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.box-card {
|
||||||
|
margin: 0 -8px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.box-card ::v-deep .el-card__header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.box-card ::v-deep .el-card__body {
|
||||||
|
padding: 12px 15px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.help-section ::v-deep .el-alert__content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.main-form ::v-deep .el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.main-form ::v-deep .el-form-item__label {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.btn-generate {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.btn-clear {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.result-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.result-section ::v-deep .el-row {
|
||||||
|
margin-left: -6px !important;
|
||||||
|
margin-right: -6px !important;
|
||||||
|
}
|
||||||
|
.result-section ::v-deep .el-col {
|
||||||
|
padding-left: 6px !important;
|
||||||
|
padding-right: 6px !important;
|
||||||
|
}
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.result-card ::v-deep .el-card__header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.result-card ::v-deep .el-card__body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.result-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn-copy {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.result-content {
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 320px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,94 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div>
|
||||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
<list-layout>
|
||||||
<el-form-item label="微信ID" prop="wxid">
|
<!-- 搜索区域 -->
|
||||||
<el-input v-model="queryParams.wxid" placeholder="请输入微信ID" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
<template #search>
|
||||||
</el-form-item>
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||||
<el-form-item label="姓名" prop="name">
|
<el-form-item label="微信ID" prop="wxid">
|
||||||
<el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
<el-input v-model="queryParams.wxid" placeholder="请输入微信ID" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联盟ID" prop="unionId">
|
<el-form-item label="姓名" prop="name">
|
||||||
<el-input v-model="queryParams.unionId" placeholder="请输入联盟ID" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
<el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="激活状态" prop="isActive">
|
<el-form-item label="联盟ID" prop="unionId">
|
||||||
<el-select v-model="queryParams.isActive" placeholder="激活状态" clearable style="width: 240px">
|
<el-input v-model="queryParams.unionId" placeholder="请输入联盟ID" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||||
<el-option label="已激活" value="1" />
|
</el-form-item>
|
||||||
<el-option label="未激活" value="0" />
|
<el-form-item label="激活状态" prop="isActive">
|
||||||
</el-select>
|
<el-select v-model="queryParams.isActive" placeholder="激活状态" clearable style="width: 240px">
|
||||||
</el-form-item>
|
<el-option label="已激活" value="1" />
|
||||||
<el-form-item label="参与统计" prop="isCount">
|
<el-option label="未激活" value="0" />
|
||||||
<el-select v-model="queryParams.isCount" placeholder="参与统计" clearable style="width: 240px">
|
</el-select>
|
||||||
<el-option label="参与统计" value="1" />
|
</el-form-item>
|
||||||
<el-option label="不参与统计" value="0" />
|
<el-form-item label="参与统计" prop="isCount">
|
||||||
</el-select>
|
<el-select v-model="queryParams.isCount" placeholder="参与统计" clearable style="width: 240px">
|
||||||
</el-form-item>
|
<el-option label="参与统计" value="1" />
|
||||||
<el-form-item>
|
<el-option label="不参与统计" value="0" />
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
</el-select>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item>
|
||||||
</el-form>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
<el-row :gutter="10" class="mb8">
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:superadmin:add']">新增</el-button>
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:superadmin:add']">新增</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:superadmin:edit']">修改</el-button>
|
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:superadmin:edit']">修改</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:superadmin:remove']">删除</el-button>
|
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:superadmin:remove']">删除</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:superadmin:export']">导出</el-button>
|
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:superadmin:export']">导出</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="superadminList" @selection-change="handleSelectionChange">
|
<!-- 表格区域 -->
|
||||||
<el-table-column type="selection" width="50" align="center" />
|
<template #table>
|
||||||
<el-table-column label="微信ID" align="center" prop="wxid" min-width="120" />
|
<el-table v-loading="loading" :data="superadminList" @selection-change="handleSelectionChange">
|
||||||
<el-table-column label="姓名" align="center" prop="name" min-width="100" />
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
<el-table-column label="联盟ID" align="center" prop="unionId" min-width="120" />
|
<el-table-column label="微信ID" align="center" prop="wxid" min-width="120" />
|
||||||
<el-table-column label="应用密钥" align="center" prop="appKey" min-width="200" :show-overflow-tooltip="true" />
|
<el-table-column label="姓名" align="center" prop="name" min-width="100" />
|
||||||
<el-table-column label="秘密密钥" align="center" prop="secretKey" min-width="200" :show-overflow-tooltip="true">
|
<el-table-column label="联盟ID" align="center" prop="unionId" min-width="120" />
|
||||||
<template slot-scope="scope">
|
<el-table-column label="应用密钥" align="center" prop="appKey" min-width="200" :show-overflow-tooltip="true" />
|
||||||
<span>{{ scope.row.secretKey ? '***' + scope.row.secretKey.substring(scope.row.secretKey.length - 4) : '-' }}</span>
|
<el-table-column label="秘密密钥" align="center" prop="secretKey" min-width="200" :show-overflow-tooltip="true">
|
||||||
</template>
|
<template slot-scope="scope">
|
||||||
</el-table-column>
|
<span>{{ scope.row.secretKey ? '***' + scope.row.secretKey.substring(scope.row.secretKey.length - 4) : '-' }}</span>
|
||||||
<el-table-column label="激活状态" align="center" prop="isActive" min-width="100">
|
</template>
|
||||||
<template slot-scope="scope">
|
</el-table-column>
|
||||||
<el-tag :type="scope.row.isActive === 1 ? 'success' : 'info'">
|
<el-table-column label="激活状态" align="center" prop="isActive" min-width="100">
|
||||||
{{ scope.row.isActive === 1 ? '已激活' : '未激活' }}
|
<template slot-scope="scope">
|
||||||
</el-tag>
|
<el-tag :type="scope.row.isActive === 1 ? 'success' : 'info'">
|
||||||
</template>
|
{{ scope.row.isActive === 1 ? '已激活' : '未激活' }}
|
||||||
</el-table-column>
|
</el-tag>
|
||||||
<el-table-column label="参与统计" align="center" prop="isCount" min-width="100">
|
</template>
|
||||||
<template slot-scope="scope">
|
</el-table-column>
|
||||||
<el-tag :type="scope.row.isCount === 1 ? 'success' : 'info'">
|
<el-table-column label="参与统计" align="center" prop="isCount" min-width="100">
|
||||||
{{ scope.row.isCount === 1 ? '参与统计' : '不参与统计' }}
|
<template slot-scope="scope">
|
||||||
</el-tag>
|
<el-tag :type="scope.row.isCount === 1 ? 'success' : 'info'">
|
||||||
</template>
|
{{ scope.row.isCount === 1 ? '参与统计' : '不参与统计' }}
|
||||||
</el-table-column>
|
</el-tag>
|
||||||
<el-table-column label="创建时间" align="center" prop="createdAt" min-width="160">
|
</template>
|
||||||
<template slot-scope="scope">
|
</el-table-column>
|
||||||
<span>{{ parseTime(scope.row.createdAt) }}</span>
|
<el-table-column label="接收人" align="center" prop="touser" min-width="200" :show-overflow-tooltip="true" />
|
||||||
</template>
|
<el-table-column label="创建时间" align="center" prop="createdAt" min-width="160">
|
||||||
</el-table-column>
|
<template slot-scope="scope">
|
||||||
<el-table-column label="更新时间" align="center" prop="updatedAt" min-width="160">
|
<span>{{ parseTime(scope.row.createdAt) }}</span>
|
||||||
<template slot-scope="scope">
|
</template>
|
||||||
<span>{{ parseTime(scope.row.updatedAt) }}</span>
|
</el-table-column>
|
||||||
</template>
|
<el-table-column label="更新时间" align="center" prop="updatedAt" min-width="160">
|
||||||
</el-table-column>
|
<template slot-scope="scope">
|
||||||
<el-table-column label="操作" align="center" min-width="180" class-name="small-padding fixed-width">
|
<span>{{ parseTime(scope.row.updatedAt) }}</span>
|
||||||
<template slot-scope="scope">
|
</template>
|
||||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
|
</el-table-column>
|
||||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:superadmin:edit']">修改</el-button>
|
<el-table-column label="操作" align="center" min-width="180" class-name="small-padding fixed-width">
|
||||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:superadmin:remove']">删除</el-button>
|
<template slot-scope="scope">
|
||||||
</template>
|
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
|
||||||
</el-table-column>
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:superadmin:edit']">修改</el-button>
|
||||||
</el-table>
|
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:superadmin:remove']">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
<!-- 分页区域 -->
|
||||||
|
<template #pagination>
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||||
|
</template>
|
||||||
|
</list-layout>
|
||||||
|
|
||||||
<!-- 添加或修改超级管理员对话框 -->
|
<!-- 添加或修改超级管理员对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||||
@@ -120,6 +132,12 @@
|
|||||||
<el-radio :label="0">不参与统计</el-radio>
|
<el-radio :label="0">不参与统计</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="接收人" prop="touser">
|
||||||
|
<el-input v-model="form.touser" placeholder="请输入接收人列表,多个用逗号分隔(如:abc,bcd,efg)" />
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
|
||||||
|
企业微信用户ID,多个用逗号分隔。京粉推送时将发送给这些接收人
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
@@ -146,6 +164,7 @@
|
|||||||
{{ currentAdmin.isCount === 1 ? '参与统计' : '不参与统计' }}
|
{{ currentAdmin.isCount === 1 ? '参与统计' : '不参与统计' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="接收人" :span="2">{{ currentAdmin.touser || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ parseTime(currentAdmin.createdAt) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ parseTime(currentAdmin.createdAt) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">{{ parseTime(currentAdmin.updatedAt) }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ parseTime(currentAdmin.updatedAt) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -158,9 +177,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listSuperadmin, getSuperadmin, delSuperadmin, addSuperadmin, updateSuperadmin } from "@/api/system/superadmin";
|
import { listSuperadmin, getSuperadmin, delSuperadmin, addSuperadmin, updateSuperadmin } from "@/api/system/superadmin";
|
||||||
|
import ListLayout from "@/components/ListLayout";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Superadmin",
|
name: "Superadmin",
|
||||||
|
components: {
|
||||||
|
ListLayout
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
@@ -240,7 +263,8 @@ export default {
|
|||||||
appKey: null,
|
appKey: null,
|
||||||
secretKey: null,
|
secretKey: null,
|
||||||
isActive: 1,
|
isActive: 1,
|
||||||
isCount: 1
|
isCount: 1,
|
||||||
|
touser: null
|
||||||
};
|
};
|
||||||
this.resetForm("form");
|
this.resetForm("form");
|
||||||
},
|
},
|
||||||
|
|||||||
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
Reference in New Issue
Block a user