37 Commits

Author SHA1 Message Date
shiran 8c49c74b72 Merge pull request 'master' (#17) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m34s
Build and Deploy Vue3 / deploy (push) Successful in 1m57s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#17
2026-01-06 21:58:14 +08:00
lin 1655d86f6b fix:修改退款对接参数
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 1m28s
2026-01-05 15:28:32 +08:00
lin fcebebd216 feate:添加退款接口
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Failing after 1m41s
2026-01-05 15:19:48 +08:00
shiran f8cac7e976 Merge pull request 'master' (#16) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m20s
Build and Deploy Vue3 / deploy (push) Successful in 1m52s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#16
2026-01-01 03:30:09 +08:00
lin 5a93f4f8a8 feate:修改打包环境
Build and Deploy Vue3 / build (push) Successful in 5m30s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s
2025-12-31 19:16:52 +08:00
lin 4d10deef86 feate:对接删除队伍接口
Build and Deploy Vue3 / build (push) Successful in 5m22s
Build and Deploy Vue3 / deploy (push) Successful in 3m12s
2025-12-31 19:07:19 +08:00
lin cf7ac515f6 fix:修改备注弄成添加字段json
Build and Deploy Vue3 / build (push) Successful in 1m7s
Build and Deploy Vue3 / deploy (push) Successful in 3m31s
2025-12-31 18:52:30 +08:00
lin 4ef208a662 fix:队伍名称修改对接
Build and Deploy Vue3 / build (push) Successful in 1m9s
Build and Deploy Vue3 / deploy (push) Successful in 10m59s
2025-12-31 15:04:45 +08:00
lin f6dcec75d7 feate:添加拼单类型接口
Build and Deploy Vue3 / build (push) Successful in 1m6s
Build and Deploy Vue3 / deploy (push) Successful in 10m44s
2025-12-31 13:07:05 +08:00
lin 4cc684eca6 Merge branch 'master' of https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc
Build and Deploy Vue3 / build (push) Successful in 1m29s
Build and Deploy Vue3 / deploy (push) Successful in 6m21s
2025-12-30 14:22:52 +08:00
lin 00ea1845a7 添加拼团活动 2025-12-30 14:22:44 +08:00
shiran deebef26dd Merge pull request 'master' (#15) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m30s
Build and Deploy Vue3 / deploy (push) Successful in 2m12s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#15
2025-12-20 15:47:29 +08:00
wlkjyy 0c6166b3c7 feat: 工单详情页增强 - 支持图片粘贴拖拽和消息编辑功能
Build and Deploy Vue3 / build (push) Successful in 2m54s
Build and Deploy Vue3 / deploy (push) Successful in 11m17s
- 支持剪贴板粘贴图片和文件拖拽上传
- 添加消息编辑功能,支持修改内容和管理图片
- 编辑对话框支持显示、删除原有图片和添加新图片
- 修复消息更新API响应检查逻辑
- 优化图片文件ID管理,支持保留和删除原始图片
2025-12-17 17:03:52 +08:00
wlkjyy 978b18d5d5 feat: 工单系统优化 - 改为列表形式,添加排序、状态修改、图片粘贴拖拽等功能
Build and Deploy Vue3 / build (push) Successful in 2m52s
Build and Deploy Vue3 / deploy (push) Successful in 2m37s
2025-12-17 15:42:14 +08:00
shiran 5fb53a2fdd Merge pull request 'master' (#14) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 2m6s
Build and Deploy Vue3 / deploy (push) Successful in 2m12s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#14
2025-12-16 15:36:59 +08:00
wlkjyy 54f78e15fe feat: 优化工单和用户管理功能
Build and Deploy Vue3 / build (push) Successful in 1m12s
Build and Deploy Vue3 / deploy (push) Successful in 5m2s
- 工单模块改为列表形式,支持点击进入详情页回复
- 新增工单列表页面(TicketList.vue)和详情页面(TicketDetail.vue)
- 工单详情页支持图片上传、快捷回复、定时刷新
- 消息按ID排序,时间显示优化(今天/昨天/星期/完整日期)
- 定时刷新时不显示loading,且只在数据变化时更新UI
- 用户列表直接使用API返回的cover字段作为头像,减少HTTP请求
- 修复用户余额页面balance_type参数undefined问题
2025-12-16 11:29:52 +08:00
wlkjyy ab2df50c0d 优化用户和工单管理功能
Build and Deploy Vue3 / build (push) Successful in 3m1s
Build and Deploy Vue3 / deploy (push) Successful in 1m50s
- 优化用户列表页面,移除头像批量加载导致的大量detail请求
- 移除工单列表自动刷新功能,避免页面跳转问题
- 将用户余额管理整合到用户列表操作菜单中
- 重构用户余额管理页面,采用现代化企业扁平化设计
- 移除用户余额管理独立菜单项
- 优化页面交互体验和视觉效果
2025-12-15 20:34:02 +08:00
wlkjyy 6859753470 feat: 工单模块改为列表形式,点击回复进入详情页
Build and Deploy Vue3 / build (push) Successful in 1m13s
Build and Deploy Vue3 / deploy (push) Successful in 2m31s
2025-12-15 16:25:16 +08:00
shiran 8897a62dc7 Merge pull request '更新 .gitea/workflows/build-service-server.yaml' (#13) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m46s
Build and Deploy Vue3 / deploy (push) Successful in 2m8s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#13
2025-12-12 21:19:43 +08:00
shiran 32bb4502e7 更新 .gitea/workflows/build-service-server.yaml
Build and Deploy Vue3 / build (push) Successful in 7m51s
Build and Deploy Vue3 / deploy (push) Successful in 12m49s
2025-12-12 21:19:12 +08:00
shiran baec1e3685 Merge pull request 'master' (#12) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m44s
Build and Deploy Vue3 / deploy (push) Has been cancelled
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#12
2025-12-12 19:12:25 +08:00
lin 4a13048718 fix:修改商品列表分页展示
Build and Deploy Vue3 / build (push) Successful in 2m47s
Build and Deploy Vue3 / deploy (push) Successful in 1m43s
2025-12-12 16:02:25 +08:00
lin b56359e572 fix:对接商品列表分页
Build and Deploy Vue3 / build (push) Successful in 2m53s
Build and Deploy Vue3 / deploy (push) Successful in 6m6s
2025-12-11 17:52:52 +08:00
lin 41d6492daf fix:修改库存控制修改操作
Build and Deploy Vue3 / build (push) Successful in 3m4s
Build and Deploy Vue3 / deploy (push) Successful in 4m49s
2025-12-11 10:14:57 +08:00
lin 14fcac3a24 fix:修改库存参数
Build and Deploy Vue3 / build (push) Successful in 1m50s
Build and Deploy Vue3 / deploy (push) Successful in 5m42s
2025-12-11 09:59:58 +08:00
lin 0fc582bc8c Merge branch 'qian' 2025-12-10 20:19:32 +08:00
lin 0fe4ece1a9 fit:工单修改和商品关联修改 2025-12-10 20:17:13 +08:00
wlkjyy a09631551b xx 2025-12-08 15:24:48 +08:00
shiran 4b73cb3ea0 Merge pull request 'fix:修改获取镜像套餐参数' (#9) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 17m43s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#9
2025-10-15 17:16:21 +08:00
shiran 8ba17ff6d0 Merge pull request 'fix:修改镜像展示label' (#8) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 22m3s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#8
2025-10-07 16:36:07 +08:00
wlkjyy 05ad6f8a44 Merge pull request 'fix: 修复了创建虚拟机无法获取镜像列表的BUG' (#7) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m23s
Build and Deploy Vue3 / deploy (push) Successful in 9m0s
Reviewed-on: https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc/pulls/7
2025-10-07 00:24:13 +08:00
shiran fcfde5191e Merge pull request 'master' (#6) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 3m14s
Build and Deploy Vue3 / deploy (push) Successful in 17m10s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#6
2025-10-06 21:36:43 +08:00
shiran 41295f27f0 Merge pull request 'fix:edit' (#5) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m22s
Build and Deploy Vue3 / deploy (push) Successful in 1m51s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#5
2025-10-05 15:52:12 +08:00
shiran 225228f666 Merge pull request 'feat:添加服务器新建容器' (#4) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 20m53s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#4
2025-10-04 23:25:58 +08:00
shiran 2e79be0b0f Merge pull request 'feat:添加新增虚拟机' (#3) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 2m53s
Build and Deploy Vue3 / deploy (push) Successful in 24m39s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#3
2025-10-01 19:01:36 +08:00
shiran 8847848d59 Merge pull request 'master' (#2) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m23s
Build and Deploy Vue3 / deploy (push) Successful in 1m47s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#2
2025-10-01 01:17:14 +08:00
shiran 8e698c2644 Merge pull request '更新 .gitea/workflows/build-service-server.yaml' (#1) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 4m13s
Build and Deploy Vue3 / deploy (push) Successful in 16m55s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#1
2025-09-29 23:21:02 +08:00
35 changed files with 5229 additions and 3393 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
deploy: deploy:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ninBo
steps: steps:
- name: Download Artifact - name: Download Artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
deploy: deploy:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ninBo
steps: steps:
- name: Download Artifact - name: Download Artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
+2
View File
@@ -1,3 +1,5 @@
# 管理员后台pc端
# 007UI 后台管理系统 # 007UI 后台管理系统
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。 一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
+52
View File
@@ -15,3 +15,55 @@ export const addSignRewardType = (data) => {
} }
}) })
} }
// 拼团活动相关接口
/**获取拼团队伍列表 */
export const getGroupBuyList = () => {
return http2.get('/api/v1/users/activity/group_buy/list')
}
/**获取拼团队伍详情 */
export const getGroupBuyDetail = (groupBuyId) => {
return http2.get('/api/v1/users/activity/group_buy/detail', {
params: { group_buy_id: groupBuyId }
})
}
/**为队伍添加随机伪人 */
export const addRandomUser = (groupBuyId) => {
const formData = new FormData()
formData.append('group_buy_id', groupBuyId)
return http2.post('/api/v1/admin/activity/group_buy/add_random_user', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**创建随机伪人队伍 */
export const addRandomGroup = (data) => {
const formData = new FormData()
formData.append('name', data.name)
formData.append('group_buy_type_id', data.group_buy_type_id)
return http2.post('/api/v1/admin/activity/group_buy/add_random_group', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**导出成功队伍信息 */
export const exportIdcInfo = () => {
return http2.get('/api/v1/admin/activity/group_buy/export_idc_info')
}
/**为指定队伍下发订单 */
export const setOrder = (groupBuyId) => {
const formData = new FormData()
formData.append('group_buy_id', groupBuyId)
return http2.post('/api/v1/admin/activity/group_buy/set_order', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
+79
View File
@@ -0,0 +1,79 @@
// 商品管理 API 接口测试文件
// 此文件用于验证所有接口是否正确对接 OpenAPI 文档
import {
// 商品分组管理
getProductGroupList,
createProductGroup,
updateProductGroup,
hideProductGroup,
startProductGroup,
deleteProductGroup,
// 商品管理
getProductList,
getProductTagList,
createProduct,
updateProduct,
deleteProduct,
// 商品参数管理
getProductParameterList,
createProductParameter,
getProductParameterDetail,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
deleteProductParameterValue,
updateProductParameterValue
} from './product'
/**
* 商品管理 API 接口对接验证
*
* 根据 OpenAPI 文档,所有接口已完整对接:
*
* 1. 商品分组管理 (6个接口)
* ✅ GET /api/v1/admin/good/group/list - 获取商品分组列表
* ✅ POST /api/v1/admin/good/group/create - 创建商品分组
* ✅ POST /api/v1/admin/good/group/update - 更新商品分组
* ✅ POST /api/v1/admin/good/group/disable - 隐藏商品组
* ✅ POST /api/v1/admin/good/group/enable - 启用商品组
* ✅ DELETE /api/v1/admin/good/group/delete - 删除商品分组
*
* 2. 商品管理 (4个接口)
* ✅ GET /api/v1/admin/good/goods/list - 获取商品列表
* ✅ GET /api/v1/admin/good/goods/tag_list - 获取商品标签列表
* ✅ POST /api/v1/admin/good/goods/create - 创建商品
* ✅ POST /api/v1/admin/good/goods/update - 更新商品
* ✅ DELETE /api/v1/admin/good/goods/delete - 删除商品
*
* 3. 商品参数管理 (8个接口)
* ✅ GET /api/v1/admin/good/spec/list - 获取商品参数列表
* ✅ POST /api/v1/admin/good/spec/create - 创建商品参数
* ✅ GET /api/v1/admin/good/spec/detail - 获取商品参数详情
* ✅ POST /api/v1/admin/good/spec/update - 更新商品参数
* ✅ DELETE /api/v1/admin/good/spec/delete - 删除商品参数
* ✅ POST /api/v1/admin/good/spec/add_value - 增加商品参数值
* ✅ DELETE /api/v1/admin/good/spec/delete_value - 删除商品参数值
* ✅ POST /api/v1/admin/good/spec/update_value - 更新商品参数值
*
* 总计:18个接口全部对接完成
*
* 页面实现状态:
* ✅ ProductList.vue - 商品列表管理页面(包含商品参数管理)
* ✅ ProductGroup.vue - 商品分组管理页面
*
* 注意事项:
* 1. 所有 POST/DELETE 接口使用 multipart/form-data 格式
* 2. 更新商品参数接口使用 query 参数而非 body
* 3. 价格字段以分为单位存储
* 4. 商品标签从 tag_list 接口获取
*/
export const API_STATUS = {
totalApis: 18,
implementedApis: 18,
completionRate: '100%',
lastUpdated: new Date().toISOString()
}
+6 -1
View File
@@ -57,6 +57,10 @@ export const deleteProductGroup = (data) => {
export const getProductList = (params) => { export const getProductList = (params) => {
return http2.get('/api/v1/admin/good/goods/list', {params: params}) return http2.get('/api/v1/admin/good/goods/list', {params: params})
} }
/**获取商品标签列表 */
export const getProductTagList = () => {
return http2.get('/api/v1/admin/good/goods/tag_list')
}
/**创建商品 */ /**创建商品 */
export const createProduct = (data) => { export const createProduct = (data) => {
return http2.post('/api/v1/admin/good/goods/create', data,{ return http2.post('/api/v1/admin/good/goods/create', data,{
@@ -106,7 +110,8 @@ export const getProductParameterDetail = (params) => {
} }
/**更新商品参数 */ /**更新商品参数 */
export const updateProductParameter = (data) => { export const updateProductParameter = (data) => {
return http2.post('/api/v1/admin/good/spec/update', data,{ return http2.post('/api/v1/admin/good/spec/update', null, {
params: data,
headers:{ headers:{
'Content-Type':'multipart/form-data' 'Content-Type':'multipart/form-data'
} }
+11 -1
View File
@@ -53,7 +53,7 @@ export const updateUserInfo = (data) => {
/**删除用户 */ /**删除用户 */
export const deleteUser = (data) => { export const deleteUser = (data) => {
return http2.delete('/api/v1/admin/user/user/delete?group_id='+data.group_id) return http2.delete('/api/v1/admin/user/user/delete?user_id='+data.user_id)
} }
/**修改用户头像 */ /**修改用户头像 */
export const updateUserAvatar = (data) => { export const updateUserAvatar = (data) => {
@@ -162,4 +162,14 @@ export const addUserGroupMember = (data) => {
'Content-Type':'multipart/form-data' 'Content-Type':'multipart/form-data'
} }
}) })
}
/**退款对应账单 */
export const refundBalance = (data) => {
return http2.get('/api/v1/admin/user/balance/refund', {
params:data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
} }
+202
View File
@@ -0,0 +1,202 @@
import request from "@/utils/request.js";
/**
* 创建拼团
* @param {Object} data - 拼团数据
* @param {string} data.name - 拼团名称
* @param {number} data.maxPerson - 最大人数
* @param {string} data.cover - 封面图片URL
* @returns {Promise} 返回拼团详情
*/
export const createGroupBuy = (data) => {
return request.post("/api/v1/group-buy/create", data)
}
/**
* 检查拼团
* @param {string} groupBuyId - 拼团ID
* @returns {Promise} 返回检查结果
*/
export const checkGroupBuy = (groupBuyId) => {
return request.get(`/api/v1/group-buy/check/${groupBuyId}`)
}
/**
* 获取拼团详情
* @param {string} groupBuyId - 拼团ID
* @returns {Promise} 返回拼团详情
*/
export const getGroupBuyDetail = (groupBuyId) => {
return request.get(`/api/v1/group-buy/${groupBuyId}`)
}
/**
* 获取拼团列表
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页数量
* @returns {Promise} 返回拼团列表
*/
export const getGroupBuyList = (params) => {
return request.get("/api/v1/users/activity/group_buy/list", params)
}
/**
* 加入拼团
* @param {string} groupBuyId - 拼团ID
* @param {Object} data - 用户数据
* @returns {Promise} 返回加入结果
*/
export const joinGroupBuy = (groupBuyId, data) => {
return request.post(`/api/v1/group-buy/${groupBuyId}/join`, data)
}
/**
* 删除拼团
* @param {string} groupBuyId - 拼团ID
* @returns {Promise} 返回删除结果
*/
export const deleteGroupBuy = (groupBuyId) => {
return request.delete(`/api/v1/group-buy/${groupBuyId}`)
}
// ==================== 拼团类型管理接口 ====================
/**
* 获取拼团活动类型列表
* @param {Object} params - 查询参数
* @param {number} [params.page=1] - 页码
* @param {number} [params.count=10] - 每页条数
* @param {string} [params.key] - 关键词筛选
* @param {number} [params.expire_time] - 过期时间筛选(时间戳)
* @param {string} [params.tag] - 标签筛选
* @returns {Promise} 返回拼团类型列表
*/
export const getGroupBuyTypeList = (params) => {
return request.get("/api/v1/admin/activity/group_buy/type/list", params)
}
/**
* 获取拼团活动类型标签列表
* @returns {Promise} 返回标签列表
*/
export const getGroupBuyTypeTags = () => {
return request.get("/api/v1/admin/activity/group_buy/type/tags")
}
/**
* 新增拼团活动类型
* @param {Object} data - 类型数据
* @param {string} data.name - 名称
* @param {string} [data.note] - 备注
* @param {string} data.price - 价格(分)
* @param {string} [data.renew_price] - 续费价格(分)
* @param {string} data.max_person - 拼团需要人数
* @param {string} [data.tag] - 标签
* @param {number} [data.expire_time] - 活动过期时间
* @returns {Promise} 返回新增结果
*/
export const addGroupBuyType = (data) => {
return request.post("/api/v1/admin/activity/group_buy/type/add", data,{
})
}
/**
* 修改拼团活动类型
* @param {Object} data - 类型数据
* @param {string} data.id - ID编号
* @param {string} [data.name] - 名称
* @param {string} [data.note] - 备注
* @param {string} [data.price] - 价格(分)
* @param {string} [data.renew_price] - 续费价格(分)
* @param {string} [data.max_person] - 拼团需要人数
* @param {string} [data.tag] - 标签
* @param {number} [data.expire_time] - 活动过期时间
* @returns {Promise} 返回修改结果
*/
export const updateGroupBuyType = (data) => {
return request.post("/api/v1/admin/activity/group_buy/type/update", data)
}
/**
* 删除拼团活动类型
* @param {string} id - 类型ID
* @returns {Promise} 返回删除结果
*/
export const deleteGroupBuyType = (id) => {
return request.delete("/api/v1/admin/activity/group_buy/type/delete", { params: { id } })
}
// ==================== 拼团队伍管理接口 ====================
/**
* 检查队伍列表
* @returns {Promise} 返回队伍检查结果
*/
export const checkGroupBuyTeams = () => {
return request.get("/api/v1/admin/activity/group_buy/check")
}
/**
* 为队伍添加随机伪人
* @param {string} groupBuyId - 队伍ID
* @returns {Promise} 返回添加结果
*/
export const addRandomUser = (groupBuyId) => {
return request.post("/api/v1/admin/activity/group_buy/add_random_user", { group_buy_id: groupBuyId })
}
/**
* 创建随机伪人队伍
* @param {Object} data - 队伍数据
* @param {string} data.name - 队伍名称
* @param {string} data.group_buy_type_id - 队伍类型ID
* @returns {Promise} 返回创建结果
*/
export const addRandomGroup = (data) => {
return request.post("/api/v1/admin/activity/group_buy/add_random_group", data)
}
/**
* 导出成功队伍信息
* @returns {Promise} 返回导出数据
*/
export const exportGroupBuyIdcInfo = () => {
return request.get("/api/v1/admin/activity/group_buy/export_idc_info")
}
/**
* 为指定队伍下发订单
* @param {string} groupBuyId - 队伍ID
* @returns {Promise} 返回下发结果
*/
export const setGroupBuyOrder = (groupBuyId) => {
return request.post("/api/v1/admin/activity/group_buy/set_order", { group_buy_id: groupBuyId })
}
/**
* 删除指定队伍
* @param {string} groupBuyId - 队伍ID
* @returns {Promise} 返回删除结果
*/
export const removeGroupBuy = (groupBuyId) => {
return request.delete("/api/v1/admin/activity/group_buy/remove", { params: { group_buy_id: groupBuyId } })
}
/**
* 清除所有队伍
* @returns {Promise} 返回清除结果
*/
export const clearAllGroupBuy = () => {
return request.delete("/api/v1/admin/activity/group_buy/clear")
}
/**
* 清除指定用户的所有队伍
* @param {string} userId - 用户ID
* @returns {Promise} 返回清除结果
*/
export const clearUserGroupBuy = (userId) => {
return request.delete("/api/v1/admin/activity/group_buy/user_clear", { params: { user_id: userId } })
}
+7 -2
View File
@@ -5,8 +5,13 @@ import request from "@/utils/request.js";
* @returns {Promise} * @returns {Promise}
*/ */
export function getTickerList(count, page, status) { export function getTickerList(count, page, status, orderBy, order) {
return request.get('/api/v1/admin/work_order/list', { count, page, status }) const params = { count, page }
if (status !== undefined && status !== '') params.status = status
if (orderBy) params.orderBy = orderBy
if (order) params.order = order
console.log('工单列表请求参数:', params) // 调试日志
return request.get('/api/v1/admin/work_order/list', params)
} }
// 待处理 // 待处理
BIN
View File
Binary file not shown.
+191
View File
@@ -0,0 +1,191 @@
<template>
<el-dialog
:model-value="visible"
title="选择用户"
width="800px"
class="user-selector-dialog"
append-to-body
@update:model-value="handleVisibleChange"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="handleSearch"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
background
class="selector-pagination"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { getUserList } from '@/api/admin/user'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'select'])
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 visible 变化,打开时加载数据
watch(() => props.visible, (newVal) => {
if (newVal) {
selectedUser.value = null
if (userList.value.length === 0) {
fetchUserList()
}
}
})
const handleVisibleChange = (val) => {
emit('update:visible', val)
}
const closeDialog = () => {
emit('update:visible', false)
}
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(searchParams)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchUserList()
}
const handleCurrentChange = (row) => {
selectedUser.value = row
}
const handleSizeChange = (size) => {
searchParams.count = size
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
const confirmSelection = () => {
if (!selectedUser.value) {
ElMessage.warning('请选择一个用户')
return
}
emit('select', selectedUser.value)
closeDialog()
}
</script>
<style scoped>
.selector-search {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: var(--el-color-primary-light-8) !important;
color: var(--el-color-primary);
font-weight: bold;
}
</style>
+42 -66
View File
@@ -5,23 +5,25 @@ export const menus = [
icon: 'DataBoard' icon: 'DataBoard'
}, },
{ {
path : '/ticket', path: '/ticket',
title: '工单理', title: '工单理',
icon: 'DataBoard' icon: 'Tickets',
children: [
{
path: '/ticket/list',
title: '工单列表'
}
]
}, },
{ {
path:'/user', path: '/user',
title: '用户管理', title: '用户管理',
icon: 'User', icon: 'User',
children: [ children: [
{ {
path: '/user/list', path: '/user/list',
title: '用户列表' title: '用户列表'
}, },
{
path: '/user/balance',
title: '用户余额管理'
},
{ {
path: '/user/group', path: '/user/group',
title: '用户组管理' title: '用户组管理'
@@ -45,10 +47,7 @@ export const menus = [
path: '/product/group', path: '/product/group',
title: '商品分组' title: '商品分组'
}, },
{
path: '/product/parameter',
title: '商品参数'
}
] ]
}, },
{ {
@@ -75,36 +74,7 @@ export const menus = [
path: '/marketing/voucher', path: '/marketing/voucher',
title: '代金券管理' title: '代金券管理'
}, },
{
path: '/marketing/user-distribution',
title: '用户分发管理'
},
{
id: 'discount-goods',
title: '商品关联管理',
path: '/marketing/discount-goods',
badge: 'NEW'
},
{
id: 'discount-users',
title: '用户关联管理',
path: '/marketing/discount-users',
badge: 'NEW'
},
{
id: 'user-info',
title: '用户信息管理',
path: '/marketing/user-info',
badge: 'NEW'
},
{
id: 'user-history',
title: '用户使用记录管理',
path: '/marketing/user-history',
badge: 'NEW'
}
] ]
}, },
{ {
@@ -115,6 +85,12 @@ export const menus = [
{ {
path: '/activity/signin', path: '/activity/signin',
title: '签到活动' title: '签到活动'
},{
path:'/activity/groupbuy',
title:'拼团活动',
},{
path:'/activity/groupbuy-type',
title:'拼团类型'
} }
] ]
}, },
@@ -141,9 +117,9 @@ export const menus = [
{ path: '/acs/images/categories', title: '镜像分类' } { path: '/acs/images/categories', title: '镜像分类' }
] ]
}, },
{ {
path: '/acs/nodes', path: '/acs/nodes',
title: '节点管理' title: '节点管理'
}, },
{ {
path: '/acs/guacamole', path: '/acs/guacamole',
@@ -158,10 +134,10 @@ export const menus = [
] ]
}, },
{ {
path:'/setting', path: '/setting',
title:'全局设置管理', title: '全局设置管理',
children:[ children: [
{path:'/setting/global',title:'全局设置'} { path: '/setting/global', title: '全局设置' }
] ]
} }
] ]
@@ -171,31 +147,31 @@ export const menus = [
title: '系统管理', title: '系统管理',
icon: 'Setting', icon: 'Setting',
children: [ children: [
{ {
path: '/system/permission', path: '/system/permission',
title: '权限管理', title: '权限管理',
children: [ children: [
{ path: '/system/permission/route', title: '路由权限' }, { path: '/system/permission/route', title: '路由权限' },
{ path: '/system/permission/admin', title: '管理员权限' } { path: '/system/permission/admin', title: '管理员权限' }
] ]
}, },
{ {
path: '/system/file', path: '/system/file',
title: '文件管理' title: '文件管理'
}, },
{ {
path: '/system/domain-whitelist', path: '/system/domain-whitelist',
title: '域名白名单' title: '域名白名单'
}, },
{ {
path: '/system/setting-group', path: '/system/setting-group',
title: '配置组管理' title: '配置组管理'
}, },
{ {
path: '/system/setting-list', path: '/system/setting-list',
title: '配置管理' title: '配置管理'
} }
] ]
} }
+45 -49
View File
@@ -39,7 +39,27 @@ const routes = [
title: '工单管理', title: '工单管理',
icon: 'Tickets' icon: 'Tickets'
}, },
component: () => import('../views/ticket/TicketChat.vue'), redirect: '/ticket/list',
children: [
{
path: 'list',
name: 'TicketList',
component: () => import('../views/ticket/TicketList.vue'),
meta: {
title: '工单列表'
}
},
{
path: 'detail',
name: 'TicketDetail',
component: () => import('../views/ticket/TicketDetail.vue'),
meta: {
title: '工单详情',
hidden: true,
activeMenu: '/ticket/list'
}
}
]
}, },
// ACS管理路由 // ACS管理路由
@@ -230,14 +250,7 @@ const routes = [
title: '商品分组' title: '商品分组'
} }
}, },
{
path: 'parameter',
name: 'ProductParameter',
component: () => import('../views/product/ProductParameter.vue'),
meta: {
title: '商品参数'
}
}
] ]
}, },
// 订单管理路由 // 订单管理路由
@@ -287,49 +300,16 @@ const routes = [
} }
}, },
{ {
path: 'user-distribution', path: 'voucher/:id/manage',
name: 'UserDistribution', name: 'VoucherManagement',
component: () => import('../views/marketing/UserVoucher.vue'), component: () => import('../views/marketing/VoucherManagement.vue'),
meta: { meta: {
title: '用户分发管理' title: '代金券详情管理',
hidden: true,
activeMenu: '/marketing/voucher'
} }
}, },
{
path: 'discount-goods',
name: 'DiscountGoods',
component: () => import('../views/marketing/DiscountGoods.vue'),
meta: {
title: '商品关联管理',
badge: 'NEW'
}
},
{
path: 'discount-users',
name: 'DiscountUsers',
component: () => import('../views/marketing/DiscountUsers.vue'),
meta: {
title: '用户关联管理',
badge: 'NEW'
}
},
{
path: 'user-info',
name: 'UserInfo',
component: () => import('../views/marketing/VoucherHolders.vue'),
meta: {
title: '用户信息管理',
badge: 'NEW'
}
},
{
path: 'user-history',
name: 'UserHistory',
component: () => import('../views/marketing/VoucherHistory.vue'),
meta: {
title: '用户使用记录管理',
badge: 'NEW'
}
}
] ]
}, },
// 活动管理路由 // 活动管理路由
@@ -349,6 +329,22 @@ const routes = [
meta: { meta: {
title: '签到活动' title: '签到活动'
} }
},
{
path: '/activity/groupbuy',
name: 'GroupBuyActivity',
component: () => import('../views/activity/GroupBuyActivity.vue'),
meta: {
title: '拼团活动'
}
},
{
path: '/activity/groupbuy-type',
name: 'GroupBuyType',
component: () => import('../views/activity/GroupBuyType.vue'),
meta: {
title: '拼团类型'
}
} }
] ]
}, },
+5 -4
View File
@@ -3,8 +3,9 @@ import { ElMessage } from 'element-plus'
import router from '@/router' import router from '@/router'
// 基础URL // 基础URL
const baseUrl = 'https://apiservertest.s1f.ren' const baseUrl = 'https://apiservertest.s1f.ren' // SSL证书有问题
// const baseUrl = 'https://cloudapi.007yjs.com' // const baseUrl = 'http://apiservertest.s1f.ren' // HTTP版本
// const baseUrl = 'https://cloudapi.007yjs.com' // 尝试备用地址
// 检查URL是否需要认证 // 检查URL是否需要认证
const urlNeedAuth = (url) => { const urlNeedAuth = (url) => {
@@ -93,8 +94,8 @@ class Request {
} }
// DELETE 请求 // DELETE 请求
delete(url,data={}, config = {}) { delete(url, config = {}) {
return this.instance.delete(url,data, config) return this.instance.delete(url, config)
} }
// PATCH 请求 // PATCH 请求
+5
View File
@@ -51,4 +51,9 @@ export function timeToTimestamp(time) {
} }
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000 return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000
}
export function reducenum(num){
return num / 100
} }
+590
View File
@@ -0,0 +1,590 @@
<template>
<div class="group-buy-container">
<el-card class="header-card">
<div class="header-actions">
<el-button type="primary" icon="Plus" @click="openCreateDialog">
创建随机队伍
</el-button>
<el-button type="success" icon="Download" @click="handleExport" :loading="exportLoading">
导出成功队伍
</el-button>
<el-button type="info" icon="Refresh" @click="fetchGroupList" :loading="loading">
刷新列表
</el-button>
<el-button type="danger" @click="handleClearAll">
清除所有队伍
</el-button>
<el-button type="warning" @click="showClearUserDialog = true">
清除用户队伍
</el-button>
</div>
</el-card>
<el-card class="table-card">
<el-table :data="groupList" v-loading="loading" stripe border>
<el-table-column prop="id" label="队伍ID" />
<el-table-column prop="name" label="队伍名称" min-width="150" />
<el-table-column prop="currentMembers" label="当前人数" width="100" align="center" />
<el-table-column prop="maxMembers" label="需要人数" width="100" align="center" />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 'pending'"
type="primary"
size="small"
@click="handleAddRandomUser(row)"
:loading="row.addingUser"
>
添加伪人
</el-button>
<el-button
v-if="row.status === 'success'"
type="success"
size="small"
@click="handleSetOrder(row)"
:loading="row.settingOrder"
>
下发订单
</el-button>
<el-button
type="info"
size="small"
@click="handleViewMembers(row)"
>
查看详情
</el-button>
<el-button
type="danger"
size="small"
@click="handleRemoveGroup(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 创建随机队伍对话框 -->
<el-dialog
v-model="showCreateDialog"
title="创建随机伪人队伍"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="createForm" :rules="createRules" ref="createFormRef" label-width="100px">
<el-form-item label="队伍名称" prop="name">
<el-input v-model="createForm.name" placeholder="请输入队伍名称" />
</el-form-item>
<el-form-item label="标签" prop="tag">
<el-select v-model="createForm.tag" placeholder="请选择标签" style="width: 100%" @change="handleTagChange">
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
<el-form-item label="拼团类型" prop="groupBuyTypeId">
<el-select v-model="createForm.groupBuyTypeId" placeholder="请先选择标签" :disabled="!createForm.tag" style="width: 100%">
<el-option v-for="item in typeList" :key="item.id" :label="`${item.name} (${item.maxPerson}人)`" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" @click="handleCreate" :loading="createLoading">
创建
</el-button>
</template>
</el-dialog>
<!-- 查看成员对话框 -->
<el-dialog
v-model="showMembersDialog"
title="队伍成员列表"
width="700px"
>
<el-table :data="currentMembers" border stripe>
<el-table-column label="头像" width="80" align="center">
<template #default="{ row }">
<el-avatar :size="50" :src="row.cover" v-if="row.cover">
<img src="https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png" />
</el-avatar>
<el-avatar :size="50" v-else>
{{ row.username?.charAt(0) || '?' }}
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column label="队长" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.teamLeader" type="warning" size="small">队长</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button type="danger" size="small" @click="handleClearUserGroups(row.userId)">清除队伍</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 清除用户队伍对话框 -->
<el-dialog
v-model="showClearUserDialog"
title="清除指定用户的所有队伍"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="clearUserForm" label-width="80px">
<el-form-item label="用户ID">
<el-input v-model="clearUserForm.userId" placeholder="请输入用户ID" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showClearUserDialog = false">取消</el-button>
<el-button type="danger" @click="handleClearUserSubmit" :loading="clearUserLoading">确认清除</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getGroupBuyList,
getGroupBuyDetail,
addRandomUser,
addRandomGroup,
exportIdcInfo,
setOrder
} from '@/api/admin/activity'
import { getGroupBuyTypeList, getGroupBuyTypeTags, removeGroupBuy, clearAllGroupBuy, clearUserGroupBuy } from '@/api/groupBuy'
// 数据状态
const loading = ref(false)
const exportLoading = ref(false)
const createLoading = ref(false)
const groupList = ref([])
// 对话框状态
const showCreateDialog = ref(false)
const showMembersDialog = ref(false)
const showClearUserDialog = ref(false)
const currentMembers = ref([])
const clearUserLoading = ref(false)
const clearUserForm = reactive({ userId: '' })
// 创建表单
const createFormRef = ref(null)
const createForm = reactive({
name: '',
tag: '',
groupBuyTypeId: ''
})
const typeList = ref([])
const tagList = ref([])
const createRules = {
name: [
{ required: true, message: '请输入队伍名称', trigger: 'blur' }
],
tag: [
{ required: true, message: '请选择标签', trigger: 'change' }
],
groupBuyTypeId: [
{ required: true, message: '请选择拼团类型', trigger: 'change' }
]
}
// 获取标签列表
const fetchTags = async () => {
try {
const res = await getGroupBuyTypeTags()
if (res.code === 200) {
tagList.value = res.data || []
}
} catch (error) {
console.error('获取标签失败:', error)
}
}
// 根据 tag 获取拼团类型列表
const fetchTypeListByTag = async (tag) => {
try {
const res = await getGroupBuyTypeList({ page: 1, count: 100, tag })
if (res.code === 200) {
typeList.value = res.data?.data || []
}
} catch (error) {
console.error('获取拼团类型失败:', error)
}
}
// tag 变化时获取对应的拼团类型
const handleTagChange = (tag) => {
createForm.groupBuyTypeId = ''
typeList.value = []
if (tag) {
fetchTypeListByTag(tag)
}
}
// 打开创建对话框
const openCreateDialog = () => {
createForm.name = ''
createForm.tag = ''
createForm.groupBuyTypeId = ''
typeList.value = []
fetchTags()
showCreateDialog.value = true
}
// 获取队伍列表
const fetchGroupList = async () => {
loading.value = true
try {
const res = await getGroupBuyList()
if (res.data.code === 200) {
const allGroups = res.data.data.group_buy_list || []
const lackGroups = res.data.data.lack_group_buy_list || []
const successGroups = res.data.data.success_group_buy_list || []
// 获取成功和缺人队伍的ID列表用于状态判断
const successIds = successGroups.map(g => g.group_buy_id)
const lackIds = lackGroups.map(g => g.group_buy_id)
// 将队伍数据转换为显示数据
groupList.value = allGroups.map(group => {
let status = 'empty'
if (successIds.includes(group.group_buy_id)) {
status = 'success'
} else if (lackIds.includes(group.group_buy_id)) {
status = 'pending'
}
return {
id: group.group_buy_id,
name: group.name,
type: group.maxPerson === 5 ? 0 : 1,
currentMembers: group.users?.length || 0,
maxMembers: group.maxPerson,
status: status,
createTime: group.createTime || '-',
members: group.users || [],
addingUser: false,
settingOrder: false
}
})
ElMessage.success(`加载成功,共 ${allGroups.length} 个队伍`)
} else {
ElMessage.error(res.message || '获取队伍列表失败')
}
} catch (error) {
console.error('获取队伍列表出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
loading.value = false
}
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'empty': '空队伍',
'pending': '进行中',
'success': '已满员'
}
return statusMap[status] || status
}
// 获取状态类型
const getStatusType = (status) => {
const typeMap = {
'empty': 'info',
'pending': 'warning',
'success': 'success'
}
return typeMap[status] || ''
}
// 创建随机队伍
const handleCreate = async () => {
if (!createFormRef.value) return
await createFormRef.value.validate(async (valid) => {
if (valid) {
createLoading.value = true
console.log("队伍名称:",createForm.name)
try {
const res = await addRandomGroup({ name: createForm.name, group_buy_type_id: String(createForm.groupBuyTypeId) })
console.log('创建队伍响应:', res)
if (res.data.code === 200) {
ElMessage.success(`创建成功!队伍ID: ${res.data.group_buy_id}`)
showCreateDialog.value = false
createForm.name = ''
createForm.groupBuyTypeId = ''
fetchGroupList()
} else {
ElMessage.error(res.message || '创建失败')
}
} catch (error) {
console.error('创建队伍出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
createLoading.value = false
}
}
})
}
// 添加随机伪人
const handleAddRandomUser = async (row) => {
row.addingUser = true
try {
const res = await addRandomUser(row.id)
if (res.data.code === 200) {
ElMessage.success('添加伪人成功')
fetchGroupList()
} else {
ElMessage.error(res.message || '添加伪人失败')
}
} catch (error) {
console.error('添加伪人出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
row.addingUser = false
}
}
// 下发订单
const handleSetOrder = async (row) => {
try {
await ElMessageBox.confirm(
'确定要为该队伍下发订单吗?',
'确认操作',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
row.settingOrder = true
try {
const res = await setOrder(row.id)
if (res.data.code === 200) {
ElMessage.success('订单下发成功')
fetchGroupList()
} else {
ElMessage.error(res.message || '订单下发失败')
}
} catch (error) {
console.error('下发订单出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
row.settingOrder = false
}
} catch {
// 用户取消操作
}
}
// 查看详情
const handleViewMembers = async (row) => {
try {
// 获取详细信息
const res = await getGroupBuyDetail(row.id)
if (res && res.data && res.data.code === 200) {
const detail = res.data.data
// 使用详情接口返回的数据
currentMembers.value = (detail.users || []).map(member => ({
userId: member.user_id,
username: member.user_name || `用户${member.user_id}`,
cover: member.cover || '',
teamLeader: member.team_leader || false,
idcUid: member.idc_uid || '-',
idcPhone: member.idc_phone || '-'
}))
// 更新列表中的数据
row.name = detail.name
row.currentMembers = detail.users?.length || 0
row.maxMembers = detail.maxPerson
row.members = detail.users || []
} else {
// 如果获取失败,使用列表中的数据
currentMembers.value = row.members.map(member => ({
userId: member.user_id,
username: member.user_name || `用户${member.user_id}`,
cover: member.cover || '',
teamLeader: member.team_leader || false,
idcUid: member.idc_uid || '-',
idcPhone: member.idc_phone || '-'
}))
}
showMembersDialog.value = true
} catch (error) {
console.error('获取成员信息失败:', error)
// 使用列表中的数据
currentMembers.value = row.members.map(member => ({
userId: member.user_id,
username: member.user_name || `用户${member.user_id}`,
cover: member.cover || '',
teamLeader: member.team_leader || false,
idcUid: member.idc_uid || '-',
idcPhone: member.idc_phone || '-'
}))
showMembersDialog.value = true
}
}
// 导出成功队伍信息
const handleExport = async () => {
exportLoading.value = true
try {
const res = await exportIdcInfo()
if (res.data && res.data.code === 200) {
// 将data对象转为JSON字符串并下载
const jsonStr = JSON.stringify(res.data.data, null, 2)
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `拼团成功队伍_${new Date().getTime()}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.error(res.data?.message || '导出失败')
}
} catch (error) {
console.error('导出出错:', error)
ElMessage.error('导出失败,请稍后重试')
} finally {
exportLoading.value = false
}
}
onMounted(() => {
fetchGroupList()
})
// 删除指定队伍
const handleRemoveGroup = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该队伍吗?', '确认删除', { type: 'warning' })
const res = await removeGroupBuy(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchGroupList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch { /* 取消 */ }
}
// 清除所有队伍
const handleClearAll = async () => {
try {
await ElMessageBox.confirm('确定要清除所有队伍吗?此操作不可恢复!', '危险操作', { type: 'error', confirmButtonText: '确定清除' })
const res = await clearAllGroupBuy()
if (res.code === 200) {
ElMessage.success('已清除所有队伍')
fetchGroupList()
} else {
ElMessage.error(res.message || '清除失败')
}
} catch { /* 取消 */ }
}
// 清除指定用户的所有队伍
const handleClearUserGroups = async (userId) => {
try {
await ElMessageBox.confirm(`确定要清除用户 ${userId} 的所有队伍吗?`, '确认操作', { type: 'warning' })
const res = await clearUserGroupBuy(userId)
if (res.code === 200) {
ElMessage.success('清除成功')
showMembersDialog.value = false
fetchGroupList()
} else {
ElMessage.error(res.message || '清除失败')
}
} catch { /* 取消 */ }
}
// 通过弹窗清除用户队伍
const handleClearUserSubmit = async () => {
if (!clearUserForm.userId) {
ElMessage.warning('请输入用户ID')
return
}
clearUserLoading.value = true
try {
const res = await clearUserGroupBuy(clearUserForm.userId)
if (res.code === 200) {
ElMessage.success('清除成功')
showClearUserDialog.value = false
clearUserForm.userId = ''
fetchGroupList()
} else {
ElMessage.error(res.message || '清除失败')
}
} catch (error) {
console.error('清除用户队伍失败:', error)
ElMessage.error('网络错误')
} finally {
clearUserLoading.value = false
}
}
</script>
<style scoped>
.group-buy-container {
padding: 20px;
}
.header-card {
margin-bottom: 20px;
}
.header-actions {
display: flex;
gap: 12px;
}
.table-card {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
:deep(.el-button) {
transition: all 0.3s;
}
:deep(.el-button:hover) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>
+260
View File
@@ -0,0 +1,260 @@
<template>
<div class="group-buy-type-container">
<el-card class="header-card">
<div class="header-actions">
<el-button type="primary" icon="Plus" @click="handleAdd">新增类型</el-button>
<el-select v-model="searchTag" placeholder="请选择标签" style="width: 180px; margin-left: 12px" @change="handleTagChange">
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
<el-input v-model="searchKey" placeholder="关键词搜索" style="width: 200px; margin-left: 12px" clearable :disabled="!searchTag" @keyup.enter="fetchList" />
<el-button type="info" icon="Refresh" @click="fetchList" :loading="loading" :disabled="!searchTag" style="margin-left: 12px">刷新</el-button>
</div>
</el-card>
<el-card class="table-card">
<el-empty v-if="!searchTag" description="请先选择标签" />
<template v-else>
<el-table :data="tableData" v-loading="loading" stripe border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column label="价格" width="120">
<template #default="{ row }">¥{{ (row.price / 100).toFixed(2) }}</template>
</el-table-column>
<el-table-column label="续费价格" width="120">
<template #default="{ row }">¥{{ (row.renewPrice / 100).toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="maxPerson" label="拼团人数" width="100" align="center" />
<el-table-column prop="tag" label="标签" width="120">
<template #default="{ row }">
<el-tag v-if="row.tag" type="info">{{ row.tag }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<template #default="{ row }">{{ row.expireTime ? formatTime(row.expireTime) : '永久' }}</template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="fetchList" @current-change="fetchList" />
</div>
</template>
</el-card>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑拼团类型' : '新增拼团类型'" width="500px" :close-on-click-modal="false">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="价格(分)" prop="price">
<el-input-number v-model="form.price" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="续费价格(分)" prop="renewPrice">
<el-input-number v-model="form.renewPrice" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="拼团人数" prop="maxPerson">
<el-input-number v-model="form.maxPerson" :min="2" :max="100" style="width: 100%" />
</el-form-item>
<el-form-item label="标签" prop="tag">
<el-select v-model="form.tag" placeholder="选择标签" filterable allow-create style="width: 100%">
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker v-model="form.expireTime" type="datetime" placeholder="选择过期时间" style="width: 100%" />
</el-form-item>
<el-form-item label="备注字段">
<div class="note-fields-container">
<el-button type="primary" size="small" @click="addNoteField" style="margin-bottom: 10px">+ 添加字段</el-button>
<el-table :data="form.noteFields" border size="small" v-if="form.noteFields.length">
<el-table-column label="名称" min-width="120">
<template #default="{ row }">
<el-input v-model="row.label" placeholder="如:内存" size="small" />
</template>
</el-table-column>
<el-table-column label="默认值" min-width="120">
<template #default="{ row }">
<el-input v-model="row.defaultValue" placeholder="如:20GB" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template #default="{ $index }">
<el-button type="danger" size="small" link @click="removeNoteField($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getGroupBuyTypeList, getGroupBuyTypeTags, addGroupBuyType, updateGroupBuyType, deleteGroupBuyType } from '@/api/groupBuy'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const searchKey = ref('')
const searchTag = ref('')
const tagList = ref([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const form = reactive({ id: '', name: '', price: 0, renewPrice: 0, maxPerson: 5, tag: '', expireTime: null, noteFields: [] })
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
renewPrice: [{ required: true, message: '请输入续费价格', trigger: 'blur' }],
maxPerson: [{ required: true, message: '请输入拼团人数', trigger: 'blur' }],
tag: [{ required: true, message: '请选择标签', trigger: 'change' }],
expireTime: [{ required: true, message: '请选择过期时间', trigger: 'change' }]
}
// 添加备注字段
const addNoteField = () => {
form.noteFields.push({ label: '', defaultValue: '' })
}
// 删除备注字段
const removeNoteField = (index) => {
form.noteFields.splice(index, 1)
}
const formatTime = (timeStr) => {
if (!timeStr) return '-'
return new Date(timeStr).toLocaleString('zh-CN')
}
const fetchList = async () => {
loading.value = true
try {
const res = await getGroupBuyTypeList({ page: page.value, count: pageSize.value, key: searchKey.value || undefined, tag: searchTag.value || undefined })
console.log("获取拼团类型列表数据:",res)
if (res.code === 200) {
tableData.value = res.data?.data || []
total.value = res.data?.all_count || 0
} else {
ElMessage.error(res.data.message || '获取列表失败')
}
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('网络错误')
} finally {
loading.value = false
}
}
const fetchTags = async () => {
try {
const res = await getGroupBuyTypeTags()
if (res.code === 200) tagList.value = res.data || []
} catch (error) {
console.error('获取标签失败:', error)
}
}
// 标签变化时重新获取列表
const handleTagChange = (tag) => {
page.value = 1
searchKey.value = ''
tableData.value = []
total.value = 0
if (tag) {
fetchList()
}
}
const handleAdd = () => {
isEdit.value = false
Object.assign(form, { id: '', name: '', price: 0, renewPrice: 0, maxPerson: 5, tag: '', expireTime: null, noteFields: [] })
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
let noteFields = []
try {
noteFields = row.note ? JSON.parse(row.note) : []
} catch { noteFields = [] }
Object.assign(form, { id: row.id, name: row.name, price: row.price, renewPrice: row.renewPrice, maxPerson: row.maxPerson, tag: row.tag || '', expireTime: row.expireTime || null, noteFields })
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const noteJson = JSON.stringify(form.noteFields.filter(f => f.label))
const data = {
name: form.name,
price: String(form.price),
renew_price: String(form.renewPrice),
max_person: String(form.maxPerson),
tag: form.tag,
expire_time: form.expireTime ? Math.floor(new Date(form.expireTime).getTime() / 1000) : 0,
note: noteJson
}
if (isEdit.value) data.id = String(form.id)
const res = isEdit.value ? await updateGroupBuyType(data) : await addGroupBuyType(data)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
dialogVisible.value = false
fetchList()
fetchTags()
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('网络错误')
} finally {
submitLoading.value = false
}
})
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该拼团类型吗?', '确认删除', { type: 'warning' })
const res = await deleteGroupBuyType(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchList()
fetchTags()
} else {
ElMessage.error(res.data.message || '删除失败')
}
} catch { /* 取消 */ }
}
onMounted(() => { fetchTags() })
</script>
<style scoped>
.group-buy-type-container { padding: 20px; }
.header-card { margin-bottom: 20px; }
.header-actions { display: flex; align-items: center; }
.table-card { box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); }
.pagination-wrapper { margin-top: 20px; display: flex; justify-content: flex-end; }
.note-fields-container { width: 100%; }
</style>
+20 -6
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<div class="filter-section"> <div class="filter-section">
<div class="filter-content"> <div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form"> <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金卷"> <el-form-item label="代金卷" v-if="!codeId">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -71,7 +71,7 @@
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" /> <el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column label="关联对象ID" width="120"> <el-table-column label="关联对象ID" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.goodId || row.goodGroupId || '-' }} {{ row.goodId || row.goodGroupId || '-' }}
@@ -149,7 +149,7 @@
placeholder="请选择代金券" placeholder="请选择代金券"
filterable filterable
clearable clearable
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit' || !!codeId"
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@@ -234,7 +234,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import { import {
@@ -249,13 +249,27 @@ import {
getProductGroupList getProductGroupList
} from '@/api/admin/product' } from '@/api/admin/product'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: '', code_id: props.codeId || '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchGoodsList()
}
})
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
+53 -159
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<div class="filter-section"> <div class="filter-section">
<div class="filter-content"> <div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form"> <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金卷"> <el-form-item label="代金卷" v-if="!codeId">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -68,10 +68,20 @@
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" /> <el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column label="关联对象ID" width="130"> <el-table-column label="用户名" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
{{ row.userId || row.userGroupId || '-' }} {{ row?.user?.user_name || '-' }}
</template>
</el-table-column>
<el-table-column label="手机号" min-width="150">
<template #default="{ row }">
{{ row?.user?.phone || '-' }}
</template>
</el-table-column>
<el-table-column label="邮箱" min-width="150">
<template #default="{ row }">
{{ row?.user?.email || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" width="120"> <el-table-column label="类型" width="120">
@@ -81,11 +91,6 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="action-buttons"> <div class="action-buttons">
@@ -224,82 +229,16 @@
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
append-to-body
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import { import {
@@ -313,14 +252,29 @@ import {
getUserList, getUserList,
getUserGroupList getUserGroupList
} from '@/api/admin/user' } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: '', code_id: props.codeId || '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUsersList()
}
})
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
@@ -364,15 +318,6 @@ const userGroupOptions = ref([]) // 用户组列表选项
// 用户选择弹窗相关 // 用户选择弹窗相关
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 格式化日期 // 格式化日期
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
@@ -388,25 +333,24 @@ const formatDate = (dateStr) => {
// 获取用户类型名称(根据行数据) // 获取用户类型名称(根据行数据)
const getUserTypeNameByRow = (row) => { const getUserTypeNameByRow = (row) => {
// userId 不为 0 说明是用户
if (row.userId && row.userId !== 0) { //通过看是否有user对象参数判断是否为用户还是用户组类型
if(row.user){
return '用户' return '用户'
} }else{
// userGroupId 不为 0 说明是用户组
if (row.userGroupId && row.userGroupId !== 0) {
return '用户组' return '用户组'
} }
return '-' return '-'
} }
// 获取用户类型标签(根据行数据) // 获取用户类型标签(根据行数据)
const getUserTypeTagByRow = (row) => { const getUserTypeTagByRow = (row) => {
// 用户用蓝色
if (row.userId && row.userId !== 0) {
if(row.user){
return 'primary' return 'primary'
} }else{
// 用户组用橙色
if (row.userGroupId && row.userGroupId !== 0) {
return 'warning' return 'warning'
} }
return 'info' return 'info'
@@ -469,70 +413,19 @@ const fetchUserGroupList = async () => {
// 打开用户选择器 // 打开用户选择器
const openUserSelector = () => { const openUserSelector = () => {
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
} }
// 确认用户选择 // 确认用户选择
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
form.selected_user = selectedUserTemp.value.UserId form.selected_user = user.UserId
form.user_id = selectedUserTemp.value.UserId form.user_id = user.UserId
// 将选中的用户添加到 userOptions 中(如果不存在) // 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(selectedUserTemp.value) userOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') ElMessage.success('用户选择成功')
@@ -691,6 +584,7 @@ const handleEdit = (row) => {
}) })
//点击编辑需要初始化加载用户列表 //点击编辑需要初始化加载用户列表
fetchUserList() fetchUserList()
fetchUserGroupList()
} }
// 删除用户关联 // 删除用户关联
+255
View File
@@ -0,0 +1,255 @@
<template>
<div class="group-buy-manage">
<el-card>
<template #header>
<div class="card-header">
<span>拼团管理</span>
<el-button type="primary" @click="showCreateDialog">创建拼团</el-button>
</div>
</template>
<!-- 拼团列表 -->
<el-table :data="groupBuyList" style="width: 100%">
<el-table-column prop="group_buy_id" label="拼团ID" width="180" />
<el-table-column prop="name" label="拼团名称" width="150" />
<el-table-column prop="maxPerson" label="最大人数" width="100" />
<el-table-column label="当前人数" width="100">
<template #default="{ row }">
{{ row.users?.length || 0 }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" fixed="right" width="200">
<template #default="{ row }">
<el-button link type="primary" @click="checkGroupBuy(row.group_buy_id)">
检查
</el-button>
<el-button link type="primary" @click="viewDetail(row)">
详情
</el-button>
<el-button link type="danger" @click="deleteGroupBuy(row.group_buy_id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 创建拼团对话框 -->
<el-dialog v-model="createDialogVisible" title="创建拼团" width="500px">
<el-form :model="createForm" label-width="100px">
<el-form-item label="拼团名称">
<el-input v-model="createForm.name" placeholder="请输入拼团名称" />
</el-form-item>
<el-form-item label="最大人数">
<el-input-number v-model="createForm.maxPerson" :min="2" :max="100" />
</el-form-item>
<el-form-item label="封面图片">
<el-input v-model="createForm.cover" placeholder="请输入封面图片URL" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleCreate" :loading="creating">
创建
</el-button>
</template>
</el-dialog>
<!-- 拼团详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="拼团详情" width="600px">
<el-descriptions :column="2" border v-if="currentGroupBuy">
<el-descriptions-item label="拼团ID">
{{ currentGroupBuy.group_buy_id }}
</el-descriptions-item>
<el-descriptions-item label="拼团名称">
{{ currentGroupBuy.name }}
</el-descriptions-item>
<el-descriptions-item label="最大人数">
{{ currentGroupBuy.maxPerson }}
</el-descriptions-item>
<el-descriptions-item label="当前人数">
{{ currentGroupBuy.users?.length || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ currentGroupBuy.createTime }}
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 20px">
<h4>参与用户</h4>
<el-table :data="currentGroupBuy?.users || []" style="width: 100%">
<el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="user_name" label="用户名" />
<el-table-column label="团长" width="80">
<template #default="{ row }">
<el-tag v-if="row.team_leader" type="success"></el-tag>
<el-tag v-else type="info"></el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
createGroupBuy as createGroupBuyApi,
checkGroupBuy as checkGroupBuyApi,
getGroupBuyList,
getGroupBuyDetail,
deleteGroupBuy as deleteGroupBuyApi
} from '@/api/groupBuy.js'
// 拼团列表
const groupBuyList = ref([])
// 创建对话框
const createDialogVisible = ref(false)
const creating = ref(false)
const createForm = ref({
name: '',
maxPerson: 5,
cover: ''
})
// 详情对话框
const detailDialogVisible = ref(false)
const currentGroupBuy = ref(null)
// 加载拼团列表
const loadGroupBuyList = async () => {
try {
const resp = await getGroupBuyList({ page: 1, pageSize: 20 })
if (resp && resp.code === 200) {
groupBuyList.value = resp.data || []
}
} catch (error) {
console.error('加载拼团列表失败:', error)
}
}
// 显示创建对话框
const showCreateDialog = () => {
createForm.value = {
name: '',
maxPerson: 5,
cover: ''
}
createDialogVisible.value = true
}
// 创建拼团
const handleCreate = async () => {
if (!createForm.value.name) {
ElMessage.warning('请输入拼团名称')
return
}
creating.value = true
try {
const resp = await createGroupBuyApi(createForm.value)
console.log('创建拼团响应:', resp)
if (resp && resp.code === 200) {
ElMessage.success('创建成功')
createDialogVisible.value = false
// 将新创建的拼团添加到列表
if (resp.data && resp.data.group_buy_id) {
groupBuyList.value.unshift(resp.data)
} else {
// 如果返回的不是完整数据,重新加载列表
await loadGroupBuyList()
}
} else {
ElMessage.error(resp?.message || '创建失败')
}
} catch (error) {
console.error('创建拼团失败:', error)
ElMessage.error('创建失败')
} finally {
creating.value = false
}
}
// 检查拼团
const checkGroupBuy = async (groupBuyId) => {
try {
const resp = await checkGroupBuyApi(groupBuyId)
console.log('检查拼团响应:', resp)
if (resp && resp.code === 200) {
ElMessage.success(`检查结果: ${resp.data}`)
} else {
ElMessage.error(resp?.message || '检查失败')
}
} catch (error) {
console.error('检查拼团失败:', error)
ElMessage.error('检查失败')
}
}
// 查看详情
const viewDetail = async (row) => {
try {
const resp = await getGroupBuyDetail(row.group_buy_id)
if (resp && resp.code === 200) {
currentGroupBuy.value = resp.data
detailDialogVisible.value = true
} else {
// 如果获取详情失败,使用列表中的数据
currentGroupBuy.value = row
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取详情失败:', error)
// 使用列表中的数据
currentGroupBuy.value = row
detailDialogVisible.value = true
}
}
// 删除拼团
const deleteGroupBuy = async (groupBuyId) => {
try {
await ElMessageBox.confirm('确定要删除这个拼团吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const resp = await deleteGroupBuyApi(groupBuyId)
if (resp && resp.code === 200) {
ElMessage.success('删除成功')
await loadGroupBuyList()
} else {
ElMessage.error(resp?.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除拼团失败:', error)
ElMessage.error('删除失败')
}
}
}
onMounted(() => {
loadGroupBuyList()
})
</script>
<style scoped>
.group-buy-manage {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
+85 -33
View File
@@ -2,8 +2,8 @@
<div class="user-voucher-container"> <div class="user-voucher-container">
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never"> <el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金券"> <el-form-item label="代金券" v-if="!codeId">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -54,37 +54,37 @@
{{ row.Id || row.id }} {{ row.Id || row.id }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用户ID" width="100"> <el-table-column label="用户ID" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.UserId || row.userId }} {{ row.UserId || row.userId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券ID" width="100"> <el-table-column label="代金券ID" width="100" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId }} {{ row.discountId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="150"> <el-table-column label="代金券名称" min-width="150" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" width="120"> <el-table-column label="面额" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="已使用/最大次数" width="150"> <el-table-column label="已使用/最大次数" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag> <el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" width="180"> <el-table-column label="过期时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="创建时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
@@ -134,7 +134,7 @@
<el-select <el-select
v-model="addForm.voucher_id" v-model="addForm.voucher_id"
placeholder="请选择代金券" placeholder="请选择代金券"
:disabled="addForm.discount_type === 'code'" :disabled="addForm.discount_type === 'code' || !!codeId"
filterable filterable
clearable clearable
style="width: 100%" style="width: 100%"
@@ -178,25 +178,22 @@
</el-form-item> </el-form-item>
<el-form-item label="用户" prop="user_id"> <el-form-item label="用户" prop="user_id">
<el-select <div class="user-selector-wrapper">
v-model="addForm.user_id" <div class="selected-user-display" v-if="addForm.user_id">
placeholder="请选择用户" <el-tag type="primary" closable @close="clearSelectedUser">
:disabled="addForm.target_type === 'group'" {{ getSelectedUserName() }}
filterable </el-tag>
clearable </div>
remote <el-button
:remote-method="searchUsers" type="primary"
:loading="userSearchLoading" plain
style="width: 100%" @click="openUserSelector"
@change="handleUserChange" style="width: 100%"
> >
<el-option <el-icon><User /></el-icon>
v-for="item in userOptions" {{ addForm.user_id ? '重新选择用户' : '选择用户' }}
:key="item.UserId" </el-button>
:label="`${item.UserName} (ID: ${item.UserId})`" </div>
:value="item.UserId"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户组" prop="group_id"> <el-form-item label="用户组" prop="group_id">
@@ -268,13 +265,19 @@
<el-button type="primary" @click="submitEdit">确定</el-button> <el-button type="primary" @click="submitEdit">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 -->
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
addUserVoucher, addUserVoucher,
@@ -285,14 +288,29 @@ import {
allocateVoucher allocateVoucher
} from '@/api/admin/discount' } from '@/api/admin/discount'
import { getUserList, getUserGroupList } from '@/api/admin/user' import { getUserList, getUserGroupList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: undefined, code_id: props.codeId || undefined,
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUserVoucherList()
}
})
// 添加表单 // 添加表单
const addForm = reactive({ const addForm = reactive({
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码 discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
@@ -321,6 +339,7 @@ const groupOptions = ref([]) // 用户组选项
const userSearchLoading = ref(false) // 用户搜索加载状态 const userSearchLoading = ref(false) // 用户搜索加载状态
const submitLoading = ref(false) // 提交加载状态 const submitLoading = ref(false) // 提交加载状态
const dataList = ref([]) // 优惠列表 const dataList = ref([]) // 优惠列表
const userSelectorVisible = ref(false)
// 编辑表单 // 编辑表单
const editForm = reactive({ const editForm = reactive({
@@ -623,6 +642,36 @@ const handleGroupChange = (val) => {
} }
} }
// 打开用户选择器
const openUserSelector = () => {
userSelectorVisible.value = true
}
// 确认用户选择
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
addForm.user_id = user.UserId
// 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(user)
}
userSelectorVisible.value = false
}
// 清除选中的用户
const clearSelectedUser = () => {
addForm.user_id = undefined
}
// 获取选中用户的显示名称
const getSelectedUserName = () => {
const user = userOptions.value.find(u => u.UserId === addForm.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${addForm.user_id}`
}
// 添加用户代金券 // 添加用户代金券
const handleAdd = async () => { const handleAdd = async () => {
addDialogVisible.value = true addDialogVisible.value = true
@@ -630,7 +679,7 @@ const handleAdd = async () => {
// 重置表单 // 重置表单
Object.assign(addForm, { Object.assign(addForm, {
discount_type: 'coupon', discount_type: 'coupon',
voucher_id: undefined, voucher_id: props.codeId || undefined,
code_id: undefined, code_id: undefined,
target_type: 'user', target_type: 'user',
user_id: undefined, user_id: undefined,
@@ -837,6 +886,9 @@ onMounted(() => {
// 加载代金券列表供选择 // 加载代金券列表供选择
fetchVoucherListOptions() fetchVoucherListOptions()
fetchDiscountList() fetchDiscountList()
if (queryParams.code_id) {
fetchUserVoucherList()
}
}) })
</script> </script>
+11 -1
View File
@@ -63,9 +63,10 @@
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon> <el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button> <el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
@@ -166,8 +167,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue' import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { import {
@@ -180,6 +183,8 @@ import {
import { timeToTimestamp } from '@/utils/tool' import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue' import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
const router = useRouter()
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
discount_type: 'coupon', // 固定为coupon表示代金券 discount_type: 'coupon', // 固定为coupon表示代金券
@@ -312,6 +317,11 @@ const handleEdit = (row) => {
}) })
} }
// 管理代金券
const handleManage = (row) => {
router.push(`/marketing/voucher/${row.id}/manage`)
}
// 查看代金券详情 // 查看代金券详情
const handleView = async (row) => { const handleView = async (row) => {
try { try {
+42 -144
View File
@@ -46,8 +46,8 @@
<el-table-column prop="user_id" label="用户ID" width="100" /> <el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" width="150" /> <el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" /> <el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="discount_id" label="代金券ID" width="120" /> <el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" /> <el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
<el-table-column label="优惠金额" width="120"> <el-table-column label="优惠金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
@@ -133,74 +133,10 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<el-row :gutter="20" style="margin-top: 20px"> <el-row :gutter="20" style="margin-top: 20px">
@@ -237,20 +173,36 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh, Download } from '@element-plus/icons-vue' import { Search, Refresh, Download } from '@element-plus/icons-vue'
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount' import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user' import { getUserList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
id: '', id: '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchHistoryList()
}
})
// 状态数据 // 状态数据
const loading = ref(false) const loading = ref(false)
const historyList = ref([]) const historyList = ref([])
@@ -261,15 +213,6 @@ const currentDetail = ref({})
const discountOptions = ref([]) const discountOptions = ref([])
const selectorType = ref('query') const selectorType = ref('query')
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
const selectedUserTemp = ref(null)
const userSelectorLoading = ref(false)
const UserOptions = ref([]) const UserOptions = ref([])
// 格式化日期 // 格式化日期
@@ -371,86 +314,41 @@ const resetUserSearch = () => {
// fetchUserSelectorList() // fetchUserSelectorList()
} }
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
}
// 确认用户选择 // 确认用户选择
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// 查询表单选择 // 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId queryParams.user_id = user.UserId
} else { } else {
// 编辑表单选择 // 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId editForm.user_id = user.UserId
} }
// 将选中的用户添加到 UserOptions 中(如果不存在) // 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(selectedUserTemp.value) UserOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') ElMessage.success('用户选择成功')
} }
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 查询 // 查询
const handleQuery = () => { const handleQuery = () => {
@@ -461,7 +359,7 @@ const handleQuery = () => {
// 重置查询 // 重置查询
const resetQuery = () => { const resetQuery = () => {
queryParams.user_id = undefined queryParams.user_id = undefined
queryParams.discount_id = undefined queryParams.code_id = undefined
queryParams.id = '' queryParams.id = ''
queryParams.page = 1 queryParams.page = 1
fetchHistoryList() fetchHistoryList()
+48 -167
View File
@@ -40,51 +40,51 @@
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="Id" label="ID" width="80" /> <el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="UserId" label="用户ID" width="100" /> <el-table-column prop="UserId" label="用户ID" min-width="100" />
<el-table-column label="代金券ID" width="120"> <el-table-column label="代金券ID" min-width="110" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId || '-' }} {{ row.discountId || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="180"> <el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券编码" width="150"> <el-table-column label="代金券编码" min-width="150" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.code || '-' }} {{ row.discount?.code || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" width="120"> <el-table-column label="面额" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="useTimes" label="已使用次数" width="120" /> <el-table-column prop="useTimes" label="已使用" min-width="100" />
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" /> <el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
<el-table-column label="状态" width="100"> <el-table-column label="状态" min-width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getStatusType(row)"> <el-tag :type="getStatusType(row)" size="small">
{{ getStatusText(row) }} {{ getStatusText(row) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" width="180"> <el-table-column label="过期时间" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="创建时间" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看详情</el-button> <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button> <el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -203,81 +203,17 @@
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue' import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
allocateVoucher, allocateVoucher,
@@ -285,14 +221,34 @@ import {
deleteUserVoucher, deleteUserVoucher,
getDiscountCodeList getDiscountCodeList
} from '@/api/admin/discount' } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user' import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
// 如果有 code_id,尝试刷新列表(取决于 API 是否支持仅按 code_id 查询)
// 如果 API 必须要求 user_id,则这里可能不需要立即刷新,或者提示用户选择用户
if (queryParams.user_id) {
fetchHoldersList()
}
}
})
// 状态数据 // 状态数据
const loading = ref(false) const loading = ref(false)
const holdersList = ref([]) const holdersList = ref([])
@@ -307,16 +263,7 @@ const discountOptions = ref([])
// 用户选择弹窗相关 // 用户选择弹窗相关
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑 const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 编辑表单 // 编辑表单
const editForm = reactive({ const editForm = reactive({
@@ -448,101 +395,36 @@ const handleExport = () => {
ElMessage.info('导出功能开发中...') ElMessage.info('导出功能开发中...')
} }
// 获取用户列表
const fetchUserList = async () => {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
}
// 打开查询用户选择器 // 打开查询用户选择器
const openQueryUserSelector = () => { const openQueryUserSelector = () => {
selectorType.value = 'query' selectorType.value = 'query'
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
} }
// 打开编辑用户选择器 // 打开编辑用户选择器
const openEditUserSelector = () => { const openEditUserSelector = () => {
selectorType.value = 'edit' selectorType.value = 'edit'
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
} }
// 确认用户选择 // 确认用户选择
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// 查询表单选择 // 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId queryParams.user_id = user.UserId
} else { } else {
// 编辑表单选择 // 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId editForm.user_id = user.UserId
} }
// 将选中的用户添加到 UserOptions 中(如果不存在) // 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(selectedUserTemp.value) UserOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
@@ -692,7 +574,6 @@ const submitEditForm = () => {
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchUserList()
fetchDiscountList() fetchDiscountList()
if (queryParams.user_id) { if (queryParams.user_id) {
fetchHoldersList() fetchHoldersList()
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="voucher-management-container">
<div class="header">
<el-page-header @back="goBack">
<template #content>
<span class="text-large font-600 mr-3">代金券管理 (ID: {{ voucherId }})</span>
</template>
</el-page-header>
</div>
<el-card class="mt-4" shadow="never">
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="用户分发管理" name="user-distribution">
<UserVoucher :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="商品关联管理" name="discount-goods">
<DiscountGoods :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户关联管理" name="discount-users">
<DiscountUsers :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户信息管理" name="user-info">
<VoucherHolders :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户使用记录" name="user-history">
<VoucherHistory :code-id="voucherId" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import UserVoucher from './UserVoucher.vue'
import DiscountGoods from './DiscountGoods.vue'
import DiscountUsers from './DiscountUsers.vue'
import VoucherHolders from './VoucherHolders.vue'
import VoucherHistory from './VoucherHistory.vue'
const route = useRoute()
const router = useRouter()
const activeTab = ref('user-distribution')
const voucherId = computed(() => route.params.id)
const goBack = () => {
router.push('/marketing/voucher')
}
</script>
<style scoped>
.voucher-management-container {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.mt-4 {
margin-top: 16px;
}
</style>
+58 -8
View File
@@ -5,6 +5,30 @@
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<div class="filter-section"> <div class="filter-section">
<div class="filter-content"> <div class="filter-content">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="关键词">
<el-input v-model="queryParams.key" placeholder="订单名称/ID" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户ID">
<el-input v-model="queryParams.user_id" placeholder="用户ID" clearable style="width: 120px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户关键词">
<el-input v-model="queryParams.user_key" placeholder="用户名/手机号/邮箱" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.state" placeholder="全部" clearable style="width: 120px">
<el-option label="待支付" value="0" />
<el-option label="已支付" value="1" />
<el-option label="已失效" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar"> <div class="action-bar">
<el-button type="primary" @click="handleAdd"> <el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增订单 <el-icon><Plus /></el-icon>新增订单
@@ -220,9 +244,12 @@ import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } f
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
page: 1, page: 1,
count: 10 count: 10,
key: '',
state: '',
user_id: '',
user_key: ''
}) })
// 订单表单 // 订单表单
@@ -282,7 +309,14 @@ const orderFormRef = ref(null)
const fetchOrderList = async () => { const fetchOrderList = async () => {
loading.value = true loading.value = true
try { try {
const res = await getOrderList(queryParams) // 过滤空值参数
const params = {}
Object.keys(queryParams).forEach(key => {
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
params[key] = queryParams[key]
}
})
const res = await getOrderList(params)
console.log('订单列表数据:', res.data) console.log('订单列表数据:', res.data)
if (res.data.code === 200) { if (res.data.code === 200) {
orderList.value = res.data.data.list || [] orderList.value = res.data.data.list || []
@@ -337,10 +371,10 @@ const handleQuery = () => {
// 重置查询 // 重置查询
const resetQuery = () => { const resetQuery = () => {
queryParams.order_no = '' queryParams.key = ''
queryParams.state = ''
queryParams.user_id = '' queryParams.user_id = ''
queryParams.status = '' queryParams.user_key = ''
queryParams.dateRange = []
queryParams.page = 1 queryParams.page = 1
fetchOrderList() fetchOrderList()
} }
@@ -532,13 +566,29 @@ onMounted(() => {
.filter-content { .filter-content {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
align-items: center; align-items: flex-start;
padding: 16px 20px; padding: 16px 20px;
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 8px;
}
.filter-form :deep(.el-form-item__label) {
font-size: 13px;
}
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
+483 -10
View File
@@ -82,8 +82,8 @@
</el-table-column> </el-table-column>
<el-table-column label="库存控制" width="100"> <el-table-column label="库存控制" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.inventory_control ? 'success' : 'info'"> <el-tag :type="row.inventoryControl ? 'success' : 'info'">
{{ row.inventory_control ? '已启用' : '未启用' }} {{ row.inventoryControl ? '已启用' : '未启用' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -99,7 +99,7 @@
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<!-- <el-button type="warning" link @click="handleSpec(row)">规格</el-button> --> <el-button type="warning" link @click="handleParameter(row)">参数</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -125,6 +125,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品' : '编辑商品'" :title="dialogType === 'add' ? '新增商品' : '编辑商品'"
width="700px" width="700px"
style="margin-top: 300px;"
> >
<el-form <el-form
ref="productFormRef" ref="productFormRef"
@@ -148,6 +149,16 @@
<el-form-item label="商品所属表" prop="table"> <el-form-item label="商品所属表" prop="table">
<el-input v-model="productForm.table" placeholder="请输入商品所属表" /> <el-input v-model="productForm.table" placeholder="请输入商品所属表" />
</el-form-item> </el-form-item>
<el-form-item label="商品标签" prop="tag">
<el-select v-model="productForm.tag" placeholder="请选择商品标签" style="width: 100%">
<el-option
v-for="item in tagOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="内容" prop="content"> <el-form-item label="内容" prop="content">
<el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" /> <el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
</el-form-item> </el-form-item>
@@ -181,6 +192,152 @@
<el-button type="primary" @click="submitForm">确定</el-button> <el-button type="primary" @click="submitForm">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 商品参数列表对话框 -->
<el-dialog
v-model="paramDialogVisible"
title="商品参数管理"
width="900px"
>
<div class="filter-section" style="border: none; padding: 0 0 16px 0;">
<div class="action-bar">
<el-button type="primary" @click="handleAddParameter">
<el-icon><Plus /></el-icon>新增参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
<el-table
v-loading="paramLoading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="150" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParameter(row)">编辑</el-button>
<el-button type="success" link @click="handleViewParamValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDeleteParameter(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="paramFormDialogVisible"
:title="paramFormType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
append-to-body
>
<el-form
ref="paramFormRef"
:model="paramForm"
:rules="paramRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="paramForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="paramForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="paramValuesDialogVisible"
title="参数值管理"
width="800px"
append-to-body
>
<div class="values-header">
<span>参数{{ currentParam?.name }}</span>
<el-button type="primary" @click="handleAddParamValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="paramValuesLoading"
:data="paramValueList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParamValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteParamValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="paramValueFormDialogVisible"
:title="paramValueFormType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
append-to-body
>
<el-form
ref="paramValueFormRef"
:model="paramValueForm"
:rules="paramValueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="paramValueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="paramValueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramValueFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamValueForm">确定</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -189,8 +346,17 @@ import { ref, reactive, onMounted } from 'vue'
import { getFileDetail } from '@/api/admin/file' import { getFileDetail } from '@/api/admin/file'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue' import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product' import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
import { getProductGroupList } from '@/api/admin/product' getProductTagList,
getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue
} from '@/api/admin/product'
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
@@ -204,6 +370,7 @@ const productForm = reactive({
id: undefined, id: undefined,
name: '', name: '',
table: '', table: '',
tag: '',
content: '', content: '',
cover_id: undefined, cover_id: undefined,
good_group_id: undefined, // 添加商品分组字段 good_group_id: undefined, // 添加商品分组字段
@@ -239,6 +406,7 @@ const productRules = {
const loading = ref(false) const loading = ref(false)
const productList = ref([]) const productList = ref([])
const groupOptions = ref([]) const groupOptions = ref([])
const tagOptions = ref([])
const total = ref(0) const total = ref(0)
const selectedRows = ref([]) const selectedRows = ref([])
const dialogVisible = ref(false) const dialogVisible = ref(false)
@@ -251,9 +419,12 @@ const fetchProductList = async () => {
try { try {
const res = await getProductList(queryParams) const res = await getProductList(queryParams)
if (res.data.code === 200) { if (res.data.code === 200) {
productList.value = res.data.data.data || [] const allData = res.data.data.data || []
productList.value = productList.value.filter(item => item.delete == false) // 过滤掉已删除的数据
total.value = res.data.data.total || 0 productList.value = allData.filter(item => item.delete == false)
// 计算未删除数据的总数(API返回的all_count包含已删除的,需要减去已删除的数量)
const deletedCount = allData.filter(item => item.delete == true).length
total.value = (res.data.data.data.length || 0) - deletedCount
productList.value = productList.value.map(item => { productList.value = productList.value.map(item => {
item.image = item.coverId ? getFileDetail({ file_id: item.coverId }).then(res => res.data.data.url) : '' item.image = item.coverId ? getFileDetail({ file_id: item.coverId }).then(res => res.data.data.url) : ''
return item return item
@@ -285,6 +456,20 @@ const fetchGroupList = async () => {
} }
} }
// 获取商品标签列表
const fetchTagList = async () => {
try {
const res = await getProductTagList()
if (res.data.code === 200) {
tagOptions.value = res.data.data || []
console.log('商品标签列表:', tagOptions.value) // 调试日志
}
} catch (error) {
console.error('获取标签列表失败:', error)
ElMessage.error('获取标签列表失败')
}
}
// 查询 // 查询
const handleQuery = () => { const handleQuery = () => {
queryParams.page = 1 queryParams.page = 1
@@ -328,6 +513,7 @@ const handleAdd = () => {
id: undefined, id: undefined,
name: '', name: '',
table: '', table: '',
tag: '',
content: '', content: '',
cover_id: undefined, cover_id: undefined,
good_group_id: undefined, good_group_id: undefined,
@@ -344,16 +530,18 @@ const handleAdd = () => {
// 编辑商品 // 编辑商品
const handleEdit = (row) => { const handleEdit = (row) => {
console.log("更新:",row)
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
Object.assign(productForm, { Object.assign(productForm, {
id: row.id, id: row.id,
name: row.name, name: row.name,
table: row.table, table: row.table,
tag: row.tag,
content: row.content, content: row.content,
cover_id: row.coverId, cover_id: row.coverId,
good_group_id: row.goodGroupId, good_group_id: row.goodGroupId,
inventory_control: row.inventory_control, inventory_control: row.inventoryControl,
inventory: row.inventory, inventory: row.inventory,
price: row.price, price: row.price,
pay_num: row.payNum, pay_num: row.payNum,
@@ -449,7 +637,7 @@ const submitForm = () => {
good_group_id: Number(productForm.good_group_id), // 确保是数字类型 good_group_id: Number(productForm.good_group_id), // 确保是数字类型
cover_id: productForm.cover_id || 0, cover_id: productForm.cover_id || 0,
inventory: productForm.inventory || 0, inventory: productForm.inventory || 0,
price: productForm.price || 0, price: productForm.price/100 || 0,
pay_num: productForm.pay_num || 1, pay_num: productForm.pay_num || 1,
expire_time: productForm.expire_time || 0, expire_time: productForm.expire_time || 0,
recommend_rebate: productForm.recommend_rebate || 0 recommend_rebate: productForm.recommend_rebate || 0
@@ -479,7 +667,273 @@ const submitForm = () => {
onMounted(() => { onMounted(() => {
fetchProductList() fetchProductList()
fetchGroupList() fetchGroupList()
fetchTagList()
}) })
// ---------------------------------------------------------------------
// 参数管理相关逻辑
// ---------------------------------------------------------------------
// 状态
const paramDialogVisible = ref(false)
const paramLoading = ref(false)
const parameterList = ref([])
const currentProductId = ref(null)
const paramFormDialogVisible = ref(false)
const paramFormType = ref('add')
const paramFormRef = ref(null)
const paramForm = reactive({
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const paramRules = {
arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }],
arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
}
const paramValuesDialogVisible = ref(false)
const paramValuesLoading = ref(false)
const paramValueList = ref([])
const currentParam = ref(null)
const paramValueFormDialogVisible = ref(false)
const paramValueFormType = ref('add')
const paramValueFormRef = ref(null)
const paramValueForm = reactive({
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0,
index: 0,
attr_range: 0,
range_type: 'equal'
})
const paramValueRules = {
attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }],
attr_value: [{ required: true, message: '请输入值', trigger: 'blur' }],
attr_price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
}
// 打开参数管理
const handleParameter = (row) => {
currentProductId.value = row.id
paramDialogVisible.value = true
fetchParameterList()
}
// 获取参数列表
const fetchParameterList = async () => {
if (!currentProductId.value) return
paramLoading.value = true
try {
const res = await getProductParameterList({ good_id: currentProductId.value })
if (res.data.code === 200) {
parameterList.value = res.data.data || []
}
} catch (error) {
ElMessage.error('获取参数列表失败')
} finally {
paramLoading.value = false
}
}
// 参数类型显示
const getArgTypeText = (type) => {
const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' }
return typeMap[type] || '未知'
}
const getArgTypeTag = (type) => {
const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' }
return tagMap[type] || 'info'
}
// 新增参数
const handleAddParameter = () => {
paramFormType.value = 'add'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
paramFormRef.value?.resetFields()
}
// 编辑参数
const handleEditParameter = (row) => {
paramFormType.value = 'edit'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
// 删除参数
const handleDeleteParameter = (row) => {
ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({ good_id: currentProductId.value, arg_id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 提交参数表单
const submitParamForm = () => {
paramFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_name: paramForm.arg_name,
arg_type: paramForm.arg_type
}
if (paramFormType.value === 'edit') {
submitData.arg_id = paramForm.arg_id
}
const res = paramFormType.value === 'add'
? await createProductParameter(submitData)
: await updateProductParameter(submitData)
if (res.data.code === 200) {
ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功')
paramFormDialogVisible.value = false
fetchParameterList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 查看参数值
const handleViewParamValues = (row) => {
currentParam.value = row
paramValuesDialogVisible.value = true
fetchParamValuesList()
}
// 获取参数值列表
const fetchParamValuesList = async () => {
if (!currentProductId.value || !currentParam.value) return
paramValuesLoading.value = true
try {
const res = await getProductParameterDetail({
good_id: currentProductId.value,
arg_id: currentParam.value.id
})
if (res.data.code === 200) {
paramValueList.value = res.data.data.attrs || []
}
} catch (error) {
ElMessage.error('获取参数值列表失败')
} finally {
paramValuesLoading.value = false
}
}
// 添加参数值
const handleAddParamValue = () => {
paramValueFormType.value = 'add'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0,
index: 0,
attr_range: 0,
range_type: 'equal'
})
paramValueFormRef.value?.resetFields()
}
// 编辑参数值
const handleEditParamValue = (row) => {
paramValueFormType.value = 'edit'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100,
index: row.index || 0,
attr_range: row.attr_range || 0,
range_type: row.range_type || 'equal'
})
}
// 删除参数值
const handleDeleteParamValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: currentProductId.value,
attr_id: row.id
})
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParamValuesList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 提交参数值表单
const submitParamValueForm = () => {
paramValueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_id: Number(currentParam.value.id),
attr_name: paramValueForm.attr_name,
attr_value: paramValueForm.attr_value,
attr_price: paramValueForm.attr_price,
index: Number(paramValueForm.index),
attr_range: Number(paramValueForm.attr_range),
range_type: paramValueForm.range_type
}
if (paramValueFormType.value === 'edit') {
submitData.attr_id = paramValueForm.attr_id
}
const res = paramValueFormType.value === 'add'
? await addProductParameterValue(submitData)
: await updateProductParameterValue(submitData)
if (res.data.code === 200) {
ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功')
paramValueFormDialogVisible.value = false
fetchParamValuesList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
</script> </script>
<style scoped> <style scoped>
@@ -607,5 +1061,24 @@ onMounted(() => {
0% { background-position: 200% 0; } 0% { background-position: 200% 0; }
100% { background-position: -200% 0; } 100% { background-position: -200% 0; }
} }
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style> </style>
-771
View File
@@ -1,771 +0,0 @@
<template>
<div class="product-parameter-container">
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form ref="queryFormRef" label-width="80px" :inline="true" :model="queryParams" class="search-form">
<el-form-item label="商品分组">
<el-select
v-model="queryParams.good_group_id"
placeholder="请选择商品分组"
clearable
@change="handleGroupChange"
style="width: 200px"
>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品">
<el-select
v-model="queryParams.good_id"
placeholder="请先选择商品分组"
clearable
:disabled="!queryParams.good_group_id"
style="width: 200px"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增商品参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 商品参数列表 -->
<div class="table-section">
<!-- 骨架屏 -->
<div v-if="loading" class="skeleton-container">
<div v-for="i in 5" :key="i" class="skeleton-row">
<div class="skeleton-cell skeleton-id"></div>
<div class="skeleton-cell skeleton-name"></div>
<div class="skeleton-cell skeleton-type"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<el-table
v-else
v-loading="loading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="200" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleViewValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
append-to-body
>
<el-form
ref="parameterFormRef"
:model="parameterForm"
:rules="parameterRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="parameterForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="parameterForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="valuesDialogVisible"
title="参数值管理"
width="800px"
append-to-body
>
<div class="values-header">
<span>参数{{ currentParameter?.arg_name }}</span>
<el-button type="primary" @click="handleAddValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="valuesLoading"
:data="valuesList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="valueDialogVisible"
:title="valueDialogType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
append-to-body
>
<el-form
ref="valueFormRef"
:model="valueForm"
:rules="valueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="valueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="valueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="valueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="valueDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitValueForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import {
getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue,
getProductList,
getProductGroupList
} from '@/api/admin/product'
// 查询参数
const queryParams = reactive({
good_group_id: undefined, // 商品分组ID
good_id: undefined, // 商品ID
page: 1,
count: 10
})
// 下拉选项数据
const groupOptions = ref([]) // 商品分组选项
const productOptions = ref([]) // 商品选项
const queryFormRef = ref(null)
// 商品参数表单
const parameterForm = reactive({
good_id: undefined,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const parameterRules = {
arg_name: [
{ required: true, message: '请输入参数名称', trigger: 'blur' }
],
arg_type: [
{ required: true, message: '请选择参数类型', trigger: 'change' }
]
}
// 参数值表单
const valueForm = reactive({
good_id: undefined,
arg_id: undefined,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
const valueRules = {
attr_name: [
{ required: true, message: '请输入值名称', trigger: 'blur' }
],
attr_value: [
{ required: true, message: '请输入值', trigger: 'blur' }
],
attr_price: [
{ required: true, message: '请输入价格', trigger: 'blur' }
]
}
// 状态数据
const loading = ref(false)
const valuesLoading = ref(false)
const parameterList = ref([])
const valuesList = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const valuesDialogVisible = ref(false)
const valueDialogVisible = ref(false)
const dialogType = ref('add')
const valueDialogType = ref('add')
const currentParameter = ref(null)
const parameterFormRef = ref(null)
const valueFormRef = ref(null)
// 获取商品分组列表
const fetchGroupList = async () => {
try {
const res = await getProductGroupList({ page: 1, count: 100 })
console.log('商品分组列表:', res.data)
if (res.data.code === 200) {
groupOptions.value = res.data.data.data || []
}
} catch (error) {
console.error('获取商品分组列表失败:', error)
ElMessage.error('获取商品分组列表失败')
}
}
// 获取商品列表(根据分组ID
const fetchProductList = async (groupId) => {
try {
const res = await getProductList({ good_group_id: groupId, page: 1, count: 100 })
console.log('商品列表:', res.data)
if (res.data.code === 200) {
productOptions.value = res.data.data.data || []
productOptions.value = productOptions.value.filter(item => item.delete == false)
}
} catch (error) {
console.error('获取商品列表失败:', error)
ElMessage.error('获取商品列表失败')
}
}
// 商品分组改变时
const handleGroupChange = (groupId) => {
// 清空商品选择
queryParams.good_id = undefined
productOptions.value = []
if (groupId) {
// 获取该分组下的商品列表
fetchProductList(groupId)
}
}
// 获取商品参数列表
const fetchParameterList = async () => {
// 如果没有选择商品ID,不查询
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
loading.value = true
try {
const res = await getProductParameterList({ good_id: queryParams.good_id })
console.log('商品参数列表:', res.data)
if (res.data.code === 200) {
parameterList.value = res.data.data || []
total.value = res.data.data.length || 0
}
} catch (error) {
console.error('获取商品参数列表失败:', error)
ElMessage.error('获取商品参数列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchParameterList()
}
// 重置查询
const resetQuery = () => {
queryParams.good_group_id = undefined
queryParams.good_id = undefined
queryParams.page = 1
productOptions.value = []
parameterList.value = []
total.value = 0
}
// 获取参数值列表
const fetchValuesList = async (goodId, argId) => {
valuesLoading.value = true
console.log('goodId', goodId)
console.log('argId', argId)
try {
const res = await getProductParameterDetail({ good_id: goodId, arg_id: argId })
console.log('参数值列表:', res.data)
if (res.data.code === 200) {
valuesList.value = res.data.data.attrs || []
}
} catch (error) {
console.error('获取参数值列表失败:', error)
ElMessage.error('获取参数值列表失败')
} finally {
valuesLoading.value = false
}
}
// 获取参数类型文本
const getArgTypeText = (type) => {
const typeMap = {
'string': '字符串',
'number': '数字',
'select': '选择'
}
return typeMap[type] || '未知'
}
// 获取参数类型标签颜色
const getArgTypeTag = (type) => {
const tagMap = {
'string': 'primary',
'number': 'success',
'select': 'warning'
}
return tagMap[type] || 'info'
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchParameterList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchParameterList()
}
// 新增商品参数
const handleAdd = () => {
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
parameterFormRef.value?.resetFields()
}
// 编辑商品参数
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
// 查看参数值
const handleViewValues = (row) => {
currentParameter.value = row
valuesDialogVisible.value = true
fetchValuesList(queryParams.good_id, row.id)
}
// 添加参数值
const handleAddValue = () => {
valueDialogType.value = 'add'
console.log('currentParameter', currentParameter.value)
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
valueFormRef.value?.resetFields()
}
// 编辑参数值
const handleEditValue = (row) => {
valueDialogType.value = 'edit'
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100 // 分转元
})
}
// 删除参数值
const handleDeleteValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: queryParams.good_id,
attr_id: row.id
})
console.log('删除参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 删除商品参数
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除商品参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({
good_id: queryParams.good_id,
arg_id: row.id
})
console.log('删除参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 提交参数表单
const submitForm = () => {
parameterFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(parameterForm.good_id),
arg_name: parameterForm.arg_name,
arg_type: parameterForm.arg_type
}
if (dialogType.value === 'edit') {
submitData.arg_id = parameterForm.arg_id
}
console.log('提交参数数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createProductParameter(submitData)
} else {
res = await updateProductParameter(submitData)
}
console.log('提交参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchParameterList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 提交参数值表单
const submitValueForm = () => {
valueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(valueForm.good_id),
arg_id: Number(valueForm.arg_id),
attr_name: valueForm.attr_name,
attr_value: valueForm.attr_value,
attr_price: valueForm.attr_price // 元转分
}
if (valueDialogType.value === 'edit') {
submitData.attr_id = valueForm.attr_id
}
console.log('提交参数值数据:', submitData)
let res
if (valueDialogType.value === 'add') {
res = await addProductParameterValue(submitData)
} else {
res = await updateProductParameterValue(submitData)
}
console.log('提交参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(valueDialogType.value === 'add' ? '添加成功' : '修改成功')
valueDialogVisible.value = false
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
// 初始化时只获取商品分组列表
fetchGroupList()
})
</script>
<style scoped>
.product-parameter-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-type { width: 120px; }
.skeleton-action { width: 250px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+266 -128
View File
@@ -33,7 +33,7 @@
@click="selectTicket(ticket)" @click="selectTicket(ticket)"
> >
<div class="ticket-avatar"> <div class="ticket-avatar">
<el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar> <el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar>
</div> </div>
<div class="ticket-content"> <div class="ticket-content">
<div class="ticket-top"> <div class="ticket-top">
@@ -96,8 +96,8 @@
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']" :class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
> >
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId)"> <el-avatar :size="36" :src="message.avatar">
{{ currentTicket.username.charAt(0) }} {{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
</el-avatar> </el-avatar>
</div> </div>
<div class="message-content"> <div class="message-content">
@@ -117,7 +117,7 @@
<div class="message-time">{{ formatMessageTime(message.time) }}</div> <div class="message-time">{{ formatMessageTime(message.time) }}</div>
</div> </div>
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar> <el-avatar :size="36" :src="message.avatar">A</el-avatar>
</div> </div>
</div> </div>
</div> </div>
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Loading } from '@element-plus/icons-vue' import { Search, Plus, Loading } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { import {
getTickerList, getTickerList,
getTicketDetail, getTicketDetail,
replyTicket, replyTicket,
closeTicket, closeTicket,
getUserAvatar, getUserAvatar,
getFileImage, getFileImage,
parseFilesToImages parseFilesToImages,
getTicketCount
} from '@/api/ticket' } from '@/api/ticket'
import notificationSound from '@/assets/7.wav'
import { useUserStore } from '@/store/userStore'
// 路由相关 // 路由相关
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// 管理员ID列表(客服ID // 用户 store
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID const userStore = useUserStore()
// 头像 // 头像
const adminAvatar = ref('') const adminAvatar = ref('')
@@ -271,6 +274,11 @@ const stats = reactive({
isLoadingStats: false isLoadingStats: false
}) })
// 上一次的待处理数量,用于判断是否有新工单
const previousPendingCount = ref(0)
// 音频对象
const audio = new Audio(notificationSound)
// 快捷回复选项 // 快捷回复选项
const quickReplies = ref([ const quickReplies = ref([
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' }, { title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息 username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user_id, userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
@@ -368,44 +377,35 @@ const fetchTicketList = async (append = false) => {
} }
} }
// 获取单个状态的工单数量
const fetchStatusStat = async (status) => {
try {
// 将状态字符串转换为API所需的状态值
let statusValue = '';
if (status === 'pending') statusValue = '0';
else if (status === 'processing') statusValue = '1';
else if (status === 'replied') statusValue = '2';
else if (status === 'completed') statusValue = '3';
const res = await getTickerList(10, 1, statusValue) // 只请求一条数据,但获取总数
if (res.code === 200) {
if (status === '') {
stats.total = res.data.all_count
} else {
stats[status] = res.data.all_count
}
} else {
console.error(`获取${status || '全部'}工单统计失败:`, res.message)
}
} catch (error) {
console.error(`获取${status || '全部'}工单统计出错:`, error)
}
}
// 获取所有状态的工单数量 // 获取所有状态的工单数量
const fetchAllStats = async () => { const fetchAllStats = async () => {
stats.isLoadingStats = true stats.isLoadingStats = true
try { try {
// 并行获取各个状态的工单数量 const res = await getTicketCount()
await Promise.all([ if (res.code === 200) {
fetchStatusStat(''), // 获取全部工单数量 const data = res.data
fetchStatusStat('pending'), // 待处理
fetchStatusStat('processing'), // 处理中 // 检查是否有新工单(待处理数量增加)
fetchStatusStat('replied'), // 已回复 if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
fetchStatusStat('completed') // 已完成 try {
]) audio.play().catch(e => console.error('播放提示音失败:', e))
} catch (e) {
console.error('播放提示音出错:', e)
}
}
// 更新上一次的数量
previousPendingCount.value = data.wait_count
stats.total = data.all_count
stats.pending = data.wait_count
stats.replied = data.reply_count
stats.completed = data.close_count
// 计算处理中的数量:总数 - 待处理 - 已回复 - 已完成
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
} else {
console.error('获取工单统计失败:', res.message)
}
} catch (error) { } catch (error) {
console.error('获取工单统计数据出错:', error) console.error('获取工单统计数据出错:', error)
} finally { } finally {
@@ -413,18 +413,12 @@ const fetchAllStats = async () => {
} }
} }
// 刷新当前分类的统计数据(用于定时刷新,减少请求 // 刷新统计数据(用于定时刷新)
const fetchCurrentStatusStat = async () => { const fetchCurrentStatusStat = async () => {
try { await fetchAllStats()
// 只获取当前选中分类的统计数据
await fetchStatusStat(activeStatus.value)
// 同时获取全部工单数量(因为顶部显示需要)
await fetchStatusStat('')
} catch (error) {
console.error('获取当前分类统计数据出错:', error)
}
} }
// 加载更多工单 // 加载更多工单
const loadMoreTickets = () => { const loadMoreTickets = () => {
if (!hasMore.value || isLoading.value) return if (!hasMore.value || isLoading.value) return
@@ -476,9 +470,9 @@ const filteredTickets = computed(() => {
}) })
}) })
// 判断是否是客服 // 判断是否是当前登录的管理员
const isAdmin = (userId) => { const isAdmin = (userId) => {
return adminUserIds.includes(userId) return userId === userStore.userInfo?.user_id
} }
// 状态转换 // 状态转换
@@ -540,25 +534,25 @@ const fetchTicketMessages = async (workId) => {
} }
// 处理消息列表 // 处理消息列表
if (detail.Content && detail.Content.length > 0) { if (detail.content && detail.content.length > 0) {
// 使用Promise.all一次性处理所有消息和图片 // 处理所有消息
const messagesPromises = detail.Content.map(async (msg) => { const messages = detail.content.map((msg) => {
const isAdminMsg = isAdmin(msg.UserId) const isAdminMsg = isAdmin(msg.user?.userId)
const images = await parseFilesToImages(msg.Flies) // 从 flies 数组中提取图片 URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
return { return {
id: msg.Id, id: msg.id,
content: msg.Content !== 'empty' ? msg.Content : null, content: msg.content !== 'empty' ? msg.content : null,
images: images, images: images,
time: new Date(msg.CreatedAt).toLocaleString(), time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
isAdmin: isAdminMsg, isAdmin: isAdminMsg,
isSystem: false, isSystem: false,
userId: msg.UserId userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
} }
}) })
// 等待所有消息处理完成
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages currentMessages.value = messages
} }
} else { } else {
@@ -589,23 +583,29 @@ const sendMessage = async () => {
const fileIds = [] const fileIds = []
try { try {
// 添加一个临时的"正在发送"消息 // 保存输入内容
const inputMsg = messageInput.value.trim()
const inputImages = [...selectedImages.value]
// 清空输入和已选图片
messageInput.value = ''
selectedImages.value = []
// 立即添加消息到界面(不显示 loading)
const tempMsg = { const tempMsg = {
content: messageInput.value.trim() || null, id: Date.now(), // 临时 ID
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null, content: inputMsg || null,
images: inputImages.length > 0 ? inputImages : [],
time: new Date().toLocaleString(), time: new Date().toLocaleString(),
isAdmin: true, isAdmin: true,
isLoading: true, isSystem: false,
userId: userStore.userInfo?.user_id,
avatar: userStore.userInfo?.cover_url || '',
isTempMessage: true isTempMessage: true
} }
currentMessages.value.push(tempMsg) currentMessages.value.push(tempMsg)
// 清空输入和已选图片
const inputMsg = messageInput.value
messageInput.value = ''
selectedImages.value = []
// 滚动到底部 // 滚动到底部
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -633,6 +633,7 @@ const sendMessage = async () => {
// 恢复输入内容 // 恢复输入内容
messageInput.value = inputMsg messageInput.value = inputMsg
selectedImages.value = inputImages
ElMessage.error(res.message || '发送失败') ElMessage.error(res.message || '发送失败')
} }
@@ -729,42 +730,149 @@ const updateTicketStats = () => {
// 格式化消息时间 // 格式化消息时间
const formatMessageTime = (timeStr) => { const formatMessageTime = (timeStr) => {
const date = new Date(timeStr) if (!timeStr) return ''
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return ''
// 格式化为 HH:MM
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
} catch (e) {
console.error('时间格式化失败:', e)
return ''
}
} }
// 格式化列表项时间 // 格式化列表项时间
const formatTime = (timeStr) => { const formatTime = (timeStr) => {
const date = new Date(timeStr) if (!timeStr) return ''; // 空值兜底
const now = new Date()
const diff = now - date // 步骤1:解析中文时间字符串(核心适配点)
let date;
// 今天内的消息只显示时间 try {
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) { // 先尝试原生解析(兼容ISO格式)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) date = new Date(timeStr);
// 若原生解析失败(返回Invalid Date),解析中文格式
if (isNaN(date.getTime())) {
// 正则提取中文时间的年、月、日、时、分、秒
const cnTimeMatch = timeStr.match(
/(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/
);
if (cnTimeMatch) {
const [, year, month, day, period, hour, minute, second] = cnTimeMatch;
// 处理下午/上午的小时转换(12小时制转24小时制)
let hour24 = parseInt(hour, 10);
if (period === '下午' && hour24 !== 12) {
hour24 += 12;
}
if (period === '上午' && hour24 === 12) {
hour24 = 0; // 上午12点转为0点
}
// 构造日期(月份从0开始,需-1)
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; // 既不是ISO也不是中文格式
}
}
} catch (e) {
console.error('时间解析失败:', e);
return '无效时间';
} }
const now = new Date();
const dateTime = date.getTime();
const nowTime = now.getTime();
const diff = nowTime - dateTime;
// 步骤2:判断“今天”(年/月/日完全一致)
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
// 一周内的显示星期几 if (isToday) {
if (diff < 7 * 24 * 60 * 60 * 1000) { // 格式化今天的时间(24小时制,补零)
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] const hour = String(date.getHours()).padStart(2, '0');
return weekdays[date.getDay()] const minute = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
} }
// 其他显示日期 // 步骤3:判断“一周内”
return date.toLocaleDateString() const oneWeek = 7 * 24 * 60 * 60 * 1000;
} if (diff < oneWeek) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekdays[date.getDay()];
}
// 步骤4:格式化其他日期(补零,统一格式:YYYY/MM/DD)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
};
// 格式化日期显示 // 格式化日期显示
const formatDate = (timeStr) => { const formatDate = (timeStr) => {
const date = new Date(timeStr) console.log("原始时间字符串:", timeStr);
const now = new Date() if (!timeStr) return ''; // 空值兜底
if (date.toDateString() === now.toDateString()) { let date;
return '今天' // 1. 先尝试原生解析(兼容ISO等标准格式)
date = new Date(timeStr);
// 2. 若原生解析失败,专门解析中文时间格式
if (isNaN(date.getTime())) {
// 正则匹配:xxxx年xx月xx日 上午/下午 xx:xx:xx
const cnTimeReg = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/;
const match = timeStr.match(cnTimeReg);
if (match) {
const [, year, month, day, period, hour, minute, second] = match;
// 处理12小时制转24小时制(关键适配)
let hour24 = parseInt(hour, 10);
if (period === '下午') {
hour24 = hour24 === 12 ? 12 : hour24 + 12; // 下午12点=12点,下午1-11点+12
} else { // 上午
hour24 = hour24 === 12 ? 0 : hour24; // 上午12点=0点,上午1-11点不变
}
// 手动构造合法的Date对象(月份从0开始,需-1)
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; // 非目标格式,返回兜底
}
} }
return date.toLocaleDateString() const now = new Date();
} // 3. 对比“今天”(按日期维度,忽略时分秒)
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) {
return '今天';
}
// 4. 格式化非今天的日期(统一格式,避免环境差异)
const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
return formattedDate;
};
// 监听显示工单变化,更新消息和滚动 // 监听显示工单变化,更新消息和滚动
watch(currentTicket, (newVal) => { watch(currentTicket, (newVal) => {
@@ -838,8 +946,9 @@ const refreshTicketList = async () => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: `用户${item.user_id}`, username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user_id, userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
@@ -874,6 +983,39 @@ const refreshTicketList = async () => {
} }
} }
// 辅助函数:去除 URL 中的查询参数
const normalizeUrl = (url) => {
if (!url) return ''
return url.split('?')[0]
}
// 辅助函数:比较两个消息数组是否相同(忽略 URL 查询参数)
const areMessagesEqual = (messages1, messages2) => {
if (messages1.length !== messages2.length) return false
for (let i = 0; i < messages1.length; i++) {
const msg1 = messages1[i]
const msg2 = messages2[i]
// 比较消息 ID 和内容
if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false
// 比较图片数量
if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false
// 比较图片 URL(去除查询参数)
if (msg1.images && msg2.images) {
for (let j = 0; j < msg1.images.length; j++) {
if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) {
return false
}
}
}
}
return true
}
// 静默刷新聊天记录(不显示loading状态) // 静默刷新聊天记录(不显示loading状态)
const refreshTicketMessages = async (workId) => { const refreshTicketMessages = async (workId) => {
try { try {
@@ -886,37 +1028,33 @@ const refreshTicketMessages = async (workId) => {
if (currentTicket.value) { if (currentTicket.value) {
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新 // 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
if (currentTicket.value.status !== 'pending') { if (currentTicket.value.status !== 'pending') {
currentTicket.value.status = convertStatusToString(detail.Status) currentTicket.value.status = convertStatusToString(detail.status)
} }
} }
// 处理消息列表 // 处理消息列表
if (detail.Content && detail.Content.length > 0) { if (detail.content && detail.content.length > 0) {
// 检查是否有新消息 // 构建新消息列表
const lastMsgId = currentMessages.value.length > 0 ? const newMessages = detail.content.map((msg) => {
currentMessages.value[currentMessages.value.length - 1].id : 0; const isAdminMsg = isAdmin(msg.user?.userId)
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId); // 从 flies 数组中提取图片 URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
if (hasNewMessage) {
// 有新消息时才更新
const messagesPromises = detail.Content.map(async (msg) => {
const isAdminMsg = isAdmin(msg.UserId)
const images = await parseFilesToImages(msg.Flies)
return {
id: msg.Id,
content: msg.Content !== 'empty' ? msg.Content : null,
images: images,
time: new Date(msg.CreatedAt).toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.UserId
}
})
// 等待所有消息处理完成 return {
const messages = await Promise.all(messagesPromises) id: msg.id,
currentMessages.value = messages content: msg.content !== 'empty' ? msg.content : null,
images: images,
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
}
})
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化)
if (!areMessagesEqual(currentMessages.value, newMessages)) {
currentMessages.value = newMessages
// 如果有新消息,滚动到底部 // 如果有新消息,滚动到底部
nextTick(() => { nextTick(() => {
File diff suppressed because it is too large Load Diff
+394
View File
@@ -0,0 +1,394 @@
<template>
<div class="ticket-list-page">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="status-tabs">
<div class="tab-item pending" :class="{ active: activeStatus === 'pending' }" @click="filterByStatus('pending')">
待处理 <span class="count">{{ stats.pending }}</span>
</div>
<div class="tab-item processing" :class="{ active: activeStatus === 'processing' }" @click="filterByStatus('processing')">
处理中 <span class="count">{{ stats.processing }}</span>
</div>
<div class="tab-item replied" :class="{ active: activeStatus === 'replied' }" @click="filterByStatus('replied')">
已回复 <span class="count">{{ stats.replied }}</span>
</div>
<div class="tab-item completed" :class="{ active: activeStatus === 'completed' }" @click="filterByStatus('completed')">
已完成 <span class="count">{{ stats.completed }}</span>
</div>
<div class="tab-item" :class="{ active: activeStatus === '' }" @click="filterByStatus('')">
全部 <span class="count">{{ stats.total }}</span>
</div>
</div>
<div class="toolbar-right">
<el-select v-model="sortBy" placeholder="排序方式" clearable style="width: 140px" @change="handleSortChange">
<el-option label="不排序" value="" />
<el-option label="创建时间" value="created_at" />
<el-option label="更新时间" value="updated_at" />
<el-option label="工单号" value="id" />
</el-select>
<el-select v-model="sortOrder" placeholder="排序顺序" clearable style="width: 100px" @change="handleSortChange">
<el-option label="默认" value="" />
<el-option label="降序" value="desc" />
<el-option label="升序" value="asc" />
</el-select>
<el-input
v-model="searchKeyword"
placeholder="搜索工单号、标题、用户名"
prefix-icon="Search"
clearable
style="width: 240px"
@input="handleSearch"
/>
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
</div>
</div>
<!-- 工单表格 -->
<el-table
v-loading="isLoading"
:data="filteredTickets"
stripe
style="width: 100%"
@row-click="handleRowClick"
>
<el-table-column prop="id" label="工单号" width="100" />
<el-table-column label="用户" width="180">
<template #default="{ row }">
<div class="user-info">
<el-avatar :size="32" :src="row.avatar">{{ row.username?.charAt(0) }}</el-avatar>
<span class="username">{{ row.username }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="title" label="工单标题" min-width="200" show-overflow-tooltip />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="lastReplyTime" label="最后回复" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click.stop="goToDetail(row)">
回复
</el-button>
<el-button
v-if="row.status !== 'completed'"
type="success"
size="small"
@click.stop="handleComplete(row)"
>
结束
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onActivated } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getTickerList,
closeTicket,
getTicketCount
} from '@/api/ticket'
const router = useRouter()
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const isLoading = ref(false)
// 工单数据
const ticketList = ref([])
const searchKeyword = ref('')
const activeStatus = ref('pending') // 默认选中"待处理"
// 排序
const sortBy = ref('') // 默认不排序
const sortOrder = ref('') // 默认不选择排序顺序
// 统计数据
const stats = reactive({
pending: 0,
processing: 0,
replied: 0,
completed: 0,
total: 0
})
// 状态转换
const convertStatusToString = (status) => {
const statusMap = { 0: 'pending', 1: 'processing', 2: 'replied', 3: 'completed' }
return statusMap[status] || 'processing'
}
const getStatusText = (status) => {
const statusMap = { pending: '待处理', processing: '处理中', replied: '已回复', completed: '已完成' }
return statusMap[status] || status
}
const getStatusType = (status) => {
const typeMap = { pending: 'warning', processing: 'primary', replied: 'info', completed: 'success' }
return typeMap[status] || ''
}
// 获取工单列表
const fetchTicketList = async () => {
try {
isLoading.value = true
let statusParam = ''
if (activeStatus.value) {
const statusMap = { pending: '0', processing: '1', replied: '2', completed: '3' }
statusParam = statusMap[activeStatus.value] || ''
}
console.log('调用getTickerList,排序参数:', { sortBy: sortBy.value, sortOrder: sortOrder.value })
const res = await getTickerList(
pageSize.value,
currentPage.value,
statusParam,
sortBy.value,
sortOrder.value
)
if (res.code === 200) {
ticketList.value = (res.data.data || []).map(item => ({
id: item.work_id,
title: item.name,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status)
}))
totalCount.value = res.data.all_count || 0
} else {
ElMessage.error(res.message || '获取工单列表失败')
}
} catch (error) {
console.error('获取工单列表出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
isLoading.value = false
}
}
// 获取统计数据
const fetchStats = async () => {
try {
const res = await getTicketCount()
if (res.code === 200) {
const data = res.data
stats.total = data.all_count
stats.pending = data.wait_count
stats.replied = data.reply_count
stats.completed = data.close_count
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
}
} catch (error) {
console.error('获取统计数据出错:', error)
}
}
// 过滤后的工单列表
const filteredTickets = computed(() => {
if (!searchKeyword.value) return ticketList.value
const keyword = searchKeyword.value.toLowerCase()
return ticketList.value.filter(ticket =>
ticket.title.toLowerCase().includes(keyword) ||
ticket.username.toLowerCase().includes(keyword) ||
String(ticket.id).includes(keyword)
)
})
// 按状态过滤
const filterByStatus = (status) => {
if (activeStatus.value === status) return
activeStatus.value = status
currentPage.value = 1
fetchTicketList()
}
// 排序变化处理
const handleSortChange = () => {
currentPage.value = 1
fetchTicketList()
}
// 搜索处理
const handleSearch = () => {}
// 分页处理
const handleSizeChange = () => {
currentPage.value = 1
fetchTicketList()
}
const handlePageChange = () => {
fetchTicketList()
}
// 刷新列表
const refreshList = () => {
fetchTicketList()
fetchStats()
}
// 跳转到详情页
const goToDetail = (row) => {
router.push({ path: '/ticket/detail', query: { id: row.id } })
}
const handleRowClick = (row) => {
goToDetail(row)
}
// 结束工单
const handleComplete = (ticket) => {
ElMessageBox.confirm('确定要结束此工单吗?结束后将无法继续回复。', '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await closeTicket(ticket.id)
if (res.code === 200) {
ElMessage.success('工单已成功结束')
refreshList()
} else {
ElMessage.error(res.message || '结束工单失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
}
}).catch(() => {})
}
let isFirstLoad = true
onMounted(() => {
fetchTicketList()
fetchStats()
})
// 当页面被激活时(从详情页返回时)
onActivated(() => {
// 跳过首次加载,只在从其他页面返回时刷新
if (!isFirstLoad) {
refreshList()
}
isFirstLoad = false
})
</script>
<style scoped>
.ticket-list-page {
padding: 0;
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
background: #fff;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 50px;
border-bottom: 1px solid #ebeef5;
}
.status-tabs {
display: flex;
gap: 8px;
}
.tab-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.2s;
}
.tab-item:hover {
background: #f5f7fa;
}
.tab-item.active {
background: #409eff;
color: #fff;
}
.tab-item.pending.active { background: #e6a23c; }
.tab-item.processing.active { background: #409eff; }
.tab-item.replied.active { background: #909399; }
.tab-item.completed.active { background: #67c23a; }
.tab-item .count {
margin-left: 4px;
font-weight: 500;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.username {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 12px 20px;
border-top: 1px solid #ebeef5;
}
:deep(.el-table) {
flex: 1;
}
:deep(.el-table tr) {
cursor: pointer;
}
</style>
File diff suppressed because it is too large Load Diff
+79 -22
View File
@@ -304,23 +304,37 @@
</el-dialog> </el-dialog>
<!-- Token 展示对话框 --> <!-- Token 展示对话框 -->
<el-dialog v-model="tokenDialogVisible" title="模拟登录 Token" width="600px" append-to-body> <el-dialog v-model="tokenDialogVisible" title="模拟登录" width="450px" append-to-body>
<div class="token-container"> <div class="token-container">
<el-alert type="success" :closable="false" show-icon class="token-alert"> <el-form label-width="100px">
<template #default> <el-form-item label="选择环境">
<span>Token 生成成功请妥善保管</span> <el-select v-model="selectedEnvironment" placeholder="请选择登录环境" size="large" style="width: 100%">
</template> <el-option label="正式环境" value="production">
</el-alert> <div class="env-option">
<div class="token-box"> <span>正式环境</span>
<el-input v-model="loginToken" type="textarea" :rows="4" readonly class="token-input" /> <span class="env-url">www.007yjs.com</span>
<div class="token-actions"> </div>
<el-button type="primary" link :icon="CopyDocument" @click="copyToken">复制</el-button> </el-option>
</div> <el-option label="测试环境" value="test">
</div> <div class="env-option">
<span>测试环境</span>
<span class="env-url">apiserver.s1f.ren</span>
</div>
</el-option>
<el-option label="本地环境" value="local">
<div class="env-option">
<span>本地环境</span>
<span class="env-url">localhost:5173</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="tokenDialogVisible = false">关闭</el-button> <el-button @click="tokenDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmJump" :disabled="!selectedEnvironment">确认跳转</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@@ -364,7 +378,7 @@ import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import { import {
ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock, ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock,
UserFilled, Document, Clock, List, Switch, User, Camera, Upload, UserFilled, Document, Clock, List, Switch, User, Camera, Upload,
UploadFilled, Key, CopyDocument UploadFilled, Key, Monitor, Setting
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { getUserGroupList } from '@/api/admin/user' import { getUserGroupList } from '@/api/admin/user'
import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file' import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file'
@@ -379,6 +393,10 @@ const Edit = EditIcon
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// 引入tagsViewStore
import { useTagsViewStore } from '@/store/tagsViewStore'
const tagsViewStore = useTagsViewStore()
// 用户信息 // 用户信息
const userInfo = ref({}) const userInfo = ref({})
const loading = ref(false) const loading = ref(false)
@@ -465,6 +483,15 @@ const realnameForm = reactive({
// Token 展示相关 // Token 展示相关
const tokenDialogVisible = ref(false) const tokenDialogVisible = ref(false)
const loginToken = ref('') const loginToken = ref('')
const loginExpire = ref('')
const selectedEnvironment = ref('')
// 环境配置
const environments = {
production: 'https://www.007yjs.com',
test: 'https://apiserver.s1f.ren',
local: 'http://localhost:5173'
}
// 管理员权限管理相关 // 管理员权限管理相关
const adminDialogVisible = ref(false) const adminDialogVisible = ref(false)
@@ -523,6 +550,8 @@ const refreshData = () => {
// 返回上一页 // 返回上一页
const goBack = () => { const goBack = () => {
// 关闭当前tab
tagsViewStore.delVisitedView(route)
router.go(-1) router.go(-1)
} }
@@ -729,8 +758,10 @@ const handleSimulateLogin = async () => {
const res = await mockUserLogin({ user_id: userInfo.value.UserId }) const res = await mockUserLogin({ user_id: userInfo.value.UserId })
if (res.data.code === 200) { if (res.data.code === 200) {
loginToken.value = res.data.data.token || '' loginToken.value = res.data.data.token || ''
loginExpire.value = res.data.data.expire_time || ''
selectedEnvironment.value = ''
tokenDialogVisible.value = true tokenDialogVisible.value = true
ElMessage.success('模拟登录成功') //ElMessage.success('模拟登录成功')
} else { } else {
ElMessage.error(res.data.message || '模拟登录失败') ElMessage.error(res.data.message || '模拟登录失败')
} }
@@ -739,14 +770,18 @@ const handleSimulateLogin = async () => {
} }
} }
// 复制 Token // 确认跳转
const copyToken = async () => { const confirmJump = () => {
try { if (!selectedEnvironment.value) {
await navigator.clipboard.writeText(loginToken.value) ElMessage.warning('请选择登录环境')
ElMessage.success('Token 已复制到剪贴板') return
} catch (err) {
ElMessage.error('复制失败,请手动复制')
} }
const baseUrl = environments[selectedEnvironment.value]
const url = `${baseUrl}/token-login?token=${loginToken.value}&expire=${loginExpire.value}`
window.open(url, '_blank')
const envName = selectedEnvironment.value === 'production' ? '正式' : selectedEnvironment.value === 'test' ? '测试' : '本地'
ElMessage.success(`正在跳转到${envName}环境`)
tokenDialogVisible.value = false
} }
// 获取实名状态文本 // 获取实名状态文本
@@ -1166,6 +1201,28 @@ onActivated(() => {
font-size: 13px; font-size: 13px;
} }
/* Token Dialog */
.token-container {
padding: 20px 0;
}
.env-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
}
.env-option span:first-child {
font-size: 14px;
color: #303133;
}
.env-url {
font-size: 12px;
color: #909399;
}
/* Responsive */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.detail-grid { .detail-grid {
+28 -24
View File
@@ -129,6 +129,7 @@
<el-dropdown-item command="password">修改密码</el-dropdown-item> <el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="group">修改用户组</el-dropdown-item> <el-dropdown-item command="group">修改用户组</el-dropdown-item>
<el-dropdown-item command="realname">实名信息</el-dropdown-item> <el-dropdown-item command="realname">实名信息</el-dropdown-item>
<el-dropdown-item command="balance">余额管理</el-dropdown-item>
<el-dropdown-item command="loginHistory">登录记录</el-dropdown-item> <el-dropdown-item command="loginHistory">登录记录</el-dropdown-item>
<el-dropdown-item command="operationHistory">操作记录</el-dropdown-item> <el-dropdown-item command="operationHistory">操作记录</el-dropdown-item>
<el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item> <el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item>
@@ -512,12 +513,24 @@ const fetchUserList = async () => {
const res = await getUserList(queryParams) const res = await getUserList(queryParams)
console.log("获取用户列表:", res) console.log("获取用户列表:", res)
if (res.data.code === 200) { if (res.data.code === 200) {
userList.value = res.data.data.data || [] // 映射 API 返回的字段到组件使用的字段格式
userList.value = (res.data.data.data || []).map(user => ({
UserId: user.user_id,
UserName: user.user_name,
Phone: user.phone,
Email: user.email,
Sex: user.sex,
Age: user.age,
IsAdmin: user.is_admin,
CoverID: user.cover_id,
avatarUrl: user.cover || '', // 直接使用 cover 字段作为头像 URL
UserGroup: user.user_group,
RealName: user.real_name,
IsDeleted: user.is_deleted,
CreatedAt: user.created_at
}))
console.log("用户列表:", userList.value) console.log("用户列表:", userList.value)
total.value = res.data.data.all_count || 0 total.value = res.data.data.all_count || 0
// 为每个用户加载头像URL
await loadAvatarsForUsers()
} }
} catch (error) { } catch (error) {
ElMessage.error('获取用户列表失败') ElMessage.error('获取用户列表失败')
@@ -526,26 +539,6 @@ const fetchUserList = async () => {
} }
} }
// 为用户列表加载头像URL
const loadAvatarsForUsers = async () => {
const promises = userList.value.map(async (user) => {
if (user.CoverID) {
try {
const res = await getFileDetail({ file_id: user.CoverID })
if (res.data.code === 200) {
user.avatarUrl = res.data.data.url
}
} catch (error) {
console.error('加载头像失败:', error)
user.avatarUrl = ''
}
} else {
user.avatarUrl = ''
}
})
await Promise.all(promises)
}
// 查询用户列表 // 查询用户列表
const handleQuery = () => { const handleQuery = () => {
queryParams.page = 1 queryParams.page = 1
@@ -676,6 +669,9 @@ const handleCommand = (command, row) => {
case 'realname': case 'realname':
handleRealnameModify(row) handleRealnameModify(row)
break break
case 'balance':
handleBalanceManage(row)
break
case 'loginHistory': case 'loginHistory':
handleLoginHistory(row) handleLoginHistory(row)
break break
@@ -691,6 +687,14 @@ const handleCommand = (command, row) => {
} }
} }
// 余额管理
const handleBalanceManage = (row) => {
router.push({
path: '/user/balance',
query: { user_id: row.UserId }
})
}
// 模拟登录 // 模拟登录
const handleSimulateLogin = async (row) => { const handleSimulateLogin = async (row) => {
+18 -598
View File
@@ -7,293 +7,13 @@
}, },
"tags": [], "tags": [],
"paths": { "paths": {
"/api/v1/admin/server/setting/group/list": { "/api/v1/admin/order/list": {
"get": { "get": {
"summary": "获取配置分组列表", "summary": "获取订单列表",
"deprecated": false, "deprecated": false,
"description": "", "description": "",
"tags": [], "tags": [],
"parameters": [ "parameters": [
{
"name": "page",
"in": "query",
"description": "获取页码 默认 1",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "count",
"in": "query",
"description": "获取条数 默认 10",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "key",
"in": "query",
"description": "关键词筛选",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/group/info": {
"get": {
"summary": "获取配置分组信息",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "setting_group_id",
"in": "query",
"description": "",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/group/create": {
"post": {
"summary": "创建配置分组",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"name": {
"description": "名称",
"example": "",
"type": "string"
},
"note": {
"description": "备注",
"example": "",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
},
"example": {
"code": 200,
"message": "Success"
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/group/update": {
"post": {
"summary": "修改配置分组",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"description": "ID",
"example": "",
"type": "string"
},
"name": {
"description": "名称",
"example": "",
"type": "string"
},
"note": {
"description": "备注",
"example": "",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/group/delete": {
"delete": {
"summary": "删除配置分组",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "setting_group_id",
"in": "query",
"description": "",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/list": {
"get": {
"summary": "获取配置列表",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "page",
"in": "query",
"description": "获取页码 默认 1",
"required": false,
"schema": {
"type": "string"
}
},
{ {
"name": "count", "name": "count",
"in": "query", "in": "query",
@@ -304,18 +24,9 @@
} }
}, },
{ {
"name": "group_id", "name": "page",
"in": "query", "in": "query",
"description": "组id(与组名称二选一)", "description": "获取页码 默认 1",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "group_name",
"in": "query",
"description": "组名称(与组id二选一)",
"required": false, "required": false,
"schema": { "schema": {
"type": "string" "type": "string"
@@ -331,53 +42,27 @@
} }
}, },
{ {
"name": "Authorization", "name": "state",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/info": {
"get": {
"summary": "获取配置信息",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "id",
"in": "query", "in": "query",
"description": "配置id (与name二选一)", "description": "状态筛选",
"required": false, "required": false,
"schema": { "schema": {
"type": "string" "type": "string"
} }
}, },
{ {
"name": "name", "name": "user_id",
"in": "query", "in": "query",
"description": "配置名称 (与id二选一)", "description": "用户id筛选",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "user_key",
"in": "query",
"description": "用户关键词筛选(用户名 手机号 邮箱)",
"required": false, "required": false,
"schema": { "schema": {
"type": "string" "type": "string"
@@ -410,276 +95,11 @@
}, },
"security": [] "security": []
} }
},
"/api/v1/admin/server/setting/create": {
"post": {
"summary": "创建配置",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"name": {
"description": "名称",
"example": "",
"type": "string"
},
"value": {
"example": "",
"type": "string"
},
"note": {
"description": "备注",
"example": "",
"type": "string"
},
"type": {
"description": "类型 string/int/float/bool/",
"example": "",
"type": "string"
},
"setting_group_id": {
"description": "配置组id",
"example": 0,
"type": "integer"
},
"open": {
"description": "是否开放访问",
"example": "",
"type": "boolean"
}
},
"required": [
"name",
"value",
"type"
]
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/update": {
"post": {
"summary": "修改配置",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"example": 0,
"type": "integer"
},
"name": {
"description": "名称",
"example": "",
"type": "string"
},
"value": {
"example": "",
"type": "string"
},
"note": {
"description": "备注",
"example": "",
"type": "string"
},
"type": {
"description": "类型 string/int/float/bool/",
"example": "",
"type": "string"
},
"setting_group_id": {
"description": "配置组id",
"example": "",
"type": "string"
}
},
"required": [
"id"
]
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/set_open": {
"post": {
"summary": "修改配置是否开放访问",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"example": 0,
"type": "integer"
},
"open": {
"description": "是否开放",
"example": "",
"type": "boolean"
}
},
"required": [
"id",
"open"
]
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/setting/delete": {
"delete": {
"summary": "删除配置",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"example": "",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
} }
}, },
"components": { "components": {
"schemas": {}, "schemas": {},
"responses": {},
"securitySchemes": {} "securitySchemes": {}
}, },
"servers": [], "servers": [],