Compare commits
222 Commits
24105e0972
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -4,11 +4,9 @@ VUE_APP_TITLE = Jarvis
|
||||
# 开发环境配置
|
||||
ENV = 'development'
|
||||
|
||||
# Jarvis/开发环境
|
||||
VUE_APP_BASE_API = ''
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
# VUE_APP_BASE_API = 'http://134.175.126.60:30313'
|
||||
VUE_APP_BASE_API=/jarvis-api
|
||||
# VUE_APP_BASE_API = 'http://127.0.0.1:30313'
|
||||
port = 8888
|
||||
|
||||
@@ -5,7 +5,6 @@ VUE_APP_TITLE = Jarvis
|
||||
ENV = 'production'
|
||||
|
||||
# Jarvis/生产环境
|
||||
VUE_APP_BASE_API = ''
|
||||
|
||||
VUE_APP_BASE_API = 'http://134.175.126.60:30313'
|
||||
VUE_APP_BASE_API=/jarvis-api
|
||||
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
|
||||
|
||||
161
nginx-https.conf
Normal file
161
nginx-https.conf
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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 /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;
|
||||
|
||||
# 请求体相关配置(重要:支持POST请求)
|
||||
proxy_set_header Content-Type $content_type;
|
||||
proxy_set_header Content-Length $content_length;
|
||||
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 /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;
|
||||
|
||||
# POST请求支持
|
||||
proxy_set_header Content-Type $content_type;
|
||||
proxy_set_header Content-Length $content_length;
|
||||
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;
|
||||
}
|
||||
|
||||
# WPS365 OAuth回调接口(必须放在 /jarvis-api/ 之后,location / 之前)
|
||||
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;
|
||||
|
||||
# POST请求支持
|
||||
proxy_set_header Content-Type $content_type;
|
||||
proxy_set_header Content-Length $content_length;
|
||||
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",
|
||||
"jsencrypt": "3.0.0-rc.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"quill": "2.0.2",
|
||||
"screenfull": "5.0.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 http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<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">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
145
src/api/jarvis/comment.js
Normal file
145
src/api/jarvis/comment.js
Normal file
@@ -0,0 +1,145 @@
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前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'
|
||||
})
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
29
src/api/jarvis/socialMedia.js
Normal file
29
src/api/jarvis/socialMedia.js
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
181
src/api/jarvis/wps365.js
Normal file
181
src/api/jarvis/wps365.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ==================== OAuth授权相关 ====================
|
||||
|
||||
/**
|
||||
* 获取WPS365授权URL
|
||||
*/
|
||||
export function getWPS365AuthUrl(state) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/authUrl',
|
||||
method: 'get',
|
||||
params: { state }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth回调获取访问令牌
|
||||
*/
|
||||
export function getWPS365AccessToken(code) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/oauth/callback',
|
||||
method: 'get',
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
export function refreshWPS365Token(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/refreshToken',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token状态
|
||||
*/
|
||||
export function getWPS365TokenStatus(userId) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/tokenStatus',
|
||||
method: 'get',
|
||||
params: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置token(用于手动授权)
|
||||
*/
|
||||
export function setWPS365Token(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/setToken',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 用户信息相关 ====================
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getWPS365UserInfo(userId) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/userInfo',
|
||||
method: 'get',
|
||||
params: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 文件相关 ====================
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*/
|
||||
export function getWPS365FileList(params) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/files',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
*/
|
||||
export function getWPS365FileInfo(userId, fileToken) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/fileInfo',
|
||||
method: 'get',
|
||||
params: { userId, fileToken }
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 工作表相关 ====================
|
||||
|
||||
/**
|
||||
* 获取工作表列表
|
||||
*/
|
||||
export function getWPS365SheetList(userId, fileToken) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/sheets',
|
||||
method: 'get',
|
||||
params: { userId, fileToken }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据表
|
||||
*/
|
||||
export function createWPS365Sheet(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/createSheet',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 单元格操作相关 ====================
|
||||
|
||||
/**
|
||||
* 读取单元格数据
|
||||
*/
|
||||
export function readWPS365Cells(params) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/readCells',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单元格数据
|
||||
*/
|
||||
export function updateWPS365Cells(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/updateCells',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新单元格数据
|
||||
*/
|
||||
export function batchUpdateWPS365Cells(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/batchUpdateCells',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== AirSheet相关 ====================
|
||||
|
||||
/**
|
||||
* 读取AirSheet工作表数据
|
||||
* @param {Object} params - {userId, fileId, worksheetId(可选,默认0), range(可选)}
|
||||
*/
|
||||
export function readWPS365AirSheetCells(params) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/readAirSheetCells',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AirSheet工作表数据
|
||||
* @param {Object} data - {userId, fileId, worksheetId(可选,默认0), range, values}
|
||||
*/
|
||||
export function updateWPS365AirSheetCells(data) {
|
||||
return request({
|
||||
url: '/jarvis/wps365/updateAirSheetCells',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,3 +7,11 @@ export function getServer() {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,4 +8,23 @@ export function executeInstruction(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function executeInstructionWithForce(data) {
|
||||
return request({
|
||||
url: '/jarvis/instruction/execute',
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
forceGenerate: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getHistory(type, limit) {
|
||||
return request({
|
||||
url: '/jarvis/instruction/history',
|
||||
method: 'get',
|
||||
params: { type, limit }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,15 @@ export function getJDOrder(id) {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新JD订单
|
||||
export function updateJDOrder(data) {
|
||||
return request({
|
||||
url: '/system/jdorder',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 一键转链
|
||||
export function generatePromotionContent(data) {
|
||||
return request({
|
||||
@@ -121,6 +130,24 @@ export function transferWithGift(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({
|
||||
@@ -129,3 +156,48 @@ export function exportJDOrders(query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 删除JD订单(支持批量,ids为逗号分隔或数组)
|
||||
export function delJDOrder(ids) {
|
||||
// 兼容数组或字符串
|
||||
const idPath = Array.isArray(ids) ? ids.join(',') : ids
|
||||
return request({
|
||||
url: `/system/jdorder/${idPath}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
1
|
||||
// 手动获取物流信息(用于调试)
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// 自定义侧边栏样式 - 适配蓝色渐变背景
|
||||
.sidebar-container {
|
||||
// 覆盖Element UI菜单的默认样式
|
||||
.el-menu {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
|
||||
.el-menu-item, .el-submenu__title {
|
||||
color: #ffffff !important;
|
||||
background: transparent !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
color: #ffffff !important;
|
||||
border-right: 3px solid #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
.el-menu {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图标颜色
|
||||
.svg-icon {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 文字颜色
|
||||
span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 激活状态的子菜单标题
|
||||
.is-active > .el-submenu__title {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 子菜单展开时的样式
|
||||
.el-submenu.is-opened > .el-submenu__title {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单样式
|
||||
.el-menu--popup {
|
||||
background: linear-gradient(135deg, #3aa4ef 0%, #0067e2 100%) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
|
||||
.el-menu-item {
|
||||
color: #ffffff !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-container {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -90,3 +90,447 @@
|
||||
.el-submenu__icon-arrow {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
@import './transition.scss';
|
||||
@import './element-ui.scss';
|
||||
@import './sidebar.scss';
|
||||
@import './custom-sidebar.scss';
|
||||
@import './btn.scss';
|
||||
@import './mobile.scss';
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
@@ -123,6 +123,10 @@ aside {
|
||||
//main-container全局样式
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.components-container {
|
||||
@@ -177,3 +181,214 @@ aside {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -12,21 +12,42 @@
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
-webkit-transition: width .28s;
|
||||
transition: width 0.28s;
|
||||
-webkit-transition: width .28s ease-in-out;
|
||||
transition: width 0.28s ease-in-out, box-shadow 0.3s ease;
|
||||
width: $base-sidebar-width !important;
|
||||
background: $base-menu-background;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
font-size: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
border-radius: 0 24px 0 0;
|
||||
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 20px 20px 0;
|
||||
-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
|
||||
.horizontal-collapse-transition {
|
||||
@@ -69,43 +90,132 @@
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
font-size: 15px; // 增加菜单字体大小
|
||||
font-size: 14px;
|
||||
background: transparent !important;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.el-menu-item, .el-submenu__title {
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
font-size: 15px; // 确保菜单项字体大小一致
|
||||
font-size: 14px !important;
|
||||
font-weight: 500;
|
||||
color: rgba(33, 33, 33, 0.85) !important;
|
||||
background: transparent !important;
|
||||
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;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.submenu-title-noDropdown,
|
||||
&:hover {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单样式优化
|
||||
.el-submenu {
|
||||
.el-submenu__title {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .theme-dark .is-active > .el-submenu__title {
|
||||
color: $base-menu-color-active !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .el-submenu .el-menu-item {
|
||||
min-width: $base-sidebar-width !important;
|
||||
border-radius: 12px;
|
||||
margin: 4px 12px;
|
||||
padding: 0 16px !important;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
background: rgba(25, 118, 210, 0.1) !important;
|
||||
color: #1976d2 !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
& .theme-dark .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .theme-dark .el-submenu .el-menu-item {
|
||||
background-color: $base-sub-menu-background !important;
|
||||
.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-color: $base-sub-menu-hover !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图标样式优化
|
||||
.svg-icon {
|
||||
color: rgba(33, 33, 33, 0.7) !important;
|
||||
transition: all 0.3s ease;
|
||||
margin-right: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #1976d2 !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,6 +240,14 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
visibility: hidden !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
@@ -142,6 +260,13 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
visibility: hidden !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,14 +274,24 @@
|
||||
.el-submenu {
|
||||
&>.el-submenu__title {
|
||||
&>span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
visibility: hidden !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
span {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
visibility: hidden !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +306,24 @@
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: transform .28s;
|
||||
transition: transform .28s ease-in-out;
|
||||
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 {
|
||||
@@ -180,6 +331,38 @@
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,9 +374,55 @@
|
||||
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 {
|
||||
.svg-icon {
|
||||
@@ -204,27 +433,7 @@
|
||||
.nest-menu .el-submenu>.el-submenu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: rgba(58, 164, 239, 0.1) !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;
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ $tiffany: #4AB7BD;
|
||||
$yellow:#FEC171;
|
||||
$panGreen: #30B08F;
|
||||
|
||||
// 默认菜单主题风格 - 蓝色渐变主题
|
||||
$base-menu-color:#ffffff;
|
||||
$base-menu-color-active:#ffffff;
|
||||
$base-menu-background:linear-gradient(0deg, #3aa4ef 0%, #0067e2 100%);
|
||||
$base-logo-title-color: #ffffff;
|
||||
// 默认菜单主题风格 - 现代化渐变主题
|
||||
$base-menu-color:rgba(33, 33, 33, 0.85);
|
||||
$base-menu-color-active:#1976d2;
|
||||
$base-menu-background:linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
$base-logo-title-color: #1976d2;
|
||||
|
||||
$base-menu-light-color:rgba(0,0,0,.70);
|
||||
$base-menu-light-background:#ffffff;
|
||||
$base-logo-light-title-color: #001529;
|
||||
|
||||
$base-sub-menu-background:rgba(255,255,255,0.1);
|
||||
$base-sub-menu-hover:rgba(255,255,255,0.2);
|
||||
$base-sub-menu-background:rgba(255,255,255,0.08);
|
||||
$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>
|
||||
311
src/components/MobileBottomNav/index.vue
Normal file
311
src/components/MobileBottomNav/index.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<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
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
navItemsCache: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['device', 'sidebarRouters']),
|
||||
isMobile() {
|
||||
return this.device === 'mobile' || window.innerWidth < 768
|
||||
},
|
||||
navItems() {
|
||||
// 如果提供了自定义items,直接使用
|
||||
if (this.items && this.items.length > 0) {
|
||||
return this.items
|
||||
}
|
||||
|
||||
// 使用缓存,避免重复计算
|
||||
if (this.navItemsCache) {
|
||||
return this.navItemsCache
|
||||
}
|
||||
|
||||
// 从侧边栏路由中获取可用的路由
|
||||
const routes = this.sidebarRouters || []
|
||||
|
||||
// 扁平化路由,获取所有叶子节点路由
|
||||
const flattenRoutes = (routes, parentPath = '') => {
|
||||
let result = []
|
||||
if (!routes || !Array.isArray(routes)) {
|
||||
return result
|
||||
}
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.hidden) return
|
||||
|
||||
// 处理路径 - 确保路径正确
|
||||
let fullPath = route.path || ''
|
||||
if (parentPath) {
|
||||
if (fullPath.startsWith('/')) {
|
||||
fullPath = fullPath
|
||||
} else {
|
||||
// 合并路径
|
||||
const basePath = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath
|
||||
fullPath = `${basePath}/${fullPath}`.replace(/\/+/g, '/')
|
||||
}
|
||||
}
|
||||
|
||||
// 确保路径以/开头
|
||||
if (fullPath && !fullPath.startsWith('/')) {
|
||||
fullPath = '/' + fullPath
|
||||
}
|
||||
|
||||
// 如果有子路由,递归处理
|
||||
if (route.children && route.children.length > 0) {
|
||||
result = result.concat(flattenRoutes(route.children, fullPath))
|
||||
} else {
|
||||
// 叶子节点路由,且有meta信息
|
||||
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: route
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const flatRoutes = 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) {
|
||||
this.navItemsCache = mainRoutes
|
||||
return mainRoutes
|
||||
}
|
||||
|
||||
// 如果没有找到路由,返回默认导航
|
||||
const defaultRoutes = [
|
||||
{
|
||||
path: '/sloworder/index',
|
||||
label: '首页',
|
||||
icon: 'el-icon-s-home'
|
||||
}
|
||||
]
|
||||
|
||||
this.navItemsCache = defaultRoutes
|
||||
return defaultRoutes
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sidebarRouters: {
|
||||
handler() {
|
||||
// 路由变化时清除缓存
|
||||
this.navItemsCache = null
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 等待路由加载完成
|
||||
this.$nextTick(() => {
|
||||
// 延迟一下,确保路由已经加载
|
||||
setTimeout(() => {
|
||||
this.navItemsCache = null
|
||||
this.$forceUpdate()
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
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"
|
||||
:page-size.sync="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="[15, 50, 100, 200,1]"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:total="total"
|
||||
v-bind="$attrs"
|
||||
@@ -31,18 +31,18 @@ export default {
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 15
|
||||
default: 50
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [15, 50, 100, 200,1]
|
||||
return [50, 100, 200, 500, 1000]
|
||||
}
|
||||
},
|
||||
// 移动端页码按钮的数量端默认值5
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
default: document.body.clientWidth < 992 ? 5 : 7
|
||||
default: 7
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
@@ -63,8 +63,19 @@ export default {
|
||||
},
|
||||
data() {
|
||||
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: {
|
||||
currentPage: {
|
||||
get() {
|
||||
@@ -84,6 +95,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
const wasMobile = this.isMobile
|
||||
this.isMobile = window.innerWidth < 768
|
||||
if (wasMobile !== this.isMobile) {
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
if (this.currentPage * val > this.total) {
|
||||
this.currentPage = 1
|
||||
@@ -106,6 +124,33 @@ export default {
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -376,6 +376,9 @@ export default {
|
||||
else { this.$modal.msgError(res.msg || '加载属性失败'); }
|
||||
} catch(e) { this.$modal.msgError('加载属性失败'); }
|
||||
},
|
||||
onPvChange() {
|
||||
// 属性值变更时的回调(可用于调试或联动逻辑)
|
||||
},
|
||||
submitPublish() {
|
||||
this.$refs.publishForm.validate(valid => {
|
||||
if (!valid) return;
|
||||
@@ -389,10 +392,26 @@ export default {
|
||||
try { channelPv = JSON.parse(f.channelPvJson); }
|
||||
catch(e) { this.$modal.msgError('属性JSON格式不正确'); return; }
|
||||
} else if (this.selectedPv && Object.keys(this.selectedPv).length) {
|
||||
channelPv = Object.keys(this.selectedPv).map(pid => ({
|
||||
property_id: isNaN(Number(pid)) ? pid : Number(pid),
|
||||
value_id: this.selectedPv[pid]
|
||||
}));
|
||||
// 从 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,
|
||||
|
||||
@@ -151,4 +151,39 @@ export default {
|
||||
background-color: #ccc;
|
||||
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>
|
||||
|
||||
@@ -51,24 +51,50 @@ export default {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
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) {
|
||||
padding-bottom: 36px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-top: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
min-height: calc(100vh - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 84px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<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" />
|
||||
<top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
|
||||
|
||||
@@ -43,7 +41,6 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import Screenfull from '@/components/Screenfull'
|
||||
import SizeSelect from '@/components/SizeSelect'
|
||||
import Search from '@/components/HeaderSearch'
|
||||
@@ -55,7 +52,6 @@ export default {
|
||||
components: {
|
||||
Breadcrumb,
|
||||
TopNav,
|
||||
Hamburger,
|
||||
Screenfull,
|
||||
SizeSelect,
|
||||
Search,
|
||||
@@ -64,7 +60,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'avatar',
|
||||
'device',
|
||||
'nickName'
|
||||
@@ -81,9 +76,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
},
|
||||
setLayout(event) {
|
||||
this.$emit('setLayout')
|
||||
},
|
||||
@@ -94,7 +86,7 @@ export default {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = '/index'
|
||||
this.$router.push('/login')
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
@@ -110,26 +102,27 @@ export default {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
// 移动端优化
|
||||
@media (max-width: 768px) {
|
||||
height: 48px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none; // 移动端隐藏面包屑
|
||||
}
|
||||
}
|
||||
|
||||
.topmenu-container {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
left: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
@@ -141,19 +134,36 @@ export default {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
line-height: 48px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
min-width: 44px; // 增大触摸目标
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 6px;
|
||||
font-size: 16px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
@@ -161,6 +171,10 @@ export default {
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, .05)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,33 +182,71 @@ export default {
|
||||
margin-right: 0px;
|
||||
padding-right: 0px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-top: 10px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-nickname{
|
||||
position: relative;
|
||||
bottom: 10px;
|
||||
font-size: 14px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
@media (max-width: 768px) {
|
||||
display: none; // 移动端隐藏设置按钮
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||
<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">
|
||||
<div class="logo-icon">
|
||||
<i class="el-icon-shopping-cart-2"></i>
|
||||
<i class="el-icon-chat-line-round"></i>
|
||||
</div>
|
||||
</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">
|
||||
<div class="logo-icon">
|
||||
<i class="el-icon-shopping-cart-2"></i>
|
||||
<i class="el-icon-chat-line-round"></i>
|
||||
</div>
|
||||
<h1 class="sidebar-title">{{ title }}</h1>
|
||||
</router-link>
|
||||
@@ -60,10 +60,17 @@ export default {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
background: linear-gradient(90deg, #3aa4ef 0%, #0067e2 100%);
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
text-align: center;
|
||||
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 {
|
||||
height: 100%;
|
||||
@@ -73,42 +80,64 @@ export default {
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
& .logo-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
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: #ffffff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
color: #1976d2;
|
||||
text-shadow: none;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
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(0, 0, 0, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
text-shadow: none;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<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-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:collapse="false"
|
||||
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
|
||||
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
||||
:unique-opened="true"
|
||||
@@ -48,9 +48,6 @@ export default {
|
||||
},
|
||||
variables() {
|
||||
return variables
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,14 @@ export default {
|
||||
background: #fff;
|
||||
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);
|
||||
|
||||
// 移动端优化
|
||||
@media (max-width: 768px) {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
display: none; // 移动端隐藏标签页
|
||||
}
|
||||
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
@@ -258,11 +266,30 @@ export default {
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
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 {
|
||||
margin-left: 15px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
@@ -298,13 +325,39 @@ export default {
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
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 {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px 16px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
|
||||
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
|
||||
<sidebar v-if="!sidebar.hide && device !== 'mobile'" class="sidebar-container"/>
|
||||
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide, 'mobile-layout': device === 'mobile'}" class="main-container">
|
||||
<div :class="{'fixed-header':fixedHeader}">
|
||||
<navbar @setLayout="setLayout"/>
|
||||
<tags-view v-if="needTagsView"/>
|
||||
<tags-view v-if="needTagsView && device !== 'mobile'"/>
|
||||
</div>
|
||||
<app-main/>
|
||||
<settings ref="settingRef"/>
|
||||
</div>
|
||||
<!-- 移动端底部导航 -->
|
||||
<mobile-bottom-nav
|
||||
v-if="device === 'mobile'"
|
||||
:items="mobileNavItems"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav'
|
||||
import ResizeMixin from './mixin/ResizeHandler'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import variables from '@/assets/styles/variables.scss'
|
||||
|
||||
export default {
|
||||
@@ -26,7 +32,8 @@ export default {
|
||||
Navbar,
|
||||
Settings,
|
||||
Sidebar,
|
||||
TagsView
|
||||
TagsView,
|
||||
MobileBottomNav
|
||||
},
|
||||
mixins: [ResizeMixin],
|
||||
computed: {
|
||||
@@ -38,10 +45,15 @@ export default {
|
||||
needTagsView: state => state.settings.tagsView,
|
||||
fixedHeader: state => state.settings.fixedHeader
|
||||
}),
|
||||
...mapGetters(['sidebarRouters']),
|
||||
mobileNavItems() {
|
||||
// 如果返回空数组,组件会使用默认逻辑从路由中自动获取所有可用路由
|
||||
return []
|
||||
},
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
hideSidebar: false, // 侧边栏始终展开
|
||||
openSidebar: true, // 侧边栏始终展开
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === 'mobile'
|
||||
}
|
||||
@@ -75,6 +87,18 @@ export default {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
&.mobile {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
|
||||
&.openSidebar {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
@@ -107,4 +131,43 @@ export default {
|
||||
.mobile .fixed-header {
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import store from '@/store'
|
||||
|
||||
const { body } = document
|
||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||
const WIDTH = 768 // 移动端断点调整为 768px,更符合移动设备标准
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
|
||||
14
src/main.js
14
src/main.js
@@ -16,6 +16,7 @@ import { download } from '@/utils/request'
|
||||
|
||||
import './assets/icons' // icon
|
||||
import './permission' // permission control
|
||||
import { initMobile } from '@/utils/mobile' // 移动端初始化
|
||||
import { getDicts } from "@/api/system/dict/data"
|
||||
import { getConfigKey } from "@/api/system/config"
|
||||
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 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
|
||||
@@ -55,6 +61,11 @@ Vue.component('Editor', Editor)
|
||||
Vue.component('FileUpload', FileUpload)
|
||||
Vue.component('ImageUpload', ImageUpload)
|
||||
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(plugins)
|
||||
@@ -75,6 +86,9 @@ Vue.use(Element, {
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// 初始化移动端优化
|
||||
initMobile()
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
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 })
|
||||
|
||||
const whiteList = ['/login', '/register','/public/comment']
|
||||
const whiteList = ['/login', '/register', '/public/home', '/tools/comment-gen', '/tools/order-search', '/public/order-submit', '/wps365-callback', '/tendoc-callback']
|
||||
|
||||
const isWhiteList = (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)
|
||||
/* has token*/
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
next({ path: '/user/profile' })
|
||||
NProgress.done()
|
||||
} else if (isWhiteList(to.path)) {
|
||||
next()
|
||||
@@ -39,7 +39,7 @@ router.beforeEach((to, from, next) => {
|
||||
}).catch(err => {
|
||||
store.dispatch('LogOut').then(() => {
|
||||
Message.error(err)
|
||||
next({ path: '/' })
|
||||
next({ path: '/user/profile' })
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ import Layout from '@/layout'
|
||||
}
|
||||
*/
|
||||
|
||||
// 公共路由
|
||||
// 公共路由(无需权限即可访问)
|
||||
export const constantRoutes = [
|
||||
{
|
||||
path: '/redirect',
|
||||
@@ -62,17 +62,8 @@ export const constantRoutes = [
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: Layout,
|
||||
redirect: 'order/list',
|
||||
children: [
|
||||
{
|
||||
path: 'order/list',
|
||||
component: () => import('@/views/system/orderrows/index'),
|
||||
name: 'OrderList',
|
||||
meta: { title: '京粉订单', icon: 'order', affix: true }
|
||||
}
|
||||
]
|
||||
path: '/',
|
||||
redirect: '/sloworder/index'
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
@@ -88,70 +79,31 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 公共工具首页
|
||||
{
|
||||
path: '/order',
|
||||
component: Layout,
|
||||
redirect: 'list',
|
||||
name: 'Order',
|
||||
meta: { title: '京粉订单', icon: 'money' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
component: () => import('@/views/system/orderrows/index'),
|
||||
name: 'OrderList',
|
||||
meta: { title: '订单列表', icon: 'list' }
|
||||
path: '/public/home',
|
||||
component: () => import('@/views/public/PublicHome'),
|
||||
hidden: true
|
||||
},
|
||||
// 评论生成工具(内部使用,不易被发现的路径)
|
||||
{
|
||||
path: 'statistics',
|
||||
component: () => import('@/views/system/orderrows/statistics'),
|
||||
name: 'OrderStatistics',
|
||||
meta: { title: '订单统计', icon: 'chart' }
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: () => import('@/views/system/orderrows/settings'),
|
||||
name: 'OrderSettings',
|
||||
meta: { title: '订单设置', icon: 'setting' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 公开评论独立页(不使用 Layout,无侧边栏)
|
||||
{
|
||||
path: '/public/comment',
|
||||
path: '/tools/comment-gen',
|
||||
component: () => import('@/views/public/CommentGenerator'),
|
||||
hidden: true
|
||||
},
|
||||
// 订单搜索工具(内部使用,不易被发现的路径)
|
||||
{
|
||||
path: '/jdorder',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'Jdorder',
|
||||
meta: { title: '一键转链', icon: 'link' },
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/jdorder/index'),
|
||||
name: 'JdorderIndex',
|
||||
meta: { title: '转链工具', icon: 'tool' }
|
||||
}
|
||||
]
|
||||
path: '/tools/order-search',
|
||||
component: () => import('@/views/public/OrderSearch'),
|
||||
hidden: true
|
||||
},
|
||||
// 公开订单提交页(不使用 Layout,无侧边栏)
|
||||
{
|
||||
path: '/jd-instruction',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'JdInstruction',
|
||||
meta: { title: '京东指令台', icon: 'guide' },
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/jd-instruction/index'),
|
||||
name: 'JdInstructionIndex',
|
||||
meta: { title: '指令执行', icon: 'form' }
|
||||
}
|
||||
]
|
||||
path: '/public/order-submit',
|
||||
component: () => import('@/views/public/order-submit/index'),
|
||||
hidden: true
|
||||
},
|
||||
// 慢单管理(移到公共路由,无需权限)
|
||||
{
|
||||
path: '/sloworder',
|
||||
component: Layout,
|
||||
@@ -166,13 +118,84 @@ export const constantRoutes = [
|
||||
meta: { title: '下好的慢单', icon: 'list' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 动态路由,基于用户权限动态去加载
|
||||
export const dynamicRoutes = [
|
||||
// 京粉订单管理
|
||||
{
|
||||
path: '/order',
|
||||
component: Layout,
|
||||
redirect: 'list',
|
||||
name: 'Order',
|
||||
meta: { title: '京粉订单', icon: 'money' },
|
||||
permissions: ['jdorder:order:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
component: () => import('@/views/system/orderrows/index'),
|
||||
name: 'OrderList',
|
||||
meta: { title: '订单列表', icon: 'list' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
component: () => import('@/views/system/orderrows/statistics'),
|
||||
name: 'OrderStatistics',
|
||||
meta: { title: '订单统计', icon: 'chart' },
|
||||
permissions: ['jdorder:order:statistics']
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: () => import('@/views/system/orderrows/settings'),
|
||||
name: 'OrderSettings',
|
||||
meta: { title: '订单设置', icon: 'setting' },
|
||||
permissions: ['jdorder:order:settings']
|
||||
}
|
||||
]
|
||||
},
|
||||
// 一键转链工具
|
||||
{
|
||||
path: '/jdorder',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'Jdorder',
|
||||
meta: { title: '一键转链', icon: 'link' },
|
||||
permissions: ['jdorder:convert:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/jdorder/index'),
|
||||
name: 'JdorderIndex',
|
||||
meta: { title: '转链工具', icon: 'tool' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 京东指令台
|
||||
{
|
||||
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',
|
||||
@@ -182,12 +205,14 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// 线报消息管理
|
||||
{
|
||||
path: '/message',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'Message',
|
||||
meta: { title: '线报消息', icon: 'message' },
|
||||
permissions: ['jdorder:message:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
@@ -197,12 +222,46 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// 批量发品
|
||||
{
|
||||
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: 'noredirect',
|
||||
name: 'DocSync',
|
||||
meta: { title: '文档同步配置', icon: 'document' },
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/jarvis/docSync/index'),
|
||||
name: 'DocSyncIndex',
|
||||
meta: { title: '文档同步配置', icon: 'document' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 线报群管理
|
||||
{
|
||||
path: '/xbgroup',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'Xbgroup',
|
||||
meta: { title: '线报群管理', icon: 'peoples' },
|
||||
permissions: ['jdorder:xbgroup:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
@@ -212,12 +271,31 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// 礼金管理
|
||||
{
|
||||
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',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: 'System',
|
||||
meta: { title: '系统管理', icon: 'system' },
|
||||
permissions: ['system:admin:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'superadmin',
|
||||
@@ -227,10 +305,7 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// 动态路由,基于用户权限动态去加载
|
||||
export const dynamicRoutes = [
|
||||
// 原有的系统路由
|
||||
{
|
||||
path: '/system/user-auth',
|
||||
component: Layout,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
733
src/views/jarvis/docSync/components/TencentDocConfig.vue
Normal file
733
src/views/jarvis/docSync/components/TencentDocConfig.vue
Normal file
@@ -0,0 +1,733 @@
|
||||
<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">
|
||||
<template v-if="config.currentProgress <= (form.startRow + 49)">
|
||||
第 {{ form.startRow }} 行
|
||||
</template>
|
||||
<template v-else-if="config.currentProgress > (form.startRow + 100)">
|
||||
第 {{ config.currentProgress - 100 }} 行
|
||||
</template>
|
||||
<template v-else>
|
||||
第 {{ form.startRow }} 行
|
||||
</template>
|
||||
</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>
|
||||
563
src/views/jarvis/docSync/components/WPS365Config.vue
Normal file
563
src/views/jarvis/docSync/components/WPS365Config.vue
Normal file
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<div class="wps365-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 label="有效期" v-if="tokenInfo.expiresIn">
|
||||
{{ 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;">
|
||||
<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="例如:VbHZwButmh(从WPS365在线表格URL中获取)"
|
||||
clearable
|
||||
>
|
||||
<el-button
|
||||
slot="append"
|
||||
icon="el-icon-search"
|
||||
@click="handleTestRead"
|
||||
:disabled="!form.fileId || !isAuthorized"
|
||||
>
|
||||
测试读取
|
||||
</el-button>
|
||||
</el-input>
|
||||
</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-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订单将自动写入WPS365在线表格' : '请完成配置') }}</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>文件ID从WPS365在线表格URL中获取</span>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<i class="el-icon-check"></i>
|
||||
<span>点击"测试读取"验证文件ID是否正确</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getWPS365AuthUrl,
|
||||
getWPS365TokenStatus,
|
||||
readWPS365AirSheetCells,
|
||||
updateWPS365AirSheetCells
|
||||
} from '@/api/jarvis/wps365'
|
||||
|
||||
export default {
|
||||
name: 'WPS365Config',
|
||||
data() {
|
||||
return {
|
||||
isAuthorized: false,
|
||||
userId: null,
|
||||
tokenInfo: null,
|
||||
config: {
|
||||
isConfigured: false,
|
||||
hint: ''
|
||||
},
|
||||
form: {
|
||||
fileId: '',
|
||||
headerRow: 2,
|
||||
startRow: 3
|
||||
},
|
||||
rules: {
|
||||
fileId: [
|
||||
{ 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' }
|
||||
]
|
||||
},
|
||||
saveLoading: false,
|
||||
testLoading: false,
|
||||
clearLoading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.checkAuthStatus()
|
||||
this.loadConfig()
|
||||
},
|
||||
methods: {
|
||||
/** 刷新配置 */
|
||||
refresh() {
|
||||
this.checkAuthStatus()
|
||||
this.loadConfig()
|
||||
},
|
||||
|
||||
/** 检查授权状态 */
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const response = await getWPS365TokenStatus(this.userId)
|
||||
if (response.code === 200) {
|
||||
this.isAuthorized = response.data.hasToken && response.data.isValid
|
||||
this.tokenInfo = response.data
|
||||
if (response.data.userId) {
|
||||
this.userId = response.data.userId
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查授权状态失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
/** 加载配置 */
|
||||
async loadConfig() {
|
||||
try {
|
||||
// TODO: 从后端加载配置
|
||||
// 暂时从localStorage读取
|
||||
const savedConfig = localStorage.getItem('wps365_auto_write_config')
|
||||
if (savedConfig) {
|
||||
const config = JSON.parse(savedConfig)
|
||||
this.form.fileId = config.fileId || ''
|
||||
this.form.headerRow = config.headerRow || 2
|
||||
this.form.startRow = config.startRow || 3
|
||||
this.config.isConfigured = !!(config.fileId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
/** 处理授权 */
|
||||
async handleAuthorize() {
|
||||
try {
|
||||
const response = await getWPS365AuthUrl()
|
||||
if (response.code === 200) {
|
||||
const width = 600
|
||||
const height = 700
|
||||
const left = (window.screen.width - width) / 2
|
||||
const top = (window.screen.height - height) / 2
|
||||
|
||||
window.open(
|
||||
response.data,
|
||||
'WPS365授权',
|
||||
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
|
||||
)
|
||||
|
||||
this.$message.success('授权页面已打开,请在新窗口中完成授权')
|
||||
|
||||
const messageHandler = (event) => {
|
||||
if (event.data && event.data.type === 'wps365_oauth_callback') {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
if (event.data.userId) {
|
||||
this.userId = event.data.userId
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.checkAuthStatus()
|
||||
this.$message.success('授权完成')
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', messageHandler)
|
||||
|
||||
setTimeout(() => {
|
||||
this.checkAuthStatus()
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('获取授权URL失败:' + (error.msg || error.message))
|
||||
}
|
||||
},
|
||||
|
||||
/** 刷新授权状态 */
|
||||
async handleRefreshAuth() {
|
||||
await this.checkAuthStatus()
|
||||
this.$message.success('授权状态已刷新')
|
||||
},
|
||||
|
||||
/** 测试读取 */
|
||||
async handleTestRead() {
|
||||
if (!this.isAuthorized || !this.userId) {
|
||||
this.$message.warning('请先完成授权')
|
||||
return
|
||||
}
|
||||
if (!this.form.fileId) {
|
||||
this.$message.warning('请输入文件ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.$message.info('正在测试读取...')
|
||||
try {
|
||||
const response = await readWPS365AirSheetCells({
|
||||
userId: this.userId,
|
||||
fileId: this.form.fileId,
|
||||
worksheetId: '0', // AirSheet中worksheetId通常是整数,0表示第一个工作表
|
||||
range: 'A1:B5'
|
||||
})
|
||||
if (response.code === 200) {
|
||||
this.$message.success('读取成功!文件ID正确。')
|
||||
console.log('读取结果:', response.data)
|
||||
} else {
|
||||
this.$message.warning('读取失败: ' + (response.msg || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('读取失败: ' + (error.msg || error.message))
|
||||
console.error('读取错误:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/** 保存配置 */
|
||||
async handleSave() {
|
||||
this.$refs.form.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
if (!this.isAuthorized) {
|
||||
this.$message.warning('请先完成授权')
|
||||
return
|
||||
}
|
||||
|
||||
this.saveLoading = true
|
||||
try {
|
||||
// TODO: 保存到后端
|
||||
// 暂时保存到localStorage
|
||||
const config = {
|
||||
fileId: this.form.fileId,
|
||||
headerRow: this.form.headerRow,
|
||||
startRow: this.form.startRow
|
||||
}
|
||||
localStorage.setItem('wps365_auto_write_config', JSON.stringify(config))
|
||||
this.config.isConfigured = true
|
||||
this.config.hint = 'H-TF订单将自动写入WPS365在线表格'
|
||||
this.$message.success('配置保存成功')
|
||||
} catch (error) {
|
||||
this.$message.error('保存配置失败: ' + (error.msg || error.message))
|
||||
} finally {
|
||||
this.saveLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 测试配置 */
|
||||
async handleTest() {
|
||||
if (!this.isAuthorized || !this.userId) {
|
||||
this.$message.warning('请先完成授权')
|
||||
return
|
||||
}
|
||||
this.$refs.form.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.testLoading = true
|
||||
try {
|
||||
// 测试读取
|
||||
const readResponse = await readWPS365AirSheetCells({
|
||||
userId: this.userId,
|
||||
fileId: this.form.fileId,
|
||||
worksheetId: '0', // AirSheet中worksheetId通常是整数,0表示第一个工作表
|
||||
range: 'A1:B5'
|
||||
})
|
||||
if (readResponse.code !== 200) {
|
||||
this.$message.error('读取测试失败: ' + (readResponse.msg || '未知错误'))
|
||||
return
|
||||
}
|
||||
|
||||
// 测试写入(写入测试数据)
|
||||
const testRange = `A${this.form.startRow}:B${this.form.startRow}`
|
||||
const testValues = [['测试数据1', '测试数据2']]
|
||||
const writeResponse = await updateWPS365AirSheetCells({
|
||||
userId: this.userId,
|
||||
fileId: this.form.fileId,
|
||||
worksheetId: '0', // AirSheet中worksheetId通常是整数,0表示第一个工作表
|
||||
range: testRange,
|
||||
values: testValues
|
||||
})
|
||||
if (writeResponse.code === 200) {
|
||||
this.$message.success('配置测试成功!读写功能正常。')
|
||||
} else {
|
||||
this.$message.warning('写入测试失败: ' + (writeResponse.msg || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('配置测试失败: ' + (error.msg || error.message))
|
||||
console.error('测试错误:', error)
|
||||
} finally {
|
||||
this.testLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 清除配置 */
|
||||
async handleClear() {
|
||||
this.$confirm('确定要清除配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.clearLoading = true
|
||||
try {
|
||||
// TODO: 清除后端配置
|
||||
localStorage.removeItem('wps365_auto_write_config')
|
||||
this.form.fileId = ''
|
||||
this.form.headerRow = 2
|
||||
this.form.startRow = 3
|
||||
this.config.isConfigured = false
|
||||
this.config.hint = ''
|
||||
this.$message.success('配置已清除')
|
||||
} catch (error) {
|
||||
this.$message.error('清除配置失败: ' + (error.msg || error.message))
|
||||
} finally {
|
||||
this.clearLoading = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wps365-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;
|
||||
}
|
||||
|
||||
.token-info {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.help-card-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.help-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-item i {
|
||||
color: #67c23a;
|
||||
}
|
||||
</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="WPS365" name="wps365">
|
||||
<span slot="label">
|
||||
<i class="el-icon-document-copy"></i> WPS365
|
||||
</span>
|
||||
<WPS365Config ref="wps365Config" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TencentDocConfig from './components/TencentDocConfig'
|
||||
import WPS365Config from './components/WPS365Config'
|
||||
|
||||
export default {
|
||||
name: 'DocSync',
|
||||
components: {
|
||||
TencentDocConfig,
|
||||
WPS365Config
|
||||
},
|
||||
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 === 'wps365' && this.$refs.wps365Config) {
|
||||
this.$refs.wps365Config.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</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>
|
||||
|
||||
453
src/views/jarvis/wps365/index.vue
Normal file
453
src/views/jarvis/wps365/index.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card>
|
||||
<div slot="header" class="clearfix">
|
||||
<span>WPS365 在线表格管理</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>请先完成WPS365授权,才能使用文档编辑功能</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>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
style="margin-left: 10px"
|
||||
@click="handleRefreshToken">
|
||||
刷新Token
|
||||
</el-button>
|
||||
</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="用户ID">{{ userInfo.user_id || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ userInfo.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ userInfo.email || '-' }}</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">
|
||||
加载文件
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="fileListLoading"
|
||||
:data="fileList"
|
||||
border
|
||||
style="width: 100%">
|
||||
<el-table-column prop="file_name" label="文件名" width="200" />
|
||||
<el-table-column prop="file_token" label="文件Token" width="250" />
|
||||
<el-table-column prop="file_type" label="类型" width="100" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
@click="handleViewFile(scope.row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
size="mini"
|
||||
@click="handleEditFile(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="fileTotal > 0"
|
||||
:total="fileTotal"
|
||||
:page.sync="queryParams.page"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="handleLoadFiles"
|
||||
/>
|
||||
</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, index) in sheetList"
|
||||
:key="index"
|
||||
:label="sheet.name"
|
||||
:value="index">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单元格范围">
|
||||
<el-input
|
||||
v-model="cellRange"
|
||||
placeholder="例如:A1:B10"
|
||||
style="width: 200px">
|
||||
</el-input>
|
||||
</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">
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getWPS365AuthUrl,
|
||||
getWPS365TokenStatus,
|
||||
getWPS365UserInfo,
|
||||
getWPS365FileList,
|
||||
getWPS365SheetList,
|
||||
readWPS365Cells,
|
||||
updateWPS365Cells,
|
||||
refreshWPS365Token
|
||||
} from '@/api/jarvis/wps365'
|
||||
|
||||
export default {
|
||||
name: 'WPS365',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
userInfo: null,
|
||||
userId: '', // 这里应该从登录用户获取,暂时使用配置
|
||||
|
||||
// 文件列表
|
||||
fileList: [],
|
||||
fileListLoading: false,
|
||||
fileTotal: 0,
|
||||
queryParams: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
},
|
||||
|
||||
// 编辑对话框
|
||||
editDialogVisible: false,
|
||||
currentFile: null,
|
||||
currentSheetIdx: 0,
|
||||
sheetList: [],
|
||||
sheetData: [],
|
||||
sheetDataLoading: false,
|
||||
cellRange: 'A1:Z100',
|
||||
sheetColumns: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 初始化时检查授权状态
|
||||
// TODO: 从当前登录用户获取userId
|
||||
this.userId = 'default_user' // 临时值,需要替换为实际用户ID
|
||||
this.checkAuthStatus()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 检查授权状态
|
||||
*/
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const response = await getWPS365TokenStatus(this.userId)
|
||||
if (response.code === 200) {
|
||||
this.isAuthorized = response.data.hasToken && response.data.isValid
|
||||
if (this.isAuthorized) {
|
||||
this.loadUserInfo()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查授权状态失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载用户信息
|
||||
*/
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const response = await getWPS365UserInfo(this.userId)
|
||||
if (response.code === 200) {
|
||||
this.userInfo = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理授权
|
||||
*/
|
||||
async handleAuthorize() {
|
||||
try {
|
||||
const response = await getWPS365AuthUrl()
|
||||
if (response.code === 200) {
|
||||
// 在新窗口打开授权页面
|
||||
window.open(response.data, '_blank')
|
||||
this.$message.success('请在新窗口中完成授权,授权完成后刷新此页面')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('获取授权URL失败:' + (error.msg || error.message))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
async handleRefreshToken() {
|
||||
try {
|
||||
// TODO: 从存储中获取refreshToken
|
||||
const response = await refreshWPS365Token({
|
||||
refreshToken: '' // 需要从存储中获取
|
||||
})
|
||||
if (response.code === 200) {
|
||||
this.$message.success('Token刷新成功')
|
||||
this.checkAuthStatus()
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('刷新Token失败:' + (error.msg || error.message))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新
|
||||
*/
|
||||
handleRefresh() {
|
||||
this.checkAuthStatus()
|
||||
if (this.isAuthorized) {
|
||||
this.handleLoadFiles()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载文件列表
|
||||
*/
|
||||
async handleLoadFiles() {
|
||||
if (!this.isAuthorized) {
|
||||
this.$message.warning('请先完成授权')
|
||||
return
|
||||
}
|
||||
|
||||
this.fileListLoading = true
|
||||
try {
|
||||
const response = await getWPS365FileList({
|
||||
userId: this.userId,
|
||||
page: this.queryParams.page,
|
||||
pageSize: this.queryParams.pageSize
|
||||
})
|
||||
if (response.code === 200) {
|
||||
this.fileList = response.data.files || []
|
||||
this.fileTotal = response.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载文件列表失败:' + (error.msg || error.message))
|
||||
} finally {
|
||||
this.fileListLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看文件
|
||||
*/
|
||||
handleViewFile(file) {
|
||||
this.$message.info('查看文件功能开发中')
|
||||
},
|
||||
|
||||
/**
|
||||
* 编辑文件
|
||||
*/
|
||||
async handleEditFile(file) {
|
||||
this.currentFile = file
|
||||
this.editDialogVisible = true
|
||||
|
||||
// 加载工作表列表
|
||||
try {
|
||||
const response = await getWPS365SheetList(this.userId, file.file_token)
|
||||
if (response.code === 200) {
|
||||
this.sheetList = response.data.sheets || []
|
||||
if (this.sheetList.length > 0) {
|
||||
this.currentSheetIdx = 0
|
||||
this.handleLoadSheetData()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载工作表列表失败:' + (error.msg || error.message))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载工作表数据
|
||||
*/
|
||||
async handleLoadSheetData() {
|
||||
if (!this.currentFile) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sheetDataLoading = true
|
||||
try {
|
||||
const response = await readWPS365Cells({
|
||||
userId: this.userId,
|
||||
fileToken: this.currentFile.file_token,
|
||||
sheetIdx: this.currentSheetIdx,
|
||||
range: this.cellRange
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
const values = response.data.values || []
|
||||
this.processSheetData(values)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载工作表数据失败:' + (error.msg || error.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.length))
|
||||
this.sheetColumns = Array.from({ length: maxCols }, (_, i) => i)
|
||||
|
||||
// 转换为表格数据格式
|
||||
this.sheetData = values.map(row => {
|
||||
const rowData = {}
|
||||
row.forEach((cell, index) => {
|
||||
rowData['col' + index] = cell !== null && cell !== undefined ? String(cell) : ''
|
||||
})
|
||||
// 填充空列
|
||||
for (let i = row.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 value = row['col' + colIndex]
|
||||
return value !== undefined ? value : ''
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await updateWPS365Cells({
|
||||
userId: this.userId,
|
||||
fileToken: this.currentFile.file_token,
|
||||
sheetIdx: this.currentSheetIdx,
|
||||
range: this.cellRange,
|
||||
values: values
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
this.$message.success('更新成功')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('更新失败:' + (error.msg || error.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,11 +24,12 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled">
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 63%"
|
||||
style="flex: 1"
|
||||
@keyup.enter.native="handleLogin"
|
||||
>
|
||||
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
|
||||
@@ -36,6 +37,7 @@
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||
<el-form-item style="width:100%;">
|
||||
@@ -73,8 +75,8 @@ export default {
|
||||
title: process.env.VUE_APP_TITLE,
|
||||
codeUrl: "",
|
||||
loginForm: {
|
||||
username: "admin",
|
||||
password: "admin123",
|
||||
username: "",
|
||||
password: "",
|
||||
rememberMe: false,
|
||||
code: "",
|
||||
uuid: ""
|
||||
@@ -142,7 +144,17 @@ export default {
|
||||
Cookies.remove('rememberMe')
|
||||
}
|
||||
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(() => {
|
||||
this.loading = false
|
||||
if (this.captchaEnabled) {
|
||||
@@ -164,11 +176,26 @@ export default {
|
||||
height: 100%;
|
||||
background-image: url("../assets/images/login-background.jpg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding: 20px;
|
||||
|
||||
// 移动端优化
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
align-items: flex-start;
|
||||
padding-top: 10vh;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
font-size: 24px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
margin: 0px auto 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
@@ -177,10 +204,26 @@ export default {
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 移动端优化
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 20px 15px 5px 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
height: 38px;
|
||||
input {
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 16px; // 防止iOS自动缩放
|
||||
}
|
||||
}
|
||||
}
|
||||
.input-icon {
|
||||
@@ -188,6 +231,27 @@ export default {
|
||||
width: 14px;
|
||||
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 {
|
||||
font-size: 13px;
|
||||
@@ -198,9 +262,22 @@ export default {
|
||||
width: 33%;
|
||||
height: 38px;
|
||||
float: right;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 35%;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-login-footer {
|
||||
@@ -214,8 +291,18 @@ export default {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 11px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
.login-code-img {
|
||||
height: 38px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -171,23 +171,109 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</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>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getServer } from "@/api/monitor/server"
|
||||
import { getServer, getHealth } from "@/api/monitor/server"
|
||||
|
||||
export default {
|
||||
name: "Server",
|
||||
data() {
|
||||
return {
|
||||
// 服务器信息
|
||||
server: []
|
||||
server: [],
|
||||
// 健康度检测信息
|
||||
health: {
|
||||
logistics: null,
|
||||
wxSend: null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.getHealthInfo()
|
||||
this.openLoading()
|
||||
},
|
||||
methods: {
|
||||
@@ -198,6 +284,16 @@ export default {
|
||||
this.$modal.closeLoading()
|
||||
})
|
||||
},
|
||||
/** 查询健康度检测信息 */
|
||||
getHealthInfo() {
|
||||
getHealth().then(response => {
|
||||
if (response.data) {
|
||||
this.health = response.data
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("获取健康度检测信息失败", error)
|
||||
})
|
||||
},
|
||||
// 打开加载层
|
||||
openLoading() {
|
||||
this.$modal.loading("正在加载服务监控数据,请稍候!")
|
||||
|
||||
@@ -1,33 +1,45 @@
|
||||
<template>
|
||||
<div class="mobile-container">
|
||||
<div class="mobile-card">
|
||||
<div class="mobile-header">
|
||||
<h3>评论生成(公开)</h3>
|
||||
</div>
|
||||
|
||||
<div class="mobile-form">
|
||||
<!-- 访问统计区域 -->
|
||||
<div class="form-section usage-statistics-section">
|
||||
<div class="usage-stats-row">
|
||||
<div class="usage-stat-item">
|
||||
<div class="usage-stat-label">今天</div>
|
||||
<div class="usage-stat-number">{{ usageStatistics.today || 0 }}</div>
|
||||
</div>
|
||||
<div class="usage-stat-item">
|
||||
<div class="usage-stat-label">近7天</div>
|
||||
<div class="usage-stat-number">{{ usageStatistics.last7Days || 0 }}</div>
|
||||
</div>
|
||||
<div class="usage-stat-item">
|
||||
<div class="usage-stat-label">近30天</div>
|
||||
<div class="usage-stat-number">{{ usageStatistics.last30Days || 0 }}</div>
|
||||
</div>
|
||||
<div class="usage-stat-item">
|
||||
<div class="usage-stat-label">累计</div>
|
||||
<div class="usage-stat-number">{{ usageStatistics.total || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 产品类型选择区域 -->
|
||||
<div class="form-section">
|
||||
<div class="form-label">型号/类型</div>
|
||||
<div class="select-row">
|
||||
<el-select
|
||||
v-model="form.productType"
|
||||
filterable
|
||||
placeholder="请选择型号/类型"
|
||||
class="mobile-select"
|
||||
size="medium"
|
||||
>
|
||||
<el-option v-for="it in typeOptions" :key="it.name" :label="it.name" :value="it.name" />
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="medium"
|
||||
class="refresh-btn"
|
||||
@click="loadTypes"
|
||||
icon="el-icon-refresh"
|
||||
>
|
||||
刷新
|
||||
</el-button>
|
||||
<div class="word-sea">
|
||||
<div v-if="Object.keys(groupedByLetter).length === 0" class="empty-hint">暂无数据</div>
|
||||
<div v-else v-for="(items, ltr) in groupedByLetter" :key="ltr" class="group">
|
||||
<div class="group-head">{{ ltr }}</div>
|
||||
<div class="group-items">
|
||||
<span
|
||||
v-for="it in items"
|
||||
:key="it.name"
|
||||
class="item-tag"
|
||||
:class="{ active: form.productType === it.name }"
|
||||
@click="selectType(it)"
|
||||
>{{ it.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,6 +130,10 @@
|
||||
</span>
|
||||
<span class="product-type">{{ statistics.productType }}</span>
|
||||
</div>
|
||||
<div v-if="statistics.lastCommentUpdateTime" class="update-time">
|
||||
<i class="el-icon-time"></i>
|
||||
更新日期:{{ formatTime(statistics.lastCommentUpdateTime) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="statistics-content">
|
||||
@@ -158,14 +174,64 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录区域 -->
|
||||
<div class="form-section history-section">
|
||||
<div class="form-label">
|
||||
<i class="el-icon-time"></i>
|
||||
历史记录
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-refresh"
|
||||
@click="loadHistory"
|
||||
:loading="historyLoading"
|
||||
style="margin-left: 8px;"
|
||||
>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="historyLoading" class="history-loading">
|
||||
<i class="el-icon-loading"></i> 加载中...
|
||||
</div>
|
||||
<div v-else-if="historyList.length === 0" class="history-empty">
|
||||
<i class="el-icon-document-remove"></i>
|
||||
<p>暂无历史记录</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div
|
||||
v-for="(item, idx) in historyList"
|
||||
:key="idx"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="history-item-header">
|
||||
<span class="history-type">{{ item.productType || '-' }}</span>
|
||||
<span class="history-time">{{ formatTime(item.createTime) }}</span>
|
||||
</div>
|
||||
<div class="history-item-info">
|
||||
<span class="history-ip">IP: {{ item.ip || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页尾导航 -->
|
||||
<PublicFooterNav />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import PublicFooterNav from '@/components/PublicFooterNav'
|
||||
import { parseTime } from '@/utils/ruoyi'
|
||||
|
||||
export default {
|
||||
name: 'CommentGeneratorPublic',
|
||||
components: {
|
||||
PublicFooterNav
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: { productType: '' },
|
||||
@@ -176,13 +242,53 @@ export default {
|
||||
statistics: null,
|
||||
lastGenerateTime: 0,
|
||||
cooldownTime: 1000, // 5秒冷却时间
|
||||
isButtonDisabled: false
|
||||
isButtonDisabled: false,
|
||||
currentIP: '',
|
||||
usageStatistics: {
|
||||
today: 0,
|
||||
last7Days: 0,
|
||||
last30Days: 0,
|
||||
total: 0
|
||||
},
|
||||
historyList: [],
|
||||
historyLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pretty() {
|
||||
try { return this.result ? JSON.stringify(this.result, null, 2) : '' } catch(e) { return '' }
|
||||
},
|
||||
groupedByLetter() {
|
||||
const groups = {}
|
||||
const items = Array.isArray(this.typeOptions) ? this.typeOptions.slice() : []
|
||||
items.forEach(it => {
|
||||
const ltr = this.getInitial(it)
|
||||
if (!groups[ltr]) groups[ltr] = []
|
||||
groups[ltr].push(it)
|
||||
})
|
||||
Object.keys(groups).forEach(k => {
|
||||
groups[k].sort((a, b) => {
|
||||
const an = (a.name || '').toString()
|
||||
const bn = (b.name || '').toString()
|
||||
return an.localeCompare(bn)
|
||||
})
|
||||
})
|
||||
// 按字母顺序排序,确保显示顺序一致
|
||||
const ordered = {}
|
||||
const letters = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
letters.concat('#').forEach(l => {
|
||||
if (groups[l] && groups[l].length) {
|
||||
ordered[l] = groups[l]
|
||||
}
|
||||
})
|
||||
// 如果有其他字母(不在A-Z范围内),也添加进去
|
||||
Object.keys(groups).forEach(k => {
|
||||
if (!ordered[k] && groups[k].length) {
|
||||
ordered[k] = groups[k]
|
||||
}
|
||||
})
|
||||
return ordered
|
||||
},
|
||||
isGenerateButtonDisabled() {
|
||||
// 如果正在加载、手动禁用、没有选择产品类型,或者在冷却时间内,则禁用按钮
|
||||
return this.loading ||
|
||||
@@ -198,6 +304,9 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.loadTypes()
|
||||
this.loadCurrentIP()
|
||||
this.loadUsageStatistics()
|
||||
this.loadHistory()
|
||||
// 启动定时器更新冷却时间显示
|
||||
this.cooldownTimer = setInterval(() => {
|
||||
// 检查倒计时是否结束,如果结束则清空lastGenerateTime
|
||||
@@ -217,6 +326,55 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCurrentIP() {
|
||||
try {
|
||||
const res = await this.$axios({ url: '/public/comment/ip', method: 'get' })
|
||||
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||
this.currentIP = res.data?.ip || res.data || '获取失败'
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('获取IP失败:', e)
|
||||
this.currentIP = '获取失败'
|
||||
}
|
||||
},
|
||||
async loadUsageStatistics() {
|
||||
try {
|
||||
const res = await this.$axios({ url: '/public/comment/usage-statistics', method: 'get' })
|
||||
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||
this.usageStatistics = {
|
||||
today: res.data?.today || 0,
|
||||
last7Days: res.data?.last7Days || res.data?.last7days || 0,
|
||||
last30Days: res.data?.last30Days || res.data?.last30days || 0,
|
||||
total: res.data?.total || 0
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('获取使用统计失败:', e)
|
||||
}
|
||||
},
|
||||
async loadHistory() {
|
||||
this.historyLoading = true
|
||||
try {
|
||||
const res = await this.$axios({
|
||||
url: '/public/comment/history',
|
||||
method: 'get',
|
||||
params: { pageNum: 1, pageSize: 20 }
|
||||
})
|
||||
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||
const list = res.data?.rows || res.data?.list || res.data || []
|
||||
this.historyList = Array.isArray(list) ? list : []
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('获取历史记录失败:', e)
|
||||
this.historyList = []
|
||||
} finally {
|
||||
this.historyLoading = false
|
||||
}
|
||||
},
|
||||
formatTime(time) {
|
||||
if (!time) return '-'
|
||||
return parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}')
|
||||
},
|
||||
async loadTypes() {
|
||||
try {
|
||||
const res = await this.$axios({ url: '/public/comment/types', method: 'get' })
|
||||
@@ -227,6 +385,34 @@ export default {
|
||||
}
|
||||
} catch(e) {}
|
||||
},
|
||||
getInitial(it) {
|
||||
const source = (it && (it.name || it.value) || '').toString().trim()
|
||||
if (!source) return '#'
|
||||
const firstChar = source[0]
|
||||
const upperAscii = firstChar.toUpperCase()
|
||||
if (upperAscii >= 'A' && upperAscii <= 'Z') return upperAscii
|
||||
// 中文等非 ASCII 字符,取拼音首字母
|
||||
try {
|
||||
const letter = pinyin(firstChar, { toneType: 'none', type: 'array' })[0]
|
||||
if (!letter || !letter.length) return '#'
|
||||
const initial = letter[0].toUpperCase()
|
||||
return (initial >= 'A' && initial <= 'Z') ? initial : '#'
|
||||
} catch(e) {
|
||||
return '#'
|
||||
}
|
||||
},
|
||||
selectType(it) {
|
||||
if (!it) return
|
||||
// 如果选择的是同一个型号,不重复提交
|
||||
if (this.form.productType === it.name) {
|
||||
return
|
||||
}
|
||||
this.form.productType = it.name
|
||||
// 自动提交请求获取评论
|
||||
this.$nextTick(() => {
|
||||
this.generate()
|
||||
})
|
||||
},
|
||||
async generate() {
|
||||
// 检查按钮是否被禁用
|
||||
if (this.isGenerateButtonDisabled) {
|
||||
@@ -278,6 +464,10 @@ export default {
|
||||
// 解析统计信息
|
||||
this.statistics = res.data && res.data.statistics ? res.data.statistics : null
|
||||
|
||||
// 刷新使用统计和历史记录
|
||||
this.loadUsageStatistics()
|
||||
this.loadHistory()
|
||||
|
||||
this.$message.success('评论生成成功')
|
||||
} else {
|
||||
this.$message.error(res && res.msg ? res.msg : '生成失败')
|
||||
@@ -347,6 +537,7 @@ export default {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
padding-bottom: calc(80px + 16px); /* 为页尾导航留出空间 */
|
||||
}
|
||||
|
||||
.mobile-card {
|
||||
@@ -356,27 +547,14 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.mobile-header {
|
||||
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
|
||||
color: #fff;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.mobile-form {
|
||||
padding: 20px 16px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@@ -384,6 +562,14 @@ export default {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
font-size: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 选择器行样式 */
|
||||
@@ -398,10 +584,26 @@ export default {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
/* 词海分组样式 */
|
||||
.word-sea .group { margin-bottom: 12px; }
|
||||
.word-sea .group-head { font-weight: 600; margin: 6px 0; color: #606266; }
|
||||
.word-sea .group-items { display: flex; flex-wrap: wrap; }
|
||||
.word-sea .item-tag {
|
||||
display: inline-block;
|
||||
padding: 8px 14px;
|
||||
margin: 4px 8px 4px 0;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
user-select: none;
|
||||
transition: all .15s ease;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.word-sea .item-tag:hover { border-color: #409eff; color: #409eff; }
|
||||
.word-sea .item-tag.active { background: #409eff; border-color: #409eff; color: #fff; }
|
||||
.word-sea .empty-hint { color: #909399; }
|
||||
|
||||
/* 生成按钮样式 */
|
||||
.generate-btn {
|
||||
@@ -603,10 +805,6 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 6px;
|
||||
@@ -616,14 +814,18 @@ export default {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.mobile-header h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 移动端型号选择按钮 */
|
||||
.word-sea .item-tag {
|
||||
padding: 6px 12px;
|
||||
margin: 4px 6px 4px 0;
|
||||
font-size: 13px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
@@ -708,6 +910,21 @@ export default {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.update-time i {
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.source-tag {
|
||||
@@ -884,6 +1101,155 @@ export default {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 访问统计样式 */
|
||||
.usage-statistics-section {
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||||
border: 1px solid #e1e8ff;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.usage-stats-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.usage-stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
padding: 6px 4px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.usage-stat-label {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.usage-stat-number {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 历史记录样式 */
|
||||
.history-section {
|
||||
margin-top: 24px;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.history-loading,
|
||||
.history-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.history-loading i,
|
||||
.history-empty i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.history-empty p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.history-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-type {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.history-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.history-ip {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 480px) {
|
||||
.usage-statistics-section {
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.usage-stats-row {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.usage-stat-item {
|
||||
padding: 5px 3px;
|
||||
}
|
||||
|
||||
.usage-stat-number {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.usage-stat-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.history-item-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
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>
|
||||
|
||||
614
src/views/system/comment/index.vue
Normal file
614
src/views/system/comment/index.vue
Normal file
@@ -0,0 +1,614 @@
|
||||
<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="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="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="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="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>
|
||||
</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
|
||||
} 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: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['device']),
|
||||
isMobile() {
|
||||
if (this.device === 'mobile') {
|
||||
return true
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
jdActionButtons() {
|
||||
return [
|
||||
{ key: 'export', label: '导出', type: 'success', icon: 'el-icon-download', handler: () => this.handleJdExport(), disabled: false }
|
||||
]
|
||||
},
|
||||
tbActionButtons() {
|
||||
return [
|
||||
{ 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`)
|
||||
},
|
||||
// 淘宝评论相关
|
||||
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`)
|
||||
},
|
||||
// 统计信息相关
|
||||
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>
|
||||
|
||||
720
src/views/system/erpProduct/index.vue
Normal file
720
src/views/system/erpProduct/index.vue
Normal file
@@ -0,0 +1,720 @@
|
||||
<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 || [];
|
||||
});
|
||||
},
|
||||
/** 加载会员名列表 */
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<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"
|
||||
@@ -31,13 +44,25 @@
|
||||
<el-option label="否" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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">
|
||||
<!-- 操作按钮区域(移动端单独显示) -->
|
||||
<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"
|
||||
@@ -347,14 +372,38 @@
|
||||
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 },
|
||||
components: {
|
||||
PublishDialog,
|
||||
MobileSearchForm,
|
||||
MobileButtonGroup
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['favoriteProductRefreshKey'])
|
||||
...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 {
|
||||
@@ -628,4 +677,31 @@ export default {
|
||||
.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>
|
||||
|
||||
@@ -9,11 +9,21 @@
|
||||
<el-form-item label="输入指令">
|
||||
<el-input v-model="form.command" type="textarea" :rows="8" placeholder="例如:京今日统计 / 京昨日订单 / 慢搜关键词 / 录单20250101-20250107" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="run" :loading="loading">执行</el-button>
|
||||
<el-button @click="fillMenu">菜单</el-button>
|
||||
<el-button @click="clearAll">清空</el-button>
|
||||
</el-form-item>
|
||||
<div class="button-group button-group-primary">
|
||||
<el-button type="success" size="medium" @click="run" :loading="loading">执行</el-button>
|
||||
<el-button type="danger" size="medium" @click="clearAll">清空</el-button>
|
||||
<el-button size="warning" @click="fillMan">慢单</el-button>
|
||||
<el-button v-if="isMobile" size="primary" @click="fillTF">腾峰</el-button>
|
||||
<el-button v-if="!isMobile" size="success" @click="fillSheng">生</el-button>
|
||||
</div>
|
||||
<div class="button-group button-group-secondary">
|
||||
<el-button v-if="!isMobile" size="primary" @click="fillTF">腾峰</el-button>
|
||||
<el-button v-if="!isMobile" type="primary" size="medium" @click="fillFan">凡</el-button>
|
||||
<el-button v-if="!isMobile" type="primary" size="medium" @click="fillWen">纹</el-button>
|
||||
<el-button v-if="!isMobile" type="primary" size="medium" @click="fillHong">鸿</el-button>
|
||||
<el-button v-if="!isMobile" type="primary" size="medium" @click="fillPDD">拼多多</el-button>
|
||||
<el-button v-if="!isMobile" type="primary" size="medium" @click="fillPDDWen">拼多多-纹</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-divider>响应</el-divider>
|
||||
@@ -21,24 +31,146 @@
|
||||
<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;">
|
||||
<div v-else class="response-container">
|
||||
<!-- 上面:完整消息 -->
|
||||
<div class="response-section response-section-full">
|
||||
<div class="response-header">
|
||||
<span>完整消息</span>
|
||||
<el-button size="mini" type="primary" @click="copyAll">复制全部</el-button>
|
||||
</div>
|
||||
<div class="response-content-full">
|
||||
<el-input :value="fullMessage" type="textarea" :rows="15" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下面:独立消息列表 -->
|
||||
<div class="response-section response-section-list">
|
||||
<div class="response-header">
|
||||
<span>消息列表(共 {{ resultList.length }} 条)</span>
|
||||
</div>
|
||||
<div class="response-content-list">
|
||||
<div class="message-list">
|
||||
<div v-for="(msg, idx) in resultList" :key="idx" class="message-item">
|
||||
<div class="message-item-header">
|
||||
<span class="message-index">第 {{ idx + 1 }} 条</span>
|
||||
<el-button size="mini" type="success" icon="el-icon-document-copy" @click="copyOne(msg)">复制</el-button>
|
||||
</div>
|
||||
<div class="message-content">{{ msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider>历史消息记录</el-divider>
|
||||
|
||||
<div class="history-controls">
|
||||
<span class="history-label">显示条数:</span>
|
||||
<el-select v-model="historyLimit" size="small" style="width: 120px;" @change="loadHistory">
|
||||
<el-option label="10条" :value="10"></el-option>
|
||||
<el-option label="20条" :value="20"></el-option>
|
||||
<el-option label="50条" :value="50"></el-option>
|
||||
<el-option label="100条" :value="100"></el-option>
|
||||
<el-option label="200条" :value="200"></el-option>
|
||||
<el-option label="500条" :value="500"></el-option>
|
||||
<el-option label="1000条" :value="1000"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="history-container">
|
||||
<div class="history-column">
|
||||
<div class="history-header">
|
||||
<span>历史请求(最近 {{ historyLimit }} 条)</span>
|
||||
<el-button size="mini" type="primary" icon="el-icon-refresh" @click="loadHistory" :loading="historyLoading">刷新</el-button>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
<div v-if="requestHistory.length === 0" class="empty-history">
|
||||
<el-empty description="暂无历史请求" :image-size="80" />
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div v-for="(item, idx) in requestHistory" :key="'req-' + idx" class="history-item">
|
||||
<div class="history-item-header">
|
||||
<div class="history-time">{{ extractTime(item) }}</div>
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-document-copy"
|
||||
type="text"
|
||||
@click="copyHistoryItem(item)"
|
||||
title="复制此条消息">
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="history-text">{{ extractMessage(item) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-column">
|
||||
<div class="history-header">
|
||||
<span>历史响应(最近 {{ historyLimit }} 条)</span>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
<div v-if="responseHistory.length === 0" class="empty-history">
|
||||
<el-empty description="暂无历史响应" :image-size="80" />
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div v-for="(item, idx) in responseHistory" :key="'res-' + idx" class="history-item">
|
||||
<div class="history-item-header">
|
||||
|
||||
<div class="history-time">{{ extractTime(item) }}</div>
|
||||
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-document-copy"
|
||||
type="text"
|
||||
@click="copyHistoryItem(item)"
|
||||
title="复制此条消息">
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="history-text">{{ extractMessage(item) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { executeInstruction } from '@/api/system/instruction'
|
||||
import { executeInstruction, getHistory, executeInstructionWithForce } from '@/api/system/instruction'
|
||||
|
||||
export default {
|
||||
name: 'JdInstruction',
|
||||
@@ -46,9 +178,40 @@ export default {
|
||||
return {
|
||||
form: { command: '' },
|
||||
loading: false,
|
||||
resultList: []
|
||||
resultList: [],
|
||||
requestHistory: [],
|
||||
responseHistory: [],
|
||||
historyLoading: false,
|
||||
historyLimit: 50,
|
||||
// 验证码相关
|
||||
verifyDialogVisible: false,
|
||||
verifyCode: '',
|
||||
verifyInput: '',
|
||||
verifyMessage: '',
|
||||
verifyLoading: false,
|
||||
pendingCommand: '' // 待执行的命令(验证通过后执行)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 生成完整消息,用三个空行分隔
|
||||
fullMessage() {
|
||||
if (!this.resultList || this.resultList.length === 0) return ''
|
||||
return this.resultList.join('\n\n\n')
|
||||
},
|
||||
// 检测移动端
|
||||
isMobile() {
|
||||
if (this.$store?.getters?.device === 'mobile') {
|
||||
return true
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadHistory()
|
||||
},
|
||||
methods: {
|
||||
copyOne(text) {
|
||||
if (!text) return
|
||||
@@ -56,7 +219,16 @@ export default {
|
||||
},
|
||||
copyAll() {
|
||||
if (!this.resultList || this.resultList.length === 0) return
|
||||
const text = this.resultList.join('\n\n')
|
||||
const text = this.resultList.join('\n\n\n')
|
||||
this.doCopy(text)
|
||||
},
|
||||
copyHistory(type) {
|
||||
const list = type === 'response' ? this.responseHistory : this.requestHistory
|
||||
if (!list || list.length === 0) {
|
||||
this.$modal.msgWarning('暂无历史记录')
|
||||
return
|
||||
}
|
||||
const text = list.map(item => this.extractMessage(item)).join('\n\n')
|
||||
this.doCopy(text)
|
||||
},
|
||||
doCopy(text) {
|
||||
@@ -89,6 +261,23 @@ export default {
|
||||
if (Array.isArray(data)) this.resultList = data
|
||||
else if (typeof data === 'string') this.resultList = data ? [data] : []
|
||||
else this.resultList = []
|
||||
|
||||
// 调试:打印返回结果
|
||||
console.log('返回结果:', this.resultList)
|
||||
|
||||
// 检查是否是地址重复或订单编号重复错误
|
||||
if (this.checkAddressDuplicate(this.resultList)) {
|
||||
console.log('检测到重复错误,准备显示验证码弹窗')
|
||||
// 显示验证码弹窗
|
||||
this.showVerifyDialog(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有以[炸弹]开头的消息
|
||||
this.checkBombAlert(this.resultList)
|
||||
|
||||
// 执行成功后刷新历史记录
|
||||
this.loadHistory()
|
||||
} else {
|
||||
this.$modal.msgError(res && res.msg ? res.msg : '执行失败')
|
||||
}
|
||||
@@ -97,21 +286,722 @@ export default {
|
||||
this.$modal.msgError('执行失败,请稍后重试')
|
||||
})
|
||||
},
|
||||
fillMenu() {
|
||||
this.form.command = '京菜单'
|
||||
// 检查是否是地址重复或订单编号重复错误
|
||||
checkAddressDuplicate(resultList) {
|
||||
if (!resultList || resultList.length === 0) {
|
||||
console.log('结果列表为空')
|
||||
return false
|
||||
}
|
||||
console.log('检查重复错误,结果列表:', resultList)
|
||||
for (let i = 0; i < resultList.length; i++) {
|
||||
const result = resultList[i]
|
||||
console.log(`检查第${i}个结果:`, result, '类型:', typeof result)
|
||||
if (typeof result === 'string') {
|
||||
// 检查是否包含地址重复或订单编号重复错误码
|
||||
const hasAddressDuplicate = result.includes('ERROR_CODE:ADDRESS_DUPLICATE')
|
||||
const hasOrderNumberDuplicate = result.includes('ERROR_CODE:ORDER_NUMBER_DUPLICATE')
|
||||
if (hasAddressDuplicate || hasOrderNumberDuplicate) {
|
||||
console.log('检测到重复错误:', result, '地址重复:', hasAddressDuplicate, '订单编号重复:', hasOrderNumberDuplicate)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('未检测到重复错误')
|
||||
return false
|
||||
},
|
||||
// 显示验证码弹窗
|
||||
showVerifyDialog(command) {
|
||||
// 生成四位随机数字验证码
|
||||
this.verifyCode = String(Math.floor(1000 + Math.random() * 9000))
|
||||
this.verifyInput = ''
|
||||
// 根据错误类型设置提示信息
|
||||
let hasOrderNumberDuplicate = false
|
||||
let hasAddressDuplicate = false
|
||||
if (this.resultList && this.resultList.length > 0) {
|
||||
for (let i = 0; i < this.resultList.length; i++) {
|
||||
const result = this.resultList[i]
|
||||
if (typeof result === 'string') {
|
||||
if (result.includes('ERROR_CODE:ORDER_NUMBER_DUPLICATE')) {
|
||||
hasOrderNumberDuplicate = true
|
||||
}
|
||||
if (result.includes('ERROR_CODE:ADDRESS_DUPLICATE')) {
|
||||
hasAddressDuplicate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasOrderNumberDuplicate && hasAddressDuplicate) {
|
||||
this.verifyMessage = '检测到订单编号和地址重复,请输入验证码以强制生成表单'
|
||||
} else if (hasOrderNumberDuplicate) {
|
||||
this.verifyMessage = '检测到订单编号重复,请输入验证码以强制生成表单'
|
||||
} else {
|
||||
this.verifyMessage = '检测到地址重复,请输入验证码以强制生成表单'
|
||||
}
|
||||
this.pendingCommand = command
|
||||
this.verifyDialogVisible = true
|
||||
},
|
||||
// 处理验证码验证
|
||||
handleVerify() {
|
||||
if (!this.verifyInput || this.verifyInput.length !== 4) {
|
||||
this.$modal.msgError('请输入四位数字验证码')
|
||||
return
|
||||
}
|
||||
if (this.verifyInput !== this.verifyCode) {
|
||||
this.$modal.msgError('验证码错误,请重新输入')
|
||||
this.verifyInput = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 验证通过,使用forceGenerate参数重新执行
|
||||
this.verifyLoading = true
|
||||
executeInstructionWithForce({ 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.checkBombAlert(this.resultList)
|
||||
|
||||
// 执行成功后刷新历史记录
|
||||
this.loadHistory()
|
||||
this.$modal.msgSuccess('表单已强制生成')
|
||||
} else {
|
||||
this.$modal.msgError(res && res.msg ? res.msg : '执行失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
this.verifyLoading = false
|
||||
this.$modal.msgError('执行失败,请稍后重试')
|
||||
})
|
||||
},
|
||||
loadHistory() {
|
||||
this.historyLoading = true
|
||||
Promise.all([
|
||||
getHistory('request', this.historyLimit),
|
||||
getHistory('response', this.historyLimit)
|
||||
]).then(([reqRes, respRes]) => {
|
||||
this.historyLoading = false
|
||||
if (reqRes && reqRes.code === 200) {
|
||||
this.requestHistory = reqRes.data || []
|
||||
}
|
||||
if (respRes && respRes.code === 200) {
|
||||
this.responseHistory = respRes.data || []
|
||||
}
|
||||
}).catch(() => {
|
||||
this.historyLoading = false
|
||||
this.$modal.msgError('加载历史记录失败')
|
||||
})
|
||||
},
|
||||
extractTime(item) {
|
||||
if (!item) return ''
|
||||
const idx = item.indexOf(' | ')
|
||||
return idx > 0 ? item.substring(0, idx) : ''
|
||||
},
|
||||
extractMessage(item) {
|
||||
if (!item) return ''
|
||||
const idx = item.indexOf(' | ')
|
||||
return idx > 0 ? item.substring(idx + 3) : item
|
||||
},
|
||||
fillTF() {
|
||||
this.form.command = 'TF'
|
||||
},
|
||||
fillSheng() {
|
||||
this.form.command = '生'
|
||||
this.run()
|
||||
},
|
||||
fillFan() {
|
||||
this.form.command = '生\r\nF'
|
||||
this.run()
|
||||
},
|
||||
fillWen() {
|
||||
this.form.command = '生\r\nW'
|
||||
this.run()
|
||||
},
|
||||
fillHong() {
|
||||
this.form.command = '生\r\nH'
|
||||
this.run()
|
||||
},
|
||||
fillPDD() {
|
||||
this.form.command = '拼多多\r\n'
|
||||
this.run()
|
||||
},
|
||||
fillPDDWen() {
|
||||
this.form.command = '拼多多 W\r\n'
|
||||
this.run()
|
||||
},
|
||||
async fillMan() {
|
||||
// 先尝试查询今天的数据
|
||||
this.form.command = '慢单'
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await executeInstruction({ command: '慢单' })
|
||||
this.loading = false
|
||||
|
||||
if (res && (res.code === 200 || res.msg === '操作成功')) {
|
||||
const data = res.data
|
||||
let resultData = []
|
||||
if (Array.isArray(data)) resultData = data
|
||||
else if (typeof data === 'string') resultData = data ? [data] : []
|
||||
else resultData = []
|
||||
|
||||
// 如果今天的数据为空,尝试查询昨天的数据
|
||||
if (resultData.length === 0 || (resultData.length === 1 && resultData[0].includes('无数据'))) {
|
||||
this.$message.info('今天暂无慢单数据,正在查询昨天的数据...')
|
||||
|
||||
// 获取昨天的日期
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0].replace(/-/g, '')
|
||||
|
||||
this.loading = true
|
||||
const yesterdayRes = await executeInstruction({ command: `慢单${yesterdayStr}` })
|
||||
this.loading = false
|
||||
|
||||
if (yesterdayRes && (yesterdayRes.code === 200 || yesterdayRes.msg === '操作成功')) {
|
||||
const yesterdayData = yesterdayRes.data
|
||||
if (Array.isArray(yesterdayData)) this.resultList = yesterdayData
|
||||
else if (typeof yesterdayData === 'string') this.resultList = yesterdayData ? [yesterdayData] : []
|
||||
else this.resultList = []
|
||||
|
||||
if (this.resultList.length > 0) {
|
||||
this.$message.success(`已查询到昨天(${yesterdayStr})的慢单数据`)
|
||||
} else {
|
||||
this.$message.warning('昨天也没有慢单数据')
|
||||
this.resultList = []
|
||||
}
|
||||
} else {
|
||||
this.$message.error('查询昨天数据失败')
|
||||
this.resultList = []
|
||||
}
|
||||
} else {
|
||||
// 今天有数据,直接显示
|
||||
this.resultList = resultData
|
||||
this.$message.success('已查询到今天的慢单数据')
|
||||
}
|
||||
|
||||
// 检查是否有以[炸弹]开头的消息
|
||||
this.checkBombAlert(this.resultList)
|
||||
|
||||
// 执行成功后刷新历史记录
|
||||
this.loadHistory()
|
||||
} else {
|
||||
this.$message.error(res && res.msg ? res.msg : '查询失败')
|
||||
this.resultList = []
|
||||
}
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.$message.error('查询失败,请稍后重试')
|
||||
this.resultList = []
|
||||
}
|
||||
},
|
||||
clearAll() {
|
||||
this.form.command = ''
|
||||
this.resultList = []
|
||||
},
|
||||
checkBombAlert(resultList) {
|
||||
if (!resultList || resultList.length === 0) return
|
||||
|
||||
// 检查是否有以[炸弹]开头的消息
|
||||
const bombMessages = resultList
|
||||
.filter(msg => {
|
||||
return msg && typeof msg === 'string' && msg.trim().startsWith('[炸弹]')
|
||||
})
|
||||
.map(msg => {
|
||||
// 移除所有的[炸弹]标记
|
||||
return msg.trim().replace(/\[炸弹\]\s*/g, '').trim()
|
||||
})
|
||||
|
||||
if (bombMessages.length > 0) {
|
||||
// 显示全屏警告弹窗
|
||||
this.$alert(bombMessages.join('\n\n'), '⚠️ 警告提示', {
|
||||
confirmButtonText: '我已知晓',
|
||||
type: 'warning',
|
||||
center: true,
|
||||
customClass: 'bomb-alert-dialog',
|
||||
showClose: false,
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
dangerouslyUseHTMLString: false
|
||||
}).catch(() => {})
|
||||
}
|
||||
},
|
||||
copyHistoryItem(item) {
|
||||
const message = this.extractMessage(item)
|
||||
if (message) {
|
||||
this.doCopy(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box-card { margin: 20px; }
|
||||
.box-card {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
/* 移动端卡片优化 */
|
||||
@media (max-width: 768px) {
|
||||
.box-card {
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.box-card ::v-deep .el-card__header {
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.box-card ::v-deep .el-card__body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
/* 响应容器 */
|
||||
.response-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.response-section {
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background-color: #F5F7FA;
|
||||
border-bottom: 1px solid #DCDFE6;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 完整消息区域 */
|
||||
.response-content-full {
|
||||
padding: 0;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.response-content-full ::v-deep .el-textarea {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.response-content-full ::v-deep .el-textarea__inner {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
resize: none;
|
||||
min-height: 400px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 消息列表区域 */
|
||||
.response-content-list {
|
||||
padding: 16px;
|
||||
background-color: #FFFFFF;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 消息列表样式 */
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: #F9FAFC;
|
||||
border-left: 3px solid #409EFF;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
background-color: #ECF5FF;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-index {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
.response-content-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.response-content-list::-webkit-scrollbar-track {
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.response-content-list::-webkit-scrollbar-thumb {
|
||||
background: #DCDFE6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.response-content-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #C0C4CC;
|
||||
}
|
||||
|
||||
/* 移动端响应容器优化 */
|
||||
@media (max-width: 768px) {
|
||||
.response-container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.response-content-full ::v-deep .el-textarea__inner {
|
||||
min-height: 250px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.response-content-list {
|
||||
max-height: 400px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-item-header {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-block { margin-bottom: 12px; }
|
||||
.msg-header { display: flex; align-items: center; justify-content: space-between; margin: 6px 0; }
|
||||
|
||||
/* 按钮组样式 */
|
||||
.button-group {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .el-button {
|
||||
margin-right: 0;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.button-group .el-button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 移动端按钮优化 */
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
display: grid !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.button-group .el-button {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
height: 44px; /* 增大触摸目标 */
|
||||
line-height: 1.2;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
flex: none !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* 移动端第一行按钮组(执行、清空、慢单、腾峰)- 每行2个,显示最常用的4个 */
|
||||
.button-group-primary {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 8px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.button-group-primary .el-button {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* 移动端:将腾峰移到第一组,第二组隐藏 */
|
||||
.button-group-secondary {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.button-group-secondary .el-button {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 10px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 确保按钮组独立于form-item */
|
||||
.button-group {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 历史记录控制条 */
|
||||
.history-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: #F5F7FA;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
}
|
||||
|
||||
.history-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 历史消息容器 */
|
||||
.history-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 移动端历史记录优化 */
|
||||
@media (max-width: 768px) {
|
||||
.history-container {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.history-column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.history-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.history-label {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.history-column {
|
||||
flex: 1;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background-color: #F5F7FA;
|
||||
border-bottom: 1px solid #DCDFE6;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #F9FAFC;
|
||||
border-left: 3px solid #409EFF;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: #ECF5FF;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.history-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.history-text {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
.history-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.history-content::-webkit-scrollbar-track {
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.history-content::-webkit-scrollbar-thumb {
|
||||
background: #DCDFE6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.history-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #C0C4CC;
|
||||
}
|
||||
|
||||
.history-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 炸弹警告弹窗样式 */
|
||||
::v-deep .bomb-alert-dialog {
|
||||
width: 80% !important;
|
||||
max-width: 1920px !important;
|
||||
}
|
||||
|
||||
::v-deep .bomb-alert-dialog .el-message-box__message {
|
||||
font-size: 16px !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;
|
||||
}
|
||||
|
||||
::v-deep .bomb-alert-dialog .el-message-box__btns {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
::v-deep .bomb-alert-dialog .el-message-box__btns .el-button {
|
||||
padding: 12px 40px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式:炸弹警告弹窗(不使用scoped,因为弹窗挂载在body下) */
|
||||
.bomb-alert-dialog {
|
||||
width: 80vw !important;
|
||||
max-width: 1920px !important;
|
||||
min-width: 500px !important;
|
||||
}
|
||||
|
||||
.bomb-alert-dialog .el-message-box__header {
|
||||
padding: 20px 20px 15px !important;
|
||||
}
|
||||
|
||||
.bomb-alert-dialog .el-message-box__title {
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.bomb-alert-dialog .el-message-box__message {
|
||||
font-size: 16px !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;
|
||||
}
|
||||
|
||||
.bomb-alert-dialog .el-message-box__btns {
|
||||
text-align: center !important;
|
||||
padding: 15px 20px 20px !important;
|
||||
}
|
||||
|
||||
.bomb-alert-dialog .el-message-box__btns .el-button {
|
||||
padding: 12px 40px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -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,749 @@
|
||||
<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" 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">
|
||||
<template v-if="config.currentProgress <= (form.startRow + 49)">
|
||||
第 {{ form.startRow }} 行
|
||||
</template>
|
||||
<template v-else-if="config.currentProgress > (form.startRow + 100)">
|
||||
第 {{ config.currentProgress - 100 }} 行
|
||||
</template>
|
||||
<template v-else>
|
||||
第 {{ form.startRow }} 行
|
||||
</template>
|
||||
</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>
|
||||
|
||||
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,6 +1,21 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<!-- 顶部搜索区域 -->
|
||||
<div class="search-section">
|
||||
<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="unionId">
|
||||
<el-select v-model="queryParams.unionId" placeholder="请选择京粉账号" clearable style="width: 240px">
|
||||
<el-option
|
||||
@@ -25,13 +40,92 @@
|
||||
<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>
|
||||
<!-- 桌面端搜索按钮 -->
|
||||
<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-card class="statistics-card" shadow="hover" v-if="orderrowsList.length > 0">
|
||||
<div slot="header" class="clearfix">
|
||||
<span><i class="el-icon-data-analysis"></i> 佣金统计</span>
|
||||
<el-button style="float: right; padding: 3px 0" type="text" @click="toggleStatistics">
|
||||
{{ showStatistics ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-show="showStatistics" class="statistics-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总订单数</div>
|
||||
<div class="stat-value">{{ statistics.totalOrders }}</div>
|
||||
</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">
|
||||
<el-col :span="12">
|
||||
<div class="status-stats">
|
||||
<h4>按状态统计</h4>
|
||||
<div class="status-list">
|
||||
<div v-for="(stat, key) in statistics.statusStats" :key="key" class="status-item">
|
||||
<el-tag :type="getStatusTypeByKey(key)" size="small">{{ stat.label }}</el-tag>
|
||||
<span class="status-count">{{ stat.count }}单</span>
|
||||
<span class="status-amount">¥{{ stat.amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="account-stats">
|
||||
<h4>按账号统计</h4>
|
||||
<div class="account-list">
|
||||
<div v-for="(stat, unionId) in statistics.accountStats" :key="unionId" class="account-item">
|
||||
<span class="account-name">{{ getAdminName(unionId) }}</span>
|
||||
<span class="account-count">{{ stat.count }}单</span>
|
||||
<span class="account-amount">¥{{ stat.amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮区域(移动端单独显示) -->
|
||||
<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="['system:orderrows:add']">新增</el-button>
|
||||
</el-col>
|
||||
@@ -46,7 +140,10 @@
|
||||
</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">
|
||||
@@ -56,7 +153,7 @@
|
||||
</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">
|
||||
<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>
|
||||
@@ -102,8 +199,12 @@
|
||||
</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>
|
||||
@@ -217,11 +318,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData } from "@/api/system/orderrows";
|
||||
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData, getOrderStatistics } from "@/api/system/orderrows";
|
||||
import { getAdminSelectData } from "@/api/system/superadmin";
|
||||
import { mapGetters } from 'vuex'
|
||||
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||
import MobileButtonGroup from '@/components/MobileButtonGroup'
|
||||
|
||||
export default {
|
||||
name: "Orderrows",
|
||||
components: {
|
||||
MobileSearchForm,
|
||||
MobileButtonGroup
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
@@ -252,7 +360,9 @@ export default {
|
||||
orderId: null,
|
||||
skuName: null,
|
||||
validCode: null,
|
||||
statusGroup: null // 新增
|
||||
statusGroup: null, // 新增
|
||||
orderBy: null, // 排序字段
|
||||
orderSort: null // 排序方向:asc/desc
|
||||
},
|
||||
// 管理员列表
|
||||
adminList: [],
|
||||
@@ -263,14 +373,61 @@ export default {
|
||||
// 查看详情对话框
|
||||
viewDialogVisible: false,
|
||||
// 当前查看的订单
|
||||
currentOrder: {}
|
||||
currentOrder: {},
|
||||
// 统计相关
|
||||
showStatistics: true,
|
||||
statistics: {
|
||||
totalOrders: 0,
|
||||
totalCosPrice: 0,
|
||||
totalEstimateFee: 0,
|
||||
totalActualFee: 0,
|
||||
statusStats: {},
|
||||
accountStats: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
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() {
|
||||
this.getList();
|
||||
this.getAdminList();
|
||||
this.getStatusList();
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询京粉订单列表 */
|
||||
getList() {
|
||||
@@ -279,12 +436,106 @@ export default {
|
||||
this.orderrowsList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
// 调用后端统计接口计算统计数据
|
||||
this.loadStatistics();
|
||||
}).catch(error => {
|
||||
console.error('获取订单列表失败:', error);
|
||||
this.loading = false;
|
||||
this.$message.error('获取订单列表失败');
|
||||
});
|
||||
},
|
||||
/** 加载统计数据(从后端获取) */
|
||||
loadStatistics() {
|
||||
// 构建统计查询参数,使用与列表查询相同的条件
|
||||
const statParams = {
|
||||
...this.queryParams,
|
||||
beginTime: this.dateRange && this.dateRange.length > 0 ? this.dateRange[0] : null,
|
||||
endTime: this.dateRange && this.dateRange.length > 1 ? this.dateRange[1] : null
|
||||
};
|
||||
|
||||
getOrderStatistics(statParams).then(response => {
|
||||
const data = response.data || {};
|
||||
const groupStats = data.groupStats || {};
|
||||
|
||||
// 转换后端返回的统计格式为前端需要的格式
|
||||
this.statistics = {
|
||||
totalOrders: data.totalOrders || 0,
|
||||
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)
|
||||
},
|
||||
accountStats: {} // 后端没有按账号统计,保留为空或后续添加
|
||||
};
|
||||
|
||||
// 如果有订单列表,计算总计佣金额和按账号统计(这些前端计算更快)
|
||||
if (this.orderrowsList.length > 0) {
|
||||
let totalCosPrice = 0;
|
||||
const accountStats = {};
|
||||
|
||||
this.orderrowsList.forEach(order => {
|
||||
// 总计佣金额
|
||||
if (order.estimateCosPrice) {
|
||||
totalCosPrice += parseFloat(order.estimateCosPrice) || 0;
|
||||
}
|
||||
|
||||
// 按账号统计
|
||||
const unionId = order.unionId;
|
||||
if (!accountStats[unionId]) {
|
||||
accountStats[unionId] = {
|
||||
count: 0,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
accountStats[unionId].count++;
|
||||
|
||||
// 计算账号佣金金额(使用与后端相同的逻辑)
|
||||
const validCode = String(order.validCode);
|
||||
const isCancel = validCode === '3';
|
||||
const isIllegal = ['25', '26', '27', '28'].includes(validCode);
|
||||
|
||||
let commissionAmount = parseFloat(order.actualFee) || 0;
|
||||
if (isIllegal && order.estimateCosPrice && order.commissionRate) {
|
||||
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
||||
} else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0)
|
||||
&& order.estimateCosPrice && order.commissionRate) {
|
||||
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
||||
}
|
||||
|
||||
accountStats[unionId].amount += commissionAmount;
|
||||
});
|
||||
|
||||
this.statistics.totalCosPrice = totalCosPrice;
|
||||
this.statistics.accountStats = accountStats;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取统计数据失败:', error);
|
||||
// 如果后端统计失败,回退到前端计算
|
||||
this.calculateStatistics();
|
||||
});
|
||||
},
|
||||
/** 转换后端分组统计格式 */
|
||||
convertGroupStat(groupStat) {
|
||||
if (!groupStat) {
|
||||
return {
|
||||
label: '',
|
||||
count: 0,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: groupStat.label || '',
|
||||
count: groupStat.count || 0,
|
||||
amount: groupStat.actualFee || 0 // 使用actualFee作为金额
|
||||
};
|
||||
},
|
||||
/** 获取管理员列表 */
|
||||
getAdminList() {
|
||||
getAdminSelectData().then(response => {
|
||||
@@ -446,6 +697,7 @@ export default {
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
console.log(this.queryParams.validCode);
|
||||
|
||||
// 合并项转为原始code数组
|
||||
if (this.queryParams.statusGroup) {
|
||||
this.queryParams.validCodes = this.statusValueMap[this.queryParams.statusGroup].map(code => Number(code));
|
||||
@@ -467,7 +719,31 @@ export default {
|
||||
this.dateRange = [];
|
||||
this.resetForm("queryForm");
|
||||
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) {
|
||||
@@ -531,7 +807,381 @@ export default {
|
||||
this.download('/jarvis/orderrows/export', {
|
||||
...this.queryParams
|
||||
}, `京粉订单数据_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
/** 切换统计显示 */
|
||||
toggleStatistics() {
|
||||
this.showStatistics = !this.showStatistics;
|
||||
},
|
||||
/** 计算统计数据 */
|
||||
calculateStatistics() {
|
||||
const stats = {
|
||||
totalOrders: this.orderrowsList.length,
|
||||
totalCosPrice: 0,
|
||||
totalEstimateFee: 0,
|
||||
totalActualFee: 0,
|
||||
statusStats: {},
|
||||
accountStats: {}
|
||||
};
|
||||
|
||||
// 状态分组映射
|
||||
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,
|
||||
amount: 0
|
||||
};
|
||||
});
|
||||
|
||||
// 遍历订单数据计算统计
|
||||
this.orderrowsList.forEach(order => {
|
||||
// 总计佣金额
|
||||
if (order.estimateCosPrice) {
|
||||
stats.totalCosPrice += parseFloat(order.estimateCosPrice) || 0;
|
||||
}
|
||||
// 预估佣金
|
||||
if (order.estimateFee) {
|
||||
stats.totalEstimateFee += parseFloat(order.estimateFee) || 0;
|
||||
}
|
||||
|
||||
// 计算实际佣金或预估佣金
|
||||
// 对于违规订单(25,26,27,28),始终使用 estimateCosPrice * commissionRate / 100 计算
|
||||
// 对于取消订单(3),如果actualFee为空或0,则通过公式计算
|
||||
const validCode = String(order.validCode);
|
||||
const isCancel = validCode === '3'; // 取消订单
|
||||
const isIllegal = ['25', '26', '27', '28'].includes(validCode); // 违规订单
|
||||
|
||||
let commissionAmount = parseFloat(order.actualFee) || 0;
|
||||
const estimateCosPrice = parseFloat(order.estimateCosPrice) || 0;
|
||||
const commissionRate = parseFloat(order.commissionRate) || 0;
|
||||
|
||||
// 违规订单始终使用公式计算佣金
|
||||
if (isIllegal && estimateCosPrice > 0 && commissionRate > 0) {
|
||||
commissionAmount = estimateCosPrice * commissionRate / 100;
|
||||
}
|
||||
// 取消订单:如果actualFee为空或0,则通过公式计算
|
||||
else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0) && estimateCosPrice > 0 && commissionRate > 0) {
|
||||
commissionAmount = estimateCosPrice * commissionRate / 100;
|
||||
}
|
||||
|
||||
// 实际佣金累计(包含计算出的违规和取消订单佣金)
|
||||
stats.totalActualFee += commissionAmount;
|
||||
|
||||
// 按状态统计
|
||||
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].amount += commissionAmount;
|
||||
|
||||
// 按账号统计
|
||||
const unionId = order.unionId;
|
||||
if (!stats.accountStats[unionId]) {
|
||||
stats.accountStats[unionId] = {
|
||||
count: 0,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
stats.accountStats[unionId].count++;
|
||||
stats.accountStats[unionId].amount += commissionAmount;
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<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-stats, .account-stats {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-stats h4, .account-stats h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #2c3e50;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-list, .account-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status-item, .account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.status-item:last-child, .account-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-count, .account-count {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #e9ecef;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-amount, .account-amount {
|
||||
font-weight: 600;
|
||||
color: #27ae60;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.account-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.status-list::-webkit-scrollbar, .account-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.status-list::-webkit-scrollbar-track, .account-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-list::-webkit-scrollbar-thumb, .account-list::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-list::-webkit-scrollbar-thumb:hover, .account-list::-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,6 +1,21 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||
<div>
|
||||
<list-layout>
|
||||
<!-- 搜索区域 -->
|
||||
<template #search>
|
||||
<mobile-search-form
|
||||
:model="queryParams"
|
||||
@search="handleQuery"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<template #form="{ expanded }">
|
||||
<el-form
|
||||
:model="queryParams"
|
||||
ref="queryForm"
|
||||
size="small"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input
|
||||
v-model="queryParams.roleName"
|
||||
@@ -45,13 +60,25 @@
|
||||
end-placeholder="结束日期"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<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">
|
||||
<!-- 操作按钮区域(移动端单独显示) -->
|
||||
<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"
|
||||
@@ -96,7 +123,10 @@
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<template #table>
|
||||
<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" />
|
||||
@@ -146,14 +176,19 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #pagination>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
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>
|
||||
@@ -254,9 +289,18 @@
|
||||
<script>
|
||||
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus, deptTreeSelect } from "@/api/system/role"
|
||||
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 {
|
||||
name: "Role",
|
||||
components: {
|
||||
ListLayout,
|
||||
MobileSearchForm,
|
||||
MobileButtonGroup
|
||||
},
|
||||
dicts: ['sys_normal_disable'],
|
||||
data() {
|
||||
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() {
|
||||
this.getList()
|
||||
},
|
||||
@@ -603,3 +667,32 @@ export default {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div>
|
||||
<list-layout>
|
||||
<!-- 搜索区域 -->
|
||||
<template #search>
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="微信ID" prop="wxid">
|
||||
<el-input v-model="queryParams.wxid" placeholder="请输入微信ID" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||
@@ -43,7 +46,10 @@
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<template #table>
|
||||
<el-table v-loading="loading" :data="superadminList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="微信ID" align="center" prop="wxid" min-width="120" />
|
||||
@@ -69,6 +75,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收人" align="center" prop="touser" min-width="200" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createdAt" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createdAt) }}</span>
|
||||
@@ -87,8 +94,13 @@
|
||||
</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>
|
||||
@@ -120,6 +132,12 @@
|
||||
<el-radio :label="0">不参与统计</el-radio>
|
||||
</el-radio-group>
|
||||
</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>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
@@ -146,6 +164,7 @@
|
||||
{{ currentAdmin.isCount === 1 ? '参与统计' : '不参与统计' }}
|
||||
</el-tag>
|
||||
</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.updatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -158,9 +177,13 @@
|
||||
|
||||
<script>
|
||||
import { listSuperadmin, getSuperadmin, delSuperadmin, addSuperadmin, updateSuperadmin } from "@/api/system/superadmin";
|
||||
import ListLayout from "@/components/ListLayout";
|
||||
|
||||
export default {
|
||||
name: "Superadmin",
|
||||
components: {
|
||||
ListLayout
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
@@ -240,7 +263,8 @@ export default {
|
||||
appKey: null,
|
||||
secretKey: null,
|
||||
isActive: 1,
|
||||
isCount: 1
|
||||
isCount: 1,
|
||||
touser: null
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
|
||||
86
update-list-pages.md
Normal file
86
update-list-pages.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 全局列表页面固定分页布局更新指南
|
||||
|
||||
## 已完成的页面
|
||||
- ✅ 京粉订单列表 (`src/views/system/orderrows/index.vue`) - 已使用自定义布局
|
||||
- ✅ 角色管理 (`src/views/system/role/index.vue`) - 已使用ListLayout组件
|
||||
- ✅ 超级管理员 (`src/views/system/superadmin/index.vue`) - 已使用ListLayout组件
|
||||
|
||||
## 待更新的页面列表
|
||||
以下是项目中其他需要更新为固定分页布局的列表页面:
|
||||
|
||||
### 系统管理模块
|
||||
- `src/views/system/user/index.vue` - 用户管理(特殊布局,需要单独处理)
|
||||
- `src/views/system/post/index.vue` - 岗位管理
|
||||
- `src/views/system/notice/index.vue` - 通知公告
|
||||
- `src/views/system/dict/index.vue` - 字典管理
|
||||
- `src/views/system/dict/data.vue` - 字典数据
|
||||
- `src/views/system/config/index.vue` - 参数设置
|
||||
|
||||
### 监控模块
|
||||
- `src/views/monitor/operlog/index.vue` - 操作日志
|
||||
- `src/views/monitor/job/log.vue` - 调度日志
|
||||
- `src/views/monitor/logininfor/index.vue` - 登录日志
|
||||
- `src/views/monitor/job/index.vue` - 定时任务
|
||||
- `src/views/monitor/online/index.vue` - 在线用户
|
||||
|
||||
### 工具模块
|
||||
- `src/views/tool/gen/index.vue` - 代码生成
|
||||
|
||||
### 业务模块
|
||||
- `src/views/system/jdorder/orderList.vue` - 京东订单列表
|
||||
- `src/views/system/xbmessage/index.vue` - 消息管理
|
||||
- `src/views/system/favoriteProduct/index.vue` - 收藏商品
|
||||
- `src/views/system/xbgroup/index.vue` - 群组管理
|
||||
|
||||
## 更新步骤
|
||||
|
||||
### 方法一:使用ListLayout组件(推荐)
|
||||
1. 在页面中导入ListLayout组件:
|
||||
```javascript
|
||||
import ListLayout from "@/components/ListLayout";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ListLayout
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. 将页面结构改为:
|
||||
```vue
|
||||
<template>
|
||||
<list-layout>
|
||||
<!-- 搜索区域 -->
|
||||
<template #search>
|
||||
<!-- 原来的搜索表单和操作按钮 -->
|
||||
</template>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<template #table>
|
||||
<!-- 原来的el-table -->
|
||||
</template>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #pagination>
|
||||
<!-- 原来的pagination组件 -->
|
||||
</template>
|
||||
</list-layout>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 方法二:直接修改CSS(适用于特殊布局)
|
||||
参考 `src/views/system/orderrows/index.vue` 的实现方式,直接修改页面样式。
|
||||
|
||||
## 注意事项
|
||||
1. 用户管理页面使用了splitpanes布局,需要特殊处理
|
||||
2. 确保所有页面都保持原有的功能不变
|
||||
3. 测试分页导航在不同数据量下的表现
|
||||
4. 保持响应式设计在移动端的良好表现
|
||||
|
||||
## 测试要点
|
||||
- [ ] 分页导航固定在页面底部
|
||||
- [ ] 表格内容可以独立滚动
|
||||
- [ ] 搜索区域固定在顶部
|
||||
- [ ] 移动端显示正常
|
||||
- [ ] 所有原有功能正常工作
|
||||
@@ -9,7 +9,12 @@ const CompressionPlugin = require('compression-webpack-plugin')
|
||||
|
||||
const name = process.env.VUE_APP_TITLE || 'Jarvis' // 网页标题
|
||||
|
||||
const baseUrl = process.env.VUE_APP_BASE_API || 'http://127.0.0.1:30313' // 后端接口
|
||||
// 后端接口地址(仅用于开发环境代理)
|
||||
// 生产环境应该使用相对路径(如 /dev-api),通过nginx代理
|
||||
// 开发环境可以使用相对路径(通过devServer代理)或绝对URL
|
||||
const baseUrl = process.env.VUE_APP_BASE_API || (process.env.NODE_ENV === 'production' ? '/dev-api' : 'http://127.0.0.1:30313')
|
||||
// 开发环境代理路径(如果未设置VUE_APP_BASE_API,使用默认代理路径)
|
||||
const devApiPath = process.env.VUE_APP_BASE_API || '/dev-api'
|
||||
|
||||
const port = process.env.port || process.env.npm_config_port || 80 // 端口
|
||||
|
||||
@@ -35,16 +40,18 @@ module.exports = {
|
||||
open: true,
|
||||
proxy: {
|
||||
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||
[process.env.VUE_APP_BASE_API]: {
|
||||
target: baseUrl,
|
||||
// 如果VUE_APP_BASE_API是相对路径(如/dev-api),则使用代理
|
||||
// 如果是绝对URL,则直接使用该URL,不配置代理
|
||||
[devApiPath]: {
|
||||
target: baseUrl.startsWith('http') ? baseUrl : 'http://127.0.0.1:30313',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
['^' + process.env.VUE_APP_BASE_API]: ''
|
||||
['^' + devApiPath]: ''
|
||||
}
|
||||
},
|
||||
// springdoc proxy
|
||||
'^/v3/api-docs/(.*)': {
|
||||
target: baseUrl,
|
||||
target: baseUrl.startsWith('http') ? baseUrl : 'http://127.0.0.1:30313',
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
|
||||
56
修复环境变量.txt
Normal file
56
修复环境变量.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
请手动修改以下两个文件:
|
||||
|
||||
===========================================
|
||||
文件1:.env.development
|
||||
===========================================
|
||||
将以下内容:
|
||||
VUE_APP_BASE_API = 'http://134.175.126.60:30313'
|
||||
|
||||
改为:
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
|
||||
完整文件内容应该是:
|
||||
# 页面标题
|
||||
VUE_APP_TITLE=Jarvis
|
||||
|
||||
# 开发环境配置
|
||||
ENV=development
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES=true
|
||||
|
||||
# 开发环境使用代理路径,通过vue.config.js的devServer代理到后端
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
|
||||
port=8888
|
||||
|
||||
===========================================
|
||||
文件2:.env.production
|
||||
===========================================
|
||||
将以下内容:
|
||||
VUE_APP_BASE_API = '/jarvis-api'
|
||||
|
||||
改为:
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
|
||||
完整文件内容应该是:
|
||||
# 页面标题
|
||||
VUE_APP_TITLE=Jarvis
|
||||
|
||||
# 生产环境配置
|
||||
ENV=production
|
||||
|
||||
# 生产环境使用相对路径,通过nginx代理到后端
|
||||
# 注意:这里的路径必须与nginx配置中的 location /dev-api/ 保持一致
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
|
||||
port=8888
|
||||
|
||||
===========================================
|
||||
重要提示:
|
||||
===========================================
|
||||
1. 注意等号两边不要有空格(VUE_APP_BASE_API=/dev-api,不是 VUE_APP_BASE_API = /dev-api)
|
||||
2. 不要使用引号(直接写 /dev-api,不要写 '/dev-api' 或 "/dev-api")
|
||||
3. 修改后必须重新打包:npm run build:prod
|
||||
4. 确保nginx配置中的 location /dev-api/ 与前端配置一致
|
||||
|
||||
20
创建环境变量文件.bat
Normal file
20
创建环境变量文件.bat
Normal file
@@ -0,0 +1,20 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo 正在创建环境变量配置文件...
|
||||
|
||||
echo # 开发环境配置 > .env.development
|
||||
echo # 开发环境使用代理路径,通过vue.config.js的devServer代理到后端 >> .env.development
|
||||
echo VUE_APP_BASE_API=/dev-api >> .env.development
|
||||
|
||||
echo # 生产环境配置 > .env.production
|
||||
echo # 生产环境使用相对路径,通过nginx代理到后端 >> .env.production
|
||||
echo VUE_APP_BASE_API=/dev-api >> .env.production
|
||||
|
||||
echo.
|
||||
echo 环境变量文件创建完成!
|
||||
echo .env.development - 开发环境配置
|
||||
echo .env.production - 生产环境配置
|
||||
echo.
|
||||
echo 请重新打包项目:npm run build:prod
|
||||
pause
|
||||
|
||||
104
快速修复指南.md
Normal file
104
快速修复指南.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 快速修复:HTTPS访问后端接口问题
|
||||
|
||||
## 问题
|
||||
访问 `https://jarvis.van333.cn` 时,前端请求仍然是 `http://134.175.126.60:30313/captchaImage`
|
||||
|
||||
## 立即执行以下步骤
|
||||
|
||||
### 步骤1:创建环境变量文件
|
||||
|
||||
在项目根目录 `d:\code\ruoyi-vue\` 下,创建两个文件:
|
||||
|
||||
#### 文件1:`.env.development`
|
||||
内容:
|
||||
```
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
```
|
||||
|
||||
#### 文件2:`.env.production`
|
||||
内容:
|
||||
```
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
```
|
||||
|
||||
**创建方法(Windows):**
|
||||
1. 在项目根目录打开命令行
|
||||
2. 执行:
|
||||
```cmd
|
||||
echo VUE_APP_BASE_API=/dev-api > .env.development
|
||||
echo VUE_APP_BASE_API=/dev-api > .env.production
|
||||
```
|
||||
|
||||
或者手动创建:
|
||||
1. 在项目根目录新建文件 `.env.development`
|
||||
2. 在项目根目录新建文件 `.env.production`
|
||||
3. 两个文件内容都是:`VUE_APP_BASE_API=/dev-api`
|
||||
|
||||
### 步骤2:删除旧的打包文件(如果存在)
|
||||
|
||||
```cmd
|
||||
cd d:\code\ruoyi-vue
|
||||
rmdir /s /q dist
|
||||
```
|
||||
|
||||
### 步骤3:重新打包
|
||||
|
||||
```cmd
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
### 步骤4:部署到服务器
|
||||
|
||||
将 `dist` 目录中的所有文件复制到服务器的 `/www/sites/jarvis.van333.cn/index/` 目录
|
||||
|
||||
### 步骤5:确认Nginx配置
|
||||
|
||||
确保nginx配置文件中包含:
|
||||
|
||||
```nginx
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤6:重启Nginx
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
### 步骤7:清除浏览器缓存并测试
|
||||
|
||||
1. 按 `Ctrl + Shift + Delete` 清除浏览器缓存
|
||||
2. 或者按 `Ctrl + F5` 强制刷新
|
||||
3. 打开开发者工具(F12)查看Network请求
|
||||
4. 确认请求URL是 `https://jarvis.van333.cn/dev-api/captchaImage`
|
||||
|
||||
## 验证配置是否正确
|
||||
|
||||
打包完成后,检查打包后的文件:
|
||||
|
||||
```cmd
|
||||
cd d:\code\ruoyi-vue\dist\static\js
|
||||
findstr /s /i "134.175.126.60" *.js
|
||||
```
|
||||
|
||||
如果找到包含该IP的文件,说明环境变量未生效,需要:
|
||||
1. 确认 `.env.production` 文件存在且内容正确
|
||||
2. 删除 `dist` 目录后重新打包
|
||||
3. 确认打包命令是 `npm run build:prod`(不是 `npm run build`)
|
||||
|
||||
## 如果还是不行
|
||||
|
||||
检查 `vue.config.js` 第15行,应该是:
|
||||
```javascript
|
||||
const baseUrl = process.env.VUE_APP_BASE_API || (process.env.NODE_ENV === 'production' ? '/dev-api' : 'http://127.0.0.1:30313')
|
||||
```
|
||||
|
||||
如果不对,说明配置未更新,需要重新保存 `vue.config.js` 文件。
|
||||
|
||||
294
移动端适配说明.md
Normal file
294
移动端适配说明.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 移动端适配说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已全面适配移动端,提供了完整的移动端体验优化,包括:
|
||||
|
||||
1. **移动端专用组件**:卡片式列表、折叠搜索表单、底部导航等
|
||||
2. **响应式布局**:自动适配不同屏幕尺寸
|
||||
3. **触摸优化**:符合移动端交互规范
|
||||
4. **性能优化**:流畅的滚动和动画效果
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. MobileTable - 移动端表格组件
|
||||
|
||||
移动端自动显示为卡片式列表,桌面端显示为表格。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<mobile-table
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:show-selection="true"
|
||||
:show-actions="true"
|
||||
:selected-rows="selectedRows"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
@action="handleAction"
|
||||
>
|
||||
<!-- 自定义卡片头部 -->
|
||||
<template #header="{ row }">
|
||||
{{ row.name }}
|
||||
</template>
|
||||
|
||||
<!-- 自定义列内容 -->
|
||||
<template #column-status="{ row, value }">
|
||||
<el-tag :type="value === '0' ? 'success' : 'danger'">
|
||||
{{ value === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<!-- 自定义操作按钮 -->
|
||||
<template #actions="{ row }">
|
||||
<el-dropdown-item command="edit" icon="el-icon-edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
|
||||
</template>
|
||||
</mobile-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
columns: [
|
||||
{ prop: 'name', label: '名称', mobile: true },
|
||||
{ prop: 'status', label: '状态', dictType: 'sys_normal_disable' },
|
||||
{ prop: 'createTime', label: '创建时间', formatter: this.parseTime }
|
||||
],
|
||||
selectedRows: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelectionChange(row, selected) {
|
||||
// 处理选择变化
|
||||
},
|
||||
handleRowClick(row) {
|
||||
// 处理行点击
|
||||
},
|
||||
handleAction(command, row) {
|
||||
// 处理操作
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. MobileSearchForm - 移动端搜索表单
|
||||
|
||||
移动端自动折叠为快速搜索,点击筛选按钮展开完整表单。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<mobile-search-form
|
||||
:model="queryParams"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@quick-search="handleQuickSearch"
|
||||
>
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择">
|
||||
<el-option label="正常" value="0" />
|
||||
<el-option label="停用" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</mobile-search-form>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. MobileButtonGroup - 移动端按钮组
|
||||
|
||||
移动端自动将按钮分组,主要按钮显示,其他按钮放入"更多"菜单。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<mobile-button-group
|
||||
:buttons="buttons"
|
||||
:primary-count="2"
|
||||
:sticky="true"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
buttons: [
|
||||
{ key: 'add', label: '新增', type: 'primary', icon: 'el-icon-plus', handler: this.handleAdd },
|
||||
{ key: 'edit', label: '修改', type: 'success', icon: 'el-icon-edit', handler: this.handleEdit },
|
||||
{ key: 'delete', label: '删除', type: 'danger', icon: 'el-icon-delete', handler: this.handleDelete },
|
||||
{ key: 'export', label: '导出', type: 'warning', icon: 'el-icon-download', handler: this.handleExport }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4. MobileBottomNav - 移动端底部导航
|
||||
|
||||
在移动端显示底部导航栏,方便快速切换页面。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<mobile-bottom-nav
|
||||
:items="navItems"
|
||||
:show="true"
|
||||
@nav-click="handleNavClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
navItems: [
|
||||
{ path: '/index', label: '首页', icon: 'el-icon-s-home' },
|
||||
{ path: '/system/user', label: '用户', icon: 'el-icon-user' },
|
||||
{ path: '/system/menu', label: '菜单', icon: 'el-icon-menu' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 工具函数
|
||||
|
||||
### mobile.js
|
||||
|
||||
提供移动端检测和优化工具函数:
|
||||
|
||||
```javascript
|
||||
import { isMobile, isIOS, initMobile } from '@/utils/mobile'
|
||||
|
||||
// 检测是否为移动端
|
||||
if (isMobile()) {
|
||||
// 移动端逻辑
|
||||
}
|
||||
|
||||
// 初始化移动端优化
|
||||
initMobile()
|
||||
```
|
||||
|
||||
## 混入
|
||||
|
||||
### mobile.js mixin
|
||||
|
||||
提供移动端相关的计算属性和方法:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import mobileMixin from '@/mixins/mobile'
|
||||
|
||||
export default {
|
||||
mixins: [mobileMixin],
|
||||
methods: {
|
||||
someMethod() {
|
||||
// 使用移动端检测
|
||||
if (this.$isMobile) {
|
||||
// 移动端逻辑
|
||||
}
|
||||
|
||||
// 使用移动端提示
|
||||
this.$mobileToast('操作成功', 'success')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 样式优化
|
||||
|
||||
### 响应式断点
|
||||
|
||||
- **移动端**:`max-width: 768px`
|
||||
- **平板**:`769px - 1024px`
|
||||
- **桌面端**:`min-width: 1025px`
|
||||
|
||||
### 主要优化
|
||||
|
||||
1. **触摸目标**:所有可点击元素最小 44px × 44px
|
||||
2. **字体大小**:输入框字体 16px 防止 iOS 自动缩放
|
||||
3. **间距优化**:移动端使用更紧凑的间距
|
||||
4. **圆角优化**:使用更大的圆角值(8px-12px)
|
||||
5. **阴影优化**:使用更柔和的阴影效果
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 完整列表页面示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索表单 -->
|
||||
<mobile-search-form
|
||||
:model="queryParams"
|
||||
@search="getList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" />
|
||||
</el-form-item>
|
||||
</mobile-search-form>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<mobile-button-group
|
||||
:buttons="buttons"
|
||||
:primary-count="2"
|
||||
/>
|
||||
|
||||
<!-- 表格/卡片列表 -->
|
||||
<mobile-table
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:show-selection="true"
|
||||
:selected-rows="selectedRows"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #column-status="{ value }">
|
||||
<dict-tag :options="dict.type.sys_normal_disable" :value="value" />
|
||||
</template>
|
||||
</mobile-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **组件注册**:移动端组件需要在使用前注册或导入
|
||||
2. **字典数据**:MobileTable 组件需要字典数据支持,确保字典已加载
|
||||
3. **路由配置**:底部导航需要配置正确的路由路径
|
||||
4. **性能优化**:大数据量时建议使用虚拟滚动或分页加载
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
- iOS Safari 12+
|
||||
- Android Chrome 70+
|
||||
- 微信内置浏览器
|
||||
- 其他现代移动浏览器
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- 初始版本
|
||||
- 支持移动端卡片式列表
|
||||
- 支持移动端折叠搜索
|
||||
- 支持移动端底部导航
|
||||
- 完整的响应式适配
|
||||
|
||||
89
配置说明.md
Normal file
89
配置说明.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 解决HTTPS访问后端接口问题
|
||||
|
||||
## 问题现象
|
||||
访问 `https://jarvis.van333.cn` 时,前端请求仍然是 `http://134.175.126.60:30313/captchaImage`,导致混合内容错误。
|
||||
|
||||
## 原因分析
|
||||
1. **缺少环境变量配置文件**:`.env.production` 文件不存在,打包时使用了默认值
|
||||
2. **打包后的代码包含硬编码IP**:如果之前打包时使用了IP地址,需要重新打包
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 1. 创建环境变量文件
|
||||
|
||||
请在项目根目录手动创建以下两个文件:
|
||||
|
||||
#### `.env.development`(开发环境)
|
||||
```
|
||||
# 开发环境配置
|
||||
# 开发环境使用代理路径,通过vue.config.js的devServer代理到后端
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
```
|
||||
|
||||
#### `.env.production`(生产环境)
|
||||
```
|
||||
# 生产环境配置
|
||||
# 生产环境使用相对路径,通过nginx代理到后端
|
||||
VUE_APP_BASE_API=/dev-api
|
||||
```
|
||||
|
||||
**或者运行批处理文件:**
|
||||
```
|
||||
双击运行:创建环境变量文件.bat
|
||||
```
|
||||
|
||||
### 2. 重新打包项目
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
### 3. 部署到服务器
|
||||
|
||||
将 `dist` 目录中的文件部署到 `/www/sites/jarvis.van333.cn/index/`
|
||||
|
||||
### 4. 配置Nginx
|
||||
|
||||
确保使用 `nginx-https.conf` 中的配置,关键部分:
|
||||
|
||||
```nginx
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 重启Nginx
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
1. **清除浏览器缓存**,强制刷新(Ctrl+F5)
|
||||
2. **打开开发者工具**(F12),查看Network标签
|
||||
3. **确认API请求**应该是:
|
||||
- ✅ `https://jarvis.van333.cn/dev-api/captchaImage`
|
||||
- ❌ 不是 `http://134.175.126.60:30313/captchaImage`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **必须重新打包**:修改环境变量后,必须重新运行 `npm run build:prod`
|
||||
2. **清除浏览器缓存**:旧的打包文件可能被浏览器缓存
|
||||
3. **检查nginx配置**:确保 `/dev-api/` 路径的代理配置正确
|
||||
4. **检查后端服务**:确保后端服务运行在 `127.0.0.1:30313`
|
||||
|
||||
## 如果仍然有问题
|
||||
|
||||
1. 检查打包后的 `dist/static/js/` 目录中的JS文件,搜索是否包含 `134.175.126.60`
|
||||
2. 如果找到,说明打包时环境变量未生效,检查:
|
||||
- `.env.production` 文件是否存在
|
||||
- 文件内容是否正确(`VUE_APP_BASE_API=/dev-api`)
|
||||
- 是否在正确的目录(项目根目录)
|
||||
- 重新打包前是否删除了旧的 `dist` 目录
|
||||
|
||||
Reference in New Issue
Block a user