Compare commits
98 Commits
777022632c
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7394afb83f | |||
| c43d1978a8 | |||
| 475c62aefc | |||
| c0daa6ed11 | |||
| 2e073c2b87 | |||
| 13248468d3 | |||
| ab7a8d5cfa | |||
| 64d40cbbbf | |||
| d72a4f804e | |||
| 8b2251ef97 | |||
| 2916c04ba5 | |||
| c7245cec67 | |||
| 985412c3bc | |||
| f53f63e679 | |||
| cae1f847e4 | |||
| 5428f01cdf | |||
| 7652b290b0 | |||
| cf188bb94a | |||
| b3ed406f84 | |||
| 2f06aa9f5f | |||
| f0e89695f4 | |||
| c07e09c151 | |||
| 71d3605f4f | |||
| b7e806cc80 | |||
| 1a4587f893 | |||
| 40a5e486a6 | |||
| 3357566b02 | |||
| 25d782b050 | |||
| 9edb59d16e | |||
| cf19956b88 | |||
| cd16ec17ae | |||
| f4dbf17ce9 | |||
| 25975c8b29 | |||
| d650bfeb61 | |||
| 3e751d4c42 | |||
| 193db5735f | |||
| 09a83f4985 | |||
| 20790cf029 | |||
| 86f3835e51 | |||
| a2a7644a9f | |||
| 3ca956d9f0 | |||
| 5d16589e54 | |||
| 255bd9e832 | |||
| 2e82ff8a34 | |||
| c100c37a32 | |||
| cdd8f86b92 | |||
| fe29a8b3d0 | |||
| 2f38932878 | |||
| fdc9db9a9c | |||
| 4d45cf535e | |||
| 9d8f23262b | |||
| e96e9c4a7e | |||
| 5a31de64b3 | |||
| b4260fedb8 | |||
| 793a96a44f | |||
| 043be60f4f | |||
| 127d54eaa6 | |||
| ead7c5bba5 | |||
| 5b5e0f62ec | |||
| 9105503850 | |||
| 20260e221c | |||
| a270f58500 | |||
| 7992ee9902 | |||
| 084aeebf13 | |||
| 1e79005440 | |||
| a6d4d70221 | |||
| e3e70114fb | |||
| 0b57581799 | |||
| 36271b8bd0 | |||
| cae89dd5ad | |||
| d3479fb0bb | |||
| 98cb0e1c8e | |||
| 779359cec5 | |||
| 60f141a0a9 | |||
| fe1a118132 | |||
| 2ce2c1a31f | |||
| 1655d86f6b | |||
| fcebebd216 | |||
| 5a93f4f8a8 | |||
| 4d10deef86 | |||
| cf7ac515f6 | |||
| 4ef208a662 | |||
| f6dcec75d7 | |||
| 4cc684eca6 | |||
| 00ea1845a7 | |||
| 0c6166b3c7 | |||
| 978b18d5d5 | |||
| 54f78e15fe | |||
| ab2df50c0d | |||
| 6859753470 | |||
| 32bb4502e7 | |||
| 4a13048718 | |||
| b56359e572 | |||
| 41d6492daf | |||
| 14fcac3a24 | |||
| 0fc582bc8c | |||
| 0fe4ece1a9 | |||
| a09631551b |
@@ -17,7 +17,12 @@ store封装到src/store目录下。
|
||||
|
||||
注册侧边栏在/config/menus.js文件中。
|
||||
|
||||
新添加要求:
|
||||
在遇到用户id需要填写和修改的弹窗将其修改为可预览样式
|
||||
关于填写表单为推荐人id的需要使用组件AvatarSelector展示,如果是文件id或者是封面id 的也需要预览展示需要向头像列表组件一样,可以弄个文件组件/api/v1/admin/file/list这个是文件列表接口
|
||||
|
||||
规则:
|
||||
1.只要涉及弹窗添加和修改xxxid类型的就需要生成一个弹窗组件并使用到页面中
|
||||
|
||||
## 1. 基础布局规范
|
||||
```css
|
||||
|
||||
@@ -37,10 +37,10 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ninBo
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||
with:
|
||||
name: vue3-build
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ninBo
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||
with:
|
||||
name: vue3-build
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# 管理员后台pc端
|
||||
|
||||
# 007UI 后台管理系统
|
||||
|
||||
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/logo.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="007UI - 高端蓝色扁平化后台管理系统模板" />
|
||||
<meta name="keywords" content="管理系统,后台,模板,Vue3,ElementPlus" />
|
||||
|
||||
Generated
+984
-931
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
+72
-1
@@ -226,11 +226,16 @@ html, body {
|
||||
color: #3498db !important;
|
||||
}
|
||||
|
||||
/* 卡片扁平化 */
|
||||
/* 卡片扁平化 + 层次感 */
|
||||
.el-card {
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid #e1e8ed !important;
|
||||
box-shadow: none !important;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.el-card[shadow="hover"]:hover {
|
||||
border-color: #c0c4cc !important;
|
||||
box-shadow: 0 2px 12px rgba(44, 62, 80, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 表格扁平化 */
|
||||
@@ -434,4 +439,70 @@ html, body {
|
||||
.el-dialog .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Descriptions 描述列表增强 */
|
||||
.el-descriptions {
|
||||
--el-descriptions-item-bordered-label-background: #fafbfc;
|
||||
}
|
||||
.el-descriptions__label {
|
||||
color: #606266 !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.el-descriptions__content {
|
||||
color: #1d2129 !important;
|
||||
}
|
||||
|
||||
/* Loading 遮罩增强 */
|
||||
.el-loading-mask {
|
||||
background-color: rgba(255, 255, 255, 0.85) !important;
|
||||
}
|
||||
.el-loading-spinner .circular {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.el-loading-spinner .el-loading-text {
|
||||
color: #606266 !important;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Message Box 增强 */
|
||||
.el-message-box {
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15) !important;
|
||||
}
|
||||
.el-message-box__header {
|
||||
padding: 16px 20px 12px !important;
|
||||
}
|
||||
.el-message-box__title {
|
||||
font-weight: 600 !important;
|
||||
color: #1d2129 !important;
|
||||
}
|
||||
.el-message-box__btns .el-button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Alert 增强 */
|
||||
.el-alert {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Tabs 增强 */
|
||||
.el-tabs__item {
|
||||
transition: color 0.2s ease !important;
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Switch 开关增强 */
|
||||
.el-switch {
|
||||
--el-switch-on-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 全局链接按钮悬浮下划线 */
|
||||
.el-button.is-link:hover,
|
||||
.el-button--primary.is-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,768 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务管理 API
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取 KVM 主控服务列表 */
|
||||
export const getKvmServiceList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/list', { params })
|
||||
}
|
||||
|
||||
/** 获取 KVM 主控服务详情 */
|
||||
export const getKvmServiceDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建 KVM 主控服务 */
|
||||
export const createKvmService = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改 KVM 主控服务 */
|
||||
export const updateKvmService = (id, data) => {
|
||||
return http2.post(`/api/v1/admin/server/host_service/update?id=${id}`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 KVM 主控服务 */
|
||||
export const deleteKvmService = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 宿主机组映射管理 API
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取本地主机组列表 */
|
||||
export const getHostGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/host_group/list', { params })
|
||||
}
|
||||
|
||||
/** 从远程同步主机组到本地 */
|
||||
export const syncHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定主机组到商品组或商品 */
|
||||
export const bindHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/bind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改本地主机组信息 */
|
||||
export const updateHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据主机组树自动生成 GoodGroup/Goods/Args */
|
||||
export const generateGoodsByHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/generate_goods', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除本地主机组 */
|
||||
export const deleteHostGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/host_group/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 远程宿主机组管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取远程主机组列表 */
|
||||
export const getRemoteHostGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/list', { params })
|
||||
}
|
||||
|
||||
/** 获取远程主机组详情 */
|
||||
export const getRemoteHostGroupDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取远程主机组树形结构 */
|
||||
export const getRemoteHostGroupTree = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/tree', { params })
|
||||
}
|
||||
|
||||
/** 获取主机组最优主机配置信息 */
|
||||
export const getOptimalHostInfo = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/optimal_host', { params })
|
||||
}
|
||||
|
||||
/** 创建远程主机组 */
|
||||
export const createRemoteHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host_group/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改远程主机组 */
|
||||
export const updateRemoteHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除远程主机组 */
|
||||
export const deleteRemoteHostGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/host_group/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 宿主机管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取宿主机列表 */
|
||||
export const getRemoteHostList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/list', { params })
|
||||
}
|
||||
|
||||
/** 获取宿主机详情 */
|
||||
export const getRemoteHostDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取宿主机指标数据 */
|
||||
export const getRemoteHostMetrics = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/metrics', { params })
|
||||
}
|
||||
|
||||
/** 查询历史指标(宿主机或虚拟机) */
|
||||
export const getMetricsHistory = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/metrics_history', { params })
|
||||
}
|
||||
|
||||
/** 新增宿主机 */
|
||||
export const addRemoteHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改宿主机 */
|
||||
export const updateRemoteHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除宿主机 */
|
||||
export const deleteRemoteHost = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/host/delete', { params })
|
||||
}
|
||||
|
||||
/** 创建宿主机注册令牌 */
|
||||
export const createHostToken = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/create_token', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 镜像管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取镜像列表 */
|
||||
export const getImageList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/list', { params })
|
||||
}
|
||||
|
||||
/** 获取镜像详情 */
|
||||
export const getImageDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取镜像在指定宿主机上的状态 */
|
||||
export const getImageHostStatus = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/host_status', { params })
|
||||
}
|
||||
|
||||
/** 创建镜像 */
|
||||
export const createImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改镜像 */
|
||||
export const updateImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除镜像 */
|
||||
export const deleteImage = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/image/delete', { params })
|
||||
}
|
||||
|
||||
/** 重新下载镜像 */
|
||||
export const reloadImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/reload', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 向宿主机同步镜像 */
|
||||
export const syncImageToHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 指定宿主机重新下载指定镜像 */
|
||||
export const reloadImageOnHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/reload_host', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取宿主机镜像列表与状态(对比) */
|
||||
export const getImageCompareHost = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/compare_host', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 网络管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取网络列表 */
|
||||
export const getNetworkList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/network/list', { params })
|
||||
}
|
||||
|
||||
/** 获取网络详情 */
|
||||
export const getNetworkDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/network/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建网络 */
|
||||
export const createNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改网络 */
|
||||
export const updateNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量创建网络 */
|
||||
export const batchCreateNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/batch_create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除网络 */
|
||||
export const deleteNetwork = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/network/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 数据卷管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取数据卷列表 */
|
||||
export const getVolumeList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/volume/list', { params })
|
||||
}
|
||||
|
||||
/** 获取数据卷详情 */
|
||||
export const getVolumeDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/volume/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建数据卷 */
|
||||
export const createVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 调整数据卷大小 */
|
||||
export const resizeVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/resize', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 挂载卷到虚拟机 */
|
||||
export const mountVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/mount', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 卸载卷 */
|
||||
export const unmountVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/unmount', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 迁移卷 */
|
||||
export const transferVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/transfer', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除卷 */
|
||||
export const deleteVolume = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/volume/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 虚拟机管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取虚拟机列表 */
|
||||
export const getVmList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/list', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机详情 */
|
||||
export const getVmDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机状态 */
|
||||
export const getVmStatus = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/status', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机指标数据 */
|
||||
export const getVmMetrics = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/metrics', { params })
|
||||
}
|
||||
|
||||
/** 创建虚拟机 */
|
||||
export const createVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改虚拟机 */
|
||||
export const updateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重建虚拟机 */
|
||||
export const rebuildVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/rebuild', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重构虚拟机 */
|
||||
export const refactorVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/refactor', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改虚拟机带宽 */
|
||||
export const updateVmTraffic = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/update_traffic', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 流量策略 ==========
|
||||
// 测试未通过(接口新增,待联调)
|
||||
/** 获取虚拟机流量策略 */
|
||||
export const getVmTrafficPolicy = (params) => http2.get('/api/v1/admin/server/host_service/point/vm/traffic_policy', { params })
|
||||
/** 修改虚拟机流量策略 */
|
||||
export const updateVmTrafficPolicy = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/update', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
/** 增加虚拟机固定流量上限 */
|
||||
export const addVmFixedTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_fixed', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
/** 增加虚拟机一次性临时流量 */
|
||||
export const addVmTemporaryTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_temporary', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
/** 启动虚拟机 */
|
||||
export const startVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/start', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 停止虚拟机 */
|
||||
export const stopVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/stop', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重启虚拟机 */
|
||||
export const rebootVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/reboot', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 暂停虚拟机 */
|
||||
export const suspendVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/suspend', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复虚拟机 */
|
||||
export const resumeVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/resume', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 虚拟机进入救援系统 */
|
||||
export const rescueVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/rescue', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 虚拟机退出救援系统 */
|
||||
export const exitRescueVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/exit_rescue', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除虚拟机 */
|
||||
export const deleteVm = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/vm/delete', { params })
|
||||
}
|
||||
|
||||
/** 迁移虚拟机(更换宿主机) */
|
||||
export const migrateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/migrate', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 发起虚拟机数据迁移 */
|
||||
export const dataMigrateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取虚拟机数据迁移进度 */
|
||||
export const getDataMigrateProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/progress', { params })
|
||||
}
|
||||
|
||||
/** 中断虚拟机数据迁移 */
|
||||
export const abortDataMigrate = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate/abort', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 安全组管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取安全组列表 */
|
||||
export const getSecurityGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/post_group/list', { params })
|
||||
}
|
||||
|
||||
/** 获取安全组详情 */
|
||||
export const getSecurityGroupDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/post_group/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建安全组 */
|
||||
export const createSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改安全组 */
|
||||
export const updateSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 同步安全组 */
|
||||
export const syncSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定安全组到虚拟机 */
|
||||
export const bindSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/bind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 解绑安全组 */
|
||||
export const unbindSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/unbind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除安全组 */
|
||||
export const deleteSecurityGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete', { params })
|
||||
}
|
||||
|
||||
/** 开启安全组白名单 */
|
||||
export const enableSecurityGroupWhitelist = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/enable_whitelist', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 关闭安全组白名单 */
|
||||
export const disableSecurityGroupWhitelist = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/disable_whitelist', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增安全组规则 */
|
||||
export const createSecurityGroupRule = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/create_rule', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改安全组规则 */
|
||||
export const updateSecurityGroupRule = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/update_rule', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除安全组规则 */
|
||||
export const deleteSecurityGroupRule = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete_rule', { params })
|
||||
}
|
||||
|
||||
/** 应用安全组 */
|
||||
export const applySecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/apply', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - VNC 节点管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取 VNC 节点列表 */
|
||||
export const getVncNodeList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vnc/list', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机 VNC 连接信息 */
|
||||
export const getVmVnc = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vnc/vm_vnc', { params })
|
||||
}
|
||||
|
||||
/** 新增 VNC 节点 */
|
||||
export const addVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 测试 VNC 节点连接 */
|
||||
export const testVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/test', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改 VNC 节点 */
|
||||
export const updateVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 VNC 节点 */
|
||||
export const deleteVncNode = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/vnc/delete', { params })
|
||||
}
|
||||
|
||||
/** 设置安全组共享状态 */
|
||||
export const setSecurityGroupShared = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/set_shared', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 快照管理 ==========
|
||||
/** 获取快照列表 */
|
||||
export const getSnapshotList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/list', { params })
|
||||
}
|
||||
|
||||
/** 获取快照任务进度 */
|
||||
export const getSnapshotProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/progress', { params })
|
||||
}
|
||||
|
||||
/** 创建快照 */
|
||||
export const createSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复快照 */
|
||||
export const restoreSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/restore', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除快照 */
|
||||
export const deleteSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/delete', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 备份管理 ==========
|
||||
/** 获取备份列表 */
|
||||
export const getBackupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/list', { params })
|
||||
}
|
||||
|
||||
/** 获取备份任务进度 */
|
||||
export const getBackupProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/progress', { params })
|
||||
}
|
||||
|
||||
/** 创建备份 */
|
||||
export const createBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复备份 */
|
||||
export const restoreBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/restore', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除备份 */
|
||||
export const deleteBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/delete', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取快照数量与上限 */
|
||||
export const getSnapshotCount = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/count', { params })
|
||||
}
|
||||
|
||||
/** 设置快照数量上限 */
|
||||
export const setSnapshotLimit = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/set_limit', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取备份数量与上限 */
|
||||
export const getBackupCount = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/count', { params })
|
||||
}
|
||||
|
||||
/** 设置备份数量上限 */
|
||||
export const setBackupLimit = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/set_limit', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 用户组网管理 (UserNetworking)
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取组网列表 */
|
||||
export const getUserNetworkingList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/networking/list', { params })
|
||||
}
|
||||
|
||||
/** 获取组网详情 */
|
||||
export const getUserNetworkingDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/networking/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建用户组网 */
|
||||
export const createUserNetworking = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 为虚拟机分配组网 IP */
|
||||
export const assignUserNetworking = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/assign', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除组网 */
|
||||
export const deleteUserNetworking = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/networking/delete', { params })
|
||||
}
|
||||
|
||||
/** 删除组网下的指定网络 */
|
||||
export const removeUserNetworkingNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/remove_network', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
@@ -33,3 +33,11 @@ export const updateOrder = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
/**重试订单流程 */
|
||||
export const retryOrderHook = (data) => {
|
||||
return http2.post('/api/v1/admin/order/retry_hook', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+115
-1
@@ -57,6 +57,10 @@ export const deleteProductGroup = (data) => {
|
||||
export const getProductList = (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) => {
|
||||
return http2.post('/api/v1/admin/good/goods/create', data,{
|
||||
@@ -106,7 +110,8 @@ export const getProductParameterDetail = (params) => {
|
||||
}
|
||||
/**更新商品参数 */
|
||||
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:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
@@ -140,3 +145,112 @@ export const updateProductParameterValue = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品套餐管理 */
|
||||
|
||||
/**获取商品套餐列表 */
|
||||
export const getProductPlanList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/plan/list', {params: params})
|
||||
}
|
||||
/**获取商品套餐详情 */
|
||||
export const getProductPlanDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/good/plan/detail', {params: params})
|
||||
}
|
||||
/**创建商品套餐 */
|
||||
export const createProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品套餐 */
|
||||
export const updateProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品套餐 */
|
||||
export const deleteProductPlan = (params) => {
|
||||
return http2.delete('/api/v1/admin/good/plan/delete', {params: params})
|
||||
}
|
||||
/**禁用商品套餐 */
|
||||
export const disableProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/disable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**启用商品套餐 */
|
||||
export const enableProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/enable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**禁用套餐固定价格 */
|
||||
export const disablePlanFixedPrice = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/disable_fixed_price', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**启用套餐固定价格 */
|
||||
export const enablePlanFixedPrice = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/enable_fixed_price', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品分组标签管理 */
|
||||
|
||||
/**获取商品分组标签列表 */
|
||||
export const getProductGroupTagList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/group_tag/list', {params: params})
|
||||
}
|
||||
/**获取商品分组标签详情 */
|
||||
export const getProductGroupTagDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/good/group_tag/detail', {params: params})
|
||||
}
|
||||
/**创建商品分组标签 */
|
||||
export const createProductGroupTag = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group_tag/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品分组标签 */
|
||||
export const updateProductGroupTag = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group_tag/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品分组标签 */
|
||||
export const deleteProductGroupTag = (params) => {
|
||||
return http2.delete('/api/v1/admin/good/group_tag/delete', {params: params})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**已购商品管理 */
|
||||
|
||||
/**获取用户已购商品列表 */
|
||||
export const getUserGoodsList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/user_goods/list', {params: params})
|
||||
}
|
||||
+11
-5
@@ -18,10 +18,6 @@ export const addUserConsumption = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**查询用户余额 */
|
||||
export const getUserBalance = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/select?user_id='+data.user_id)
|
||||
}
|
||||
|
||||
/**获取用户余额记录 */
|
||||
export const getUserBalanceRecord = (data) => {
|
||||
@@ -53,7 +49,7 @@ export const updateUserInfo = (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) => {
|
||||
@@ -163,3 +159,13 @@ export const addUserGroupMember = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**退款对应账单 */
|
||||
export const refundBalance = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/refund', {
|
||||
params:data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const BASE = '/api/v1/admin/good/user_vm'
|
||||
const GOODS_BASE = '/api/v1/admin/good/user_goods'
|
||||
|
||||
const fd = (data) => {
|
||||
const f = new FormData()
|
||||
Object.entries(data).forEach(([k, v]) => {
|
||||
if (v === undefined || v === null) return
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach(item => f.append(k, item))
|
||||
} else if (typeof v === 'boolean') {
|
||||
f.append(k, v ? 'true' : 'false')
|
||||
} else {
|
||||
f.append(k, v)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
// ========== 用户虚拟机 ==========
|
||||
export const getUserVmList = (params) => http2.get(`${BASE}/list`, { params })
|
||||
export const getUserVmDetail = (params) => http2.get(`${BASE}/detail`, { params })
|
||||
export const getUserVmVnc = (params) => http2.get(`${BASE}/vnc`, { params })
|
||||
export const getUserVmHostImages = (params) => http2.get(`${BASE}/host_images`, { params })
|
||||
export const getGoodHostGroupImages = (params) => http2.get(`${BASE}/good_host_group_images`, { params })
|
||||
export const createUserVm = (data) => http2.post(`${BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const bindUserVm = (data) => http2.post(`${BASE}/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const transferUserVm = (data) => http2.post(`${BASE}/transfer`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const migrateUserVm = (data) => http2.post(`${BASE}/migrate`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmTraffic = (data) => http2.post(`${BASE}/update_traffic`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVm = (data) => http2.post(`${BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const refactorUserVm = (data) => http2.post(`${BASE}/refactor`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const startUserVm = (data) => http2.post(`${BASE}/start`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const stopUserVm = (data) => http2.post(`${BASE}/stop`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rebootUserVm = (data) => http2.post(`${BASE}/reboot`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const suspendUserVm = (data) => http2.post(`${BASE}/suspend`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const resumeUserVm = (data) => http2.post(`${BASE}/resume`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rescueUserVm = (data) => http2.post(`${BASE}/rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const exitRescueUserVm = (data) => http2.post(`${BASE}/exit_rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rebuildUserVm = (data) => http2.post(`${BASE}/rebuild`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVm = (params) => http2.delete(`${BASE}/delete`, { params })
|
||||
|
||||
// ========== 数据卷 ==========
|
||||
export const getUserVmVolumeList = (params) => http2.get(`${BASE}/volume/list`, { params })
|
||||
export const getUserVmVolumeDetail = (params) => http2.get(`${BASE}/volume/detail`, { params })
|
||||
export const createUserVmVolume = (data) => http2.post(`${BASE}/volume/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const resizeUserVmVolume = (data) => http2.post(`${BASE}/volume/resize`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const mountUserVmVolume = (data) => http2.post(`${BASE}/volume/mount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const unmountUserVmVolume = (data) => http2.post(`${BASE}/volume/unmount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmVolume = (params) => http2.delete(`${BASE}/volume/delete`, { params })
|
||||
|
||||
// ========== 快照 ==========
|
||||
export const getUserVmSnapshotList = (params) => http2.get(`${BASE}/snapshot/list`, { params })
|
||||
export const getUserVmSnapshotProgress = (params) => http2.get(`${BASE}/snapshot/progress`, { params })
|
||||
export const getUserVmSnapshotCount = (params) => http2.get(`${BASE}/snapshot/count`, { params })
|
||||
export const createUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const restoreUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setUserVmSnapshotLimit = (data) => http2.post(`${BASE}/snapshot/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 备份 ==========
|
||||
export const getUserVmBackupList = (params) => http2.get(`${BASE}/backup/list`, { params })
|
||||
export const getUserVmBackupProgress = (params) => http2.get(`${BASE}/backup/progress`, { params })
|
||||
export const getUserVmBackupCount = (params) => http2.get(`${BASE}/backup/count`, { params })
|
||||
export const createUserVmBackup = (data) => http2.post(`${BASE}/backup/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const restoreUserVmBackup = (data) => http2.post(`${BASE}/backup/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmBackup = (data) => http2.post(`${BASE}/backup/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setUserVmBackupLimit = (data) => http2.post(`${BASE}/backup/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 安全组 ==========
|
||||
export const getUserVmPostGroupList = (params) => http2.get(`${BASE}/post_group/list`, { params })
|
||||
export const getUserVmPostGroupDetail = (params) => http2.get(`${BASE}/post_group/detail`, { params })
|
||||
export const getUserVmPostGroupUserList = (params) => http2.get(`${BASE}/post_group/user_list`, { params })
|
||||
export const createUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const bindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const unbindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/unbind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const applyUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/apply`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setSharedUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/set_shared`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmPostGroup = (params) => http2.delete(`${BASE}/post_group/delete`, { params })
|
||||
export const enableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/enable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const disableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/disable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const createUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/create_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/update_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmPostGroupRule = (params) => http2.delete(`${BASE}/post_group/delete_rule`, { params })
|
||||
|
||||
// ========== 网络 ==========
|
||||
export const getUserVmNetworkList = (params) => http2.get(`${BASE}/network/list`, { params })
|
||||
export const getUserVmNetworkDetail = (params) => http2.get(`${BASE}/network/detail`, { params })
|
||||
|
||||
// ========== 组网 ==========
|
||||
export const getUserVmNetworkingList = (params) => http2.get(`${BASE}/networking/list`, { params })
|
||||
export const getUserVmNetworkingDetail = (params) => http2.get(`${BASE}/networking/detail`, { params })
|
||||
export const createUserVmNetworking = (data) => http2.post(`${BASE}/networking/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const assignUserVmNetworking = (data) => http2.post(`${BASE}/networking/assign`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const removeUserVmNetworkingNetwork = (data) => http2.post(`${BASE}/networking/remove_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmNetworking = (params) => http2.delete(`${BASE}/networking/delete`, { params })
|
||||
|
||||
// ========== 用户商品 ==========
|
||||
export const getUserGoodsList = (params) => http2.get(`${GOODS_BASE}/list`, { params })
|
||||
export const getUserGoodsDetail = (params) => http2.get(`${GOODS_BASE}/detail`, { params })
|
||||
export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params })
|
||||
|
||||
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
|
||||
|
||||
// ========== 流量策略 ==========
|
||||
// 测试未通过(接口新增,待联调)
|
||||
export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params })
|
||||
export const updateUserVmTrafficPolicy = (data) => http2.post(`${BASE}/traffic_policy/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const addUserVmFixedTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_fixed`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const addUserVmTemporaryTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_temporary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 到期提醒 ==========
|
||||
export const getExpireRemindList = (params) => http2.get(`${GOODS_BASE}/expire_remind/list`, { params })
|
||||
export const sendExpireRemind = (data) => http2.post(`${GOODS_BASE}/expire_remind/send`, data, { headers: { 'Content-Type': 'application/json' } })
|
||||
@@ -0,0 +1,24 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const fd = (data) => {
|
||||
const f = new FormData()
|
||||
Object.entries(data).forEach(([k, v]) => {
|
||||
if (v === undefined || v === null || v === '') return
|
||||
f.append(k, v)
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
const BASE_GROUP = '/api/v1/admin/server/vnc_command/group'
|
||||
const BASE_ITEM = '/api/v1/admin/server/vnc_command/item'
|
||||
|
||||
// 分组
|
||||
export const getVncCommandGroupList = () => http2.get(`${BASE_GROUP}/list`)
|
||||
export const createVncCommandGroup = (data) => http2.post(`${BASE_GROUP}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateVncCommandGroup = (data) => http2.post(`${BASE_GROUP}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteVncCommandGroup = (params) => http2.delete(`${BASE_GROUP}/delete`, { params })
|
||||
|
||||
// 指令项
|
||||
export const createVncCommandItem = (data) => http2.post(`${BASE_ITEM}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateVncCommandItem = (data) => http2.post(`${BASE_ITEM}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteVncCommandItem = (params) => http2.delete(`${BASE_ITEM}/delete`, { params })
|
||||
@@ -0,0 +1,64 @@
|
||||
import { http2 } from "@/utils/request.js"
|
||||
|
||||
// ========== 后台菜单管理 ==========
|
||||
|
||||
/** 获取后台菜单列表 */
|
||||
export const getWebRoutsList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/list', { params })
|
||||
}
|
||||
|
||||
/** 新增后台菜单 */
|
||||
export const addWebRouts = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改后台菜单 */
|
||||
export const updateWebRouts = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除后台菜单 */
|
||||
export const deleteWebRouts = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/web_routs/delete', {
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 后台菜单权限管理 ==========
|
||||
|
||||
/** 获取后台菜单权限列表 */
|
||||
export const getWebRoutsPermissionList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/permission/list', { params })
|
||||
}
|
||||
|
||||
/** 新增后台菜单权限 */
|
||||
export const addWebRoutsPermission = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/permission/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改后台菜单权限 */
|
||||
export const updateWebRoutsPermission = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/permission/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除后台菜单权限 */
|
||||
export const deleteWebRoutsPermission = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/web_routs/permission/delete', {
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取当前用户的后台菜单权限树 */
|
||||
export const getMyWebRoutsPermission = () => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/my')
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -8,3 +8,13 @@ export const userLogin = (username,password) => {
|
||||
export const getUserInfo = () => {
|
||||
return request.get("/api/v1/users/info/info")
|
||||
}
|
||||
|
||||
// 获取交换token(用于无感刷新)
|
||||
export const getRefreshToken = (domain) => {
|
||||
return request.get("/api/v1/users/info/refresh_token", { domain })
|
||||
}
|
||||
|
||||
// 使用交换token获取新的access token
|
||||
export const refreshAccessToken = (refresh_token) => {
|
||||
return request.post("/api/v1/user/refresh_token", { refresh_token })
|
||||
}
|
||||
+9
-2
@@ -5,8 +5,15 @@ import request from "@/utils/request.js";
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
export function getTickerList(count, page, status) {
|
||||
return request.get('/api/v1/admin/work_order/list', { count, page, status })
|
||||
export function getTickerList(count, page, status, orderBy, order, userId, keyword) {
|
||||
const params = { count, page }
|
||||
if (status !== undefined && status !== '') params.status = status
|
||||
if (orderBy) params.orderBy = orderBy
|
||||
if (order) params.order = order
|
||||
if (userId) params.user_id = userId
|
||||
if (keyword) params.keyword = keyword
|
||||
console.log('工单列表请求参数:', params) // 调试日志
|
||||
return request.get('/api/v1/admin/work_order/list', params)
|
||||
}
|
||||
|
||||
// 待处理
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="选择用户"
|
||||
width="700px"
|
||||
class="user-selector-dialog"
|
||||
append-to-body
|
||||
@update:model-value="handleVisibleChange"
|
||||
>
|
||||
<div class="user-selector-content">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="selector-search">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索用户名、邮箱或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="handleReset" class="reset-btn">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
max-height="350"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column prop="user_name" label="用户名" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="user-name-cell">
|
||||
<el-avatar v-if="row.cover" :src="row.cover" :size="28" />
|
||||
<el-avatar v-else :size="28">
|
||||
{{ row.user_name?.charAt(0)?.toUpperCase() || 'U' }}
|
||||
</el-avatar>
|
||||
<span class="user-name">{{ row.user_name || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="text-ellipsis">{{ row.email || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.disable" type="danger" size="small">禁用</el-tag>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
background
|
||||
small
|
||||
class="selector-pagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<span v-if="selectedUser" class="selected-info">
|
||||
已选择: <el-tag type="primary" size="small">{{ selectedUser.user_name }} (ID: {{ selectedUser.user_id }})</el-tag>
|
||||
</span>
|
||||
<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, Refresh } 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
|
||||
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>
|
||||
.user-selector-content {
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selector-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selector-pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
margin-right: auto;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row):hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.current-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.current-row td) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-avatar) {
|
||||
background-color: var(--el-color-primary-light-5);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择头像"
|
||||
:title="title"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="avatar-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 用户文件列表 -->
|
||||
<el-tab-pane label="用户文件" name="userFiles">
|
||||
<!-- 文件列表 -->
|
||||
<el-tab-pane label="文件" name="userFiles">
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
<h4>用户文件列表</h4>
|
||||
<h4>文件列表</h4>
|
||||
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||
上传新头像
|
||||
上传新文件
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="file-grid" v-loading="loading">
|
||||
@@ -58,8 +58,8 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 上传头像 -->
|
||||
<el-tab-pane label="上传头像" name="upload">
|
||||
<!-- 上传文件 -->
|
||||
<el-tab-pane label="上传文件" name="upload">
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
@@ -118,6 +118,10 @@ import { closeAllMessage } from '../../utils/message'
|
||||
currentCoverId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '选择文件'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -270,6 +274,7 @@ import { closeAllMessage } from '../../utils/message'
|
||||
formData.append('files', file)
|
||||
formData.append('file_names', file.name)
|
||||
formData.append('update_type', 'cover')
|
||||
formData.append('open_down', 'true')
|
||||
|
||||
try {
|
||||
const res = await uploadFile(formData)
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择优惠码"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="discount-code-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择优惠码 -->
|
||||
<el-tab-pane label="选择优惠码" name="selectCode">
|
||||
<div class="code-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索优惠码名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 优惠码列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="codeList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="优惠码ID" width="100" align="center" />
|
||||
<el-table-column prop="name" label="优惠码名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="code" label="优惠码" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" effect="plain">{{ row.code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
|
||||
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠值" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.percentage > 0" class="discount-value">{{ row.percentage }}%</span>
|
||||
<span v-else class="discount-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>无限制</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'expired': isExpired(row.endTime) }">
|
||||
{{ formatDate(row.endTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="codeList.length === 0 && !loading" description="暂无优惠码数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedCode"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getDiscountCodeList } from '@/api/admin/discount'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的优惠码ID(用于回显)
|
||||
currentCodeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 类型过滤:discount_code - 优惠码
|
||||
codeType: {
|
||||
type: String,
|
||||
default: 'code'
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectCode')
|
||||
const loading = ref(false)
|
||||
const codeList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedCode = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectCode'
|
||||
selectedCode.value = null
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取优惠码列表
|
||||
const fetchCodeList = async () => {
|
||||
loading.value = true
|
||||
codeList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
discount_type: props.codeType
|
||||
}
|
||||
if (searchParams.key) {
|
||||
params.key = searchParams.key
|
||||
}
|
||||
|
||||
const res = await getDiscountCodeList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
codeList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的优惠码ID,自动选中
|
||||
if (props.currentCodeId) {
|
||||
const currentCode = codeList.value.find(
|
||||
code => code.id === props.currentCodeId
|
||||
)
|
||||
if (currentCode) {
|
||||
selectedCode.value = currentCode
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取优惠码列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠码列表失败:', error)
|
||||
ElMessage.error('获取优惠码列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectCode') {
|
||||
fetchCodeList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 选择优惠码
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedCode.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedCode.value && row.id === selectedCode.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedCode.value = null
|
||||
codeList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
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 isExpired = (endTime) => {
|
||||
if (!endTime) return false
|
||||
return new Date(endTime) < new Date()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedCode.value) {
|
||||
emit('confirm', selectedCode.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个优惠码')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discount-code-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.code-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.discount-value {
|
||||
color: #e6a23c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expired {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择宿主机组" width="650px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索宿主机组名称" clearable style="width:200px" @keyup.enter="handleSearch" @clear="handleSearch" />
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="serviceId" label="服务ID" width="80" />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getHostGroupList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!keyword.value) return list.value
|
||||
const kw = keyword.value.toLowerCase()
|
||||
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw))
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getHostGroupList({ service_id: props.serviceId, page: page.value, count: pageSize })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const items = Array.isArray(body.data) ? body.data : (body.data.data || body.data.list || [])
|
||||
list.value = items.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name ?? i.Name,
|
||||
note: i.note ?? i.Note,
|
||||
serviceId: i.serviceId ?? i.service_id ?? 0,
|
||||
serviceHostGroupId: i.serviceHostGroupId ?? 0
|
||||
}))
|
||||
total.value = body.data.total ?? body.data.all_count ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择宿主机" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索宿主机名称/IP" clearable style="width:200px" @keyup.enter="loadList" @clear="loadList" />
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="ip" label="IP" min-width="130" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '在线' : '离线' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getRemoteHostList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostGroupId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!keyword.value) return list.value
|
||||
const kw = keyword.value.toLowerCase()
|
||||
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw) || (i.ip || '').includes(kw))
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (props.hostGroupId) params.host_group_id = props.hostGroupId
|
||||
const res = await getRemoteHostList(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
const hosts = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
list.value = hosts.map(i => ({
|
||||
id: i.id, name: i.name, ip: i.ip, is_active: i.is_active ?? true,
|
||||
host_group_id: i.host_group_id
|
||||
}))
|
||||
total.value = inner.total ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="icon-selector">
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
@click="popoverVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon v-if="modelValue" :size="18">
|
||||
<component :is="modelValue" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<el-icon v-if="modelValue" class="clear-btn" @click.stop="handleClear"><CircleClose /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog v-model="popoverVisible" title="选择图标" width="680px" append-to-body>
|
||||
<el-input v-model="searchKey" placeholder="搜索图标名称" clearable class="icon-search">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="icon-grid">
|
||||
<div
|
||||
v-for="name in filteredIcons"
|
||||
:key="name"
|
||||
class="icon-item"
|
||||
:class="{ active: modelValue === name }"
|
||||
@click="handleSelect(name)"
|
||||
>
|
||||
<el-icon :size="22"><component :is="name" /></el-icon>
|
||||
<span class="icon-name">{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredIcons.length === 0" class="icon-empty">
|
||||
未找到匹配的图标
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { Search, CircleClose } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const popoverVisible = ref(false)
|
||||
const searchKey = ref('')
|
||||
|
||||
const allIcons = Object.keys(ElementPlusIconsVue).sort()
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
if (!searchKey.value) return allIcons
|
||||
const key = searchKey.value.toLowerCase()
|
||||
return allIcons.filter(name => name.toLowerCase().includes(key))
|
||||
})
|
||||
|
||||
const handleSelect = (name) => {
|
||||
emit('update:modelValue', name)
|
||||
popoverVisible.value = false
|
||||
searchKey.value = ''
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-selector { width: 100%; }
|
||||
.icon-search { margin-bottom: 12px; }
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 10px 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.icon-item:hover {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.icon-item.active {
|
||||
border-color: #409eff;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
.icon-name {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icon-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.clear-btn {
|
||||
cursor: pointer;
|
||||
color: #c0c4cc;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.clear-btn:hover { color: #f56c6c; }
|
||||
</style>
|
||||
@@ -0,0 +1,684 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择图片"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="image-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 文件库 -->
|
||||
<el-tab-pane label="文件库" name="fileLibrary">
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
<h4>图片文件库</h4>
|
||||
<div class="header-actions">
|
||||
<span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
|
||||
已选 {{ selectedIds.size }} 个文件
|
||||
</span>
|
||||
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||
上传新图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索过滤 -->
|
||||
<div class="filter-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="width: 300px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="file-grid" v-loading="loading">
|
||||
<div
|
||||
v-for="file in filteredFileList"
|
||||
:key="file.id"
|
||||
class="file-item"
|
||||
:class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
<div class="file-check-badge" v-if="props.multiple && selectedIds.has(file.id)">
|
||||
<el-icon><Select /></el-icon>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<img
|
||||
:src="processImageUrl(file.url)"
|
||||
:alt="file.realName"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<p class="file-name" :title="file.realName">{{ file.realName }}</p>
|
||||
<p class="file-size">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="filteredFileList.length === 0 && !loading" description="暂无图片文件" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<el-tab-pane label="上传图片" name="upload">
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
accept="image/*"
|
||||
multiple
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持jpg、png、gif、webp等图片格式,单个文件不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<!-- 待上传文件列表 -->
|
||||
<div v-if="pendingFiles.length > 0" class="pending-files">
|
||||
<div class="pending-header">
|
||||
<h4>待上传文件 ({{ pendingFiles.length }})</h4>
|
||||
<el-button type="danger" link @click="pendingFiles = []">清空</el-button>
|
||||
</div>
|
||||
<div class="pending-list">
|
||||
<div v-for="(file, index) in pendingFiles" :key="index" class="pending-item">
|
||||
<img :src="file.previewUrl" class="pending-preview" />
|
||||
<span class="pending-name" :title="file.name">{{ file.name }}</span>
|
||||
<span class="pending-size">{{ formatFileSize(file.size) }}</span>
|
||||
<el-button type="danger" link size="small" @click="removePendingFile(index)">移除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleBatchUpload"
|
||||
:loading="uploading"
|
||||
style="margin-top: 16px; width: 100%;"
|
||||
>
|
||||
开始上传 ({{ pendingFiles.length }} 个文件)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="props.multiple ? selectedIds.size === 0 : !selectedId"
|
||||
>
|
||||
确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, UploadFilled, Search, Select, Delete } from '@element-plus/icons-vue'
|
||||
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentFileId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('fileLibrary')
|
||||
const fileList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedId = ref('')
|
||||
const selectedIds = ref(new Set()) // 多选模式下选中的文件ID集合
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const total = ref(0)
|
||||
const searchKeyword = ref('')
|
||||
const pendingFiles = ref([]) // 待上传文件列表
|
||||
const uploading = ref(false) // 批量上传中
|
||||
let fetchVersion = 0 // 防止 fetchFileList 竞态条件
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
selectedId.value = props.currentFileId
|
||||
selectedIds.value = new Set()
|
||||
currentPage.value = 1
|
||||
searchKeyword.value = ''
|
||||
fetchFileList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 过滤后的文件列表
|
||||
const filteredFileList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return fileList.value
|
||||
}
|
||||
return fileList.value.filter(file =>
|
||||
file.realName?.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 处理图片URL,确保正确显示
|
||||
const processImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
// 先处理转义字符:将 \u0026 替换为 &
|
||||
let processedUrl = url.replace(/\\u0026/g, '&')
|
||||
// 再进行URL解码
|
||||
return decodeURIComponent(processedUrl)
|
||||
}
|
||||
|
||||
// 获取文件列表(带版本号防止竞态条件)
|
||||
const fetchFileList = async () => {
|
||||
const currentFetchVersion = ++fetchVersion
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await getFileList({
|
||||
page: currentPage.value,
|
||||
count: pageSize.value
|
||||
})
|
||||
|
||||
// 如果有更新的请求发起,丢弃当前结果
|
||||
if (currentFetchVersion !== fetchVersion) return
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const list = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
|
||||
// 并行获取所有文件详情(替代逐个串行,大幅提升速度)
|
||||
const detailPromises = list.map(item =>
|
||||
getFileDetail({ file_id: item.id })
|
||||
.then(res2 => {
|
||||
if (res2.data.code === 200) {
|
||||
return {
|
||||
id: res2.data.data.data.id,
|
||||
url: res2.data.data.url,
|
||||
size: res2.data.data.data.size,
|
||||
realName: res2.data.data.data.realName
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取文件详情失败:', error)
|
||||
return null
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.all(detailPromises)
|
||||
|
||||
// 再次检查版本号,防止旧结果覆盖新结果
|
||||
if (currentFetchVersion !== fetchVersion) return
|
||||
|
||||
fileList.value = results.filter(item => item !== null)
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentFetchVersion === fetchVersion) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
ElMessage.error('获取文件列表失败')
|
||||
}
|
||||
} finally {
|
||||
if (currentFetchVersion === fetchVersion) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.name === 'fileLibrary') {
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索时重置到第一页
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 切换到上传标签页
|
||||
const switchToUpload = () => {
|
||||
activeTab.value = 'upload'
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (!size) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let unitIndex = 0
|
||||
let fileSize = size
|
||||
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
const selectFile = (file) => {
|
||||
if (props.multiple) {
|
||||
// 多选模式:切换选中状态
|
||||
const newSet = new Set(selectedIds.value)
|
||||
if (newSet.has(file.id)) {
|
||||
newSet.delete(file.id)
|
||||
} else {
|
||||
newSet.add(file.id)
|
||||
}
|
||||
selectedIds.value = newSet
|
||||
} else {
|
||||
selectedId.value = file.id
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择变化(收集待上传文件)
|
||||
const handleFileChange = (file) => {
|
||||
const rawFile = file.raw
|
||||
if (!rawFile) return
|
||||
|
||||
// 验证文件类型
|
||||
const isImage = rawFile.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error(`${rawFile.name} 超过 5MB,已跳过`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否重复添加
|
||||
const exists = pendingFiles.value.some(f => f.name === rawFile.name && f.size === rawFile.size)
|
||||
if (exists) return
|
||||
|
||||
// 添加到待上传列表,生成本地预览URL
|
||||
pendingFiles.value.push({
|
||||
raw: rawFile,
|
||||
name: rawFile.name,
|
||||
size: rawFile.size,
|
||||
previewUrl: URL.createObjectURL(rawFile)
|
||||
})
|
||||
}
|
||||
|
||||
// 移除待上传文件
|
||||
const removePendingFile = (index) => {
|
||||
const file = pendingFiles.value[index]
|
||||
if (file?.previewUrl) {
|
||||
URL.revokeObjectURL(file.previewUrl)
|
||||
}
|
||||
pendingFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 批量上传(所有文件合并为一次请求,多个 file_names 和 files 条目)
|
||||
const handleBatchUpload = async () => {
|
||||
if (pendingFiles.value.length === 0) {
|
||||
ElMessage.warning('请先选择要上传的文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
|
||||
const formData = new FormData()
|
||||
pendingFiles.value.forEach(file => {
|
||||
formData.append('file_names', file.name)
|
||||
formData.append('files', file.raw)
|
||||
})
|
||||
formData.append('update_type', 'cover')
|
||||
formData.append('open_down', 'true')
|
||||
|
||||
try {
|
||||
const res = await uploadFile(formData)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const count = pendingFiles.value.length
|
||||
// 释放所有预览URL
|
||||
pendingFiles.value.forEach(f => {
|
||||
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
|
||||
})
|
||||
pendingFiles.value = []
|
||||
ElMessage.success(`成功上传 ${count} 个文件`)
|
||||
|
||||
// 刷新文件列表并切换到文件库
|
||||
currentPage.value = 1
|
||||
await fetchFileList()
|
||||
activeTab.value = 'fileLibrary'
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error)
|
||||
ElMessage.error('上传失败,请重试')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载错误处理
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedId.value = ''
|
||||
selectedIds.value = new Set()
|
||||
fileList.value = []
|
||||
currentPage.value = 1
|
||||
total.value = 0
|
||||
searchKeyword.value = ''
|
||||
// 清理待上传文件的预览URL
|
||||
pendingFiles.value.forEach(f => {
|
||||
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
|
||||
})
|
||||
pendingFiles.value = []
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (props.multiple) {
|
||||
// 多选模式:返回选中的文件数组
|
||||
if (selectedIds.value.size === 0) return
|
||||
const selectedFiles = fileList.value
|
||||
.filter(file => selectedIds.value.has(file.id))
|
||||
.map(file => ({
|
||||
id: file.id,
|
||||
url: file.url || '',
|
||||
realName: file.realName || ''
|
||||
}))
|
||||
emit('confirm', selectedFiles)
|
||||
handleClose()
|
||||
} else {
|
||||
// 单选模式:返回单个文件对象
|
||||
if (selectedId.value) {
|
||||
const selectedFile = fileList.value.find(file => file.id === selectedId.value)
|
||||
emit('confirm', {
|
||||
id: selectedId.value,
|
||||
url: selectedFile?.url || '',
|
||||
realName: selectedFile?.realName || ''
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-selector {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.file-list-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-list-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #409EFF;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
border-color: #409EFF;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-check-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
color: #409EFF;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
margin: 0 0 4px 0;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 待上传文件列表 */
|
||||
.pending-files {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pending-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pending-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pending-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pending-item:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.pending-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.pending-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pending-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择镜像" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索镜像名称" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="filterOsType" placeholder="系统类型" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="Linux" value="linux" />
|
||||
<el-option label="Windows" value="windows" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="系统" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.os_type === 'linux' ? 'success' : 'primary'" size="small">{{ row.os_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'system' ? '' : 'warning'" size="small">{{ row.type === 'system' ? '系统' : '数据' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getImageList } from '@/api/admin/kvmService'
|
||||
import { getUserVmHostImages, getGoodHostGroupImages } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
goodId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 },
|
||||
useUserVmApi: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const filterOsType = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
let res
|
||||
if (props.goodId > 0) {
|
||||
const params = { good_id: props.goodId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getGoodHostGroupImages(params)
|
||||
} else if (props.useUserVmApi) {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getUserVmHostImages(params)
|
||||
} else {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getImageList(params)
|
||||
}
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
let items = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
if (props.useUserVmApi || props.goodId > 0) {
|
||||
items = items.map(item => item.image || item).filter(Boolean)
|
||||
}
|
||||
list.value = items
|
||||
total.value = inner.total ?? inner.all_count ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择主控服务" width="640px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索服务名称/地址" clearable style="width:220px"
|
||||
@keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="320" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="服务名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="地址" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="font-family:monospace;color:#409eff">{{ row.host }}:{{ row.port }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无主控服务" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getKvmServiceList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Boolean, default: false } })
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
const res = await getKvmServiceList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
list.value = raw.map(s => ({ id: s.id ?? s.Id, name: s.name ?? s.Name, host: s.host ?? s.Host, port: s.port ?? s.Port, note: s.note ?? s.Note }))
|
||||
total.value = inner.all_count ?? inner.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
const handleRefresh = () => { keyword.value = ''; page.value = 1; loadList() }
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="menu-path-selector">
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
placeholder="点击从菜单中选择路径,或手动输入"
|
||||
clearable
|
||||
@input="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="dialogVisible = true">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="选择菜单路径" width="550px" append-to-body>
|
||||
<el-input v-model="searchKey" placeholder="搜索菜单名称或路径" clearable class="path-search">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="menu-tree">
|
||||
<el-tree
|
||||
:data="filteredMenuTree"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
node-key="path"
|
||||
:default-expand-all="!!searchKey"
|
||||
:expand-on-click-node="false"
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="tree-node" :class="{ 'is-selected': modelValue === data.path, 'no-path': !data.path }">
|
||||
<el-icon v-if="data.icon" :size="16" style="margin-right: 6px; flex-shrink: 0;">
|
||||
<component :is="data.icon" />
|
||||
</el-icon>
|
||||
<span class="node-title">{{ data.title }}</span>
|
||||
<el-tag v-if="data.path" size="small" type="info" class="node-path">{{ data.path }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
<div v-if="filteredMenuTree.length === 0" class="tree-empty">
|
||||
未找到匹配的菜单
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { Search, FolderOpened } from '@element-plus/icons-vue'
|
||||
import { menus } from '@/config/menus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const searchKey = ref('')
|
||||
|
||||
const buildTreeData = (menuList) => {
|
||||
return menuList.map(item => {
|
||||
const node = {
|
||||
path: item.path || '',
|
||||
title: item.title,
|
||||
icon: item.icon || '',
|
||||
label: item.title
|
||||
}
|
||||
if (item.children?.length) {
|
||||
node.children = buildTreeData(item.children)
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const menuTree = computed(() => buildTreeData(menus))
|
||||
|
||||
const filterTree = (nodes, keyword) => {
|
||||
const key = keyword.toLowerCase()
|
||||
const result = []
|
||||
for (const node of nodes) {
|
||||
const titleMatch = node.title?.toLowerCase().includes(key)
|
||||
const pathMatch = node.path?.toLowerCase().includes(key)
|
||||
let filteredChildren = []
|
||||
if (node.children?.length) {
|
||||
filteredChildren = filterTree(node.children, keyword)
|
||||
}
|
||||
if (titleMatch || pathMatch || filteredChildren.length > 0) {
|
||||
result.push({
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : node.children
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const filteredMenuTree = computed(() => {
|
||||
if (!searchKey.value) return menuTree.value
|
||||
return filterTree(menuTree.value, searchKey.value)
|
||||
})
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
if (!data.path) return
|
||||
emit('update:modelValue', data.path)
|
||||
dialogVisible.value = false
|
||||
searchKey.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-path-selector { width: 100%; }
|
||||
.path-search { margin-bottom: 12px; }
|
||||
.menu-tree {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tree-node.is-selected {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.tree-node.no-path {
|
||||
color: #909399;
|
||||
cursor: default;
|
||||
}
|
||||
.node-title { margin-right: 8px; font-size: 13px; }
|
||||
.node-path { flex-shrink: 0; }
|
||||
.tree-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.el-tree-node__content) { height: 36px; }
|
||||
:deep(.el-tree-node__content:hover) { background-color: #f5f7fa; }
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-if="!filterType" v-model="typeFilter" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="网桥(Bridge)" value="bridge" />
|
||||
<el-option label="内网(NAT)" value="nat" />
|
||||
</el-select>
|
||||
<el-tag v-else type="success" size="small">仅{{ filterType === 'bridge' ? '网桥' : filterType === 'nat' ? '内网' : filterType }}</el-tag>
|
||||
<el-select v-if="!filterUsed" v-model="usedFilter" placeholder="占用状态" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="未占用" value="false" />
|
||||
<el-option label="已占用" value="true" />
|
||||
</el-select>
|
||||
<el-tag v-else :type="filterUsed === 'false' ? 'success' : 'info'" size="small">{{ filterUsed === 'false' ? '仅未占用' : '仅已占用' }}</el-tag>
|
||||
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
|
||||
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="gateway" label="网关" width="130" />
|
||||
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row._used === true" type="danger" size="small">已占用</el-tag>
|
||||
<el-tag v-else-if="row._used === false" type="success" size="small">空闲</el-tag>
|
||||
<el-tag v-else type="info" size="small">-</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建网络</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getNetworkList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 },
|
||||
filterType: { type: String, default: '' },
|
||||
filterUsed: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const typeFilter = ref('')
|
||||
const usedFilter = ref('')
|
||||
const ipVersionFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
const type = ref('bridge')
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
typeFilter.value = props.filterType || ''
|
||||
usedFilter.value = props.filterUsed || ''
|
||||
ipVersionFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId || !props.hostId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value }
|
||||
const effectiveType = props.filterType || typeFilter.value || type.value
|
||||
if (effectiveType) params.type = effectiveType
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const effectiveUsed = props.filterUsed || usedFilter.value
|
||||
if (effectiveUsed) params.used = effectiveUsed
|
||||
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
|
||||
const res = await getNetworkList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const items = inner.data || inner.networks || (Array.isArray(inner) ? inner : [])
|
||||
list.value = items.map(item => ({
|
||||
...item,
|
||||
_used: item.used !== undefined ? item.used
|
||||
: effectiveUsed === 'true' ? true
|
||||
: effectiveUsed === 'false' ? false
|
||||
: null
|
||||
}))
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
emit('create')
|
||||
}
|
||||
|
||||
defineExpose({ loadList })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择订单" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索订单名称/ID" clearable style="width:220px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="360" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="订单名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="价格" width="100">
|
||||
<template #default="{ row }">¥{{ ((row.price || 0) / 100).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.state === 1 ? 'success' : row.state === 0 ? 'warning' : 'info'" size="small">
|
||||
{{ row.state === 1 ? '已支付' : row.state === 0 ? '待支付' : '已失效' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期时间" width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.expireTime || row.expire_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无订单" />
|
||||
<div class="selector-selected" v-if="selected">
|
||||
<el-tag type="primary" size="large" closable @close="selected = null">已选:{{ selected.name }} (ID: {{ selected.id }})</el-tag>
|
||||
</div>
|
||||
<div class="selector-footer-bar">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }" @current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getOrderList } from '@/api/admin/order'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Boolean, default: false } })
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm') : '-'
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
const res = await getOrderList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.list || d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
const handleRefresh = () => { keyword.value = ''; page.value = 1; loadList() }
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-selected { margin-top: 12px; }
|
||||
.selector-footer-bar { display: flex; justify-content: flex-end; align-items: center; margin-top: 10px; }
|
||||
</style>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择路径权限"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="permission-selector">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索路径或名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求方法">
|
||||
<el-select
|
||||
v-model="searchParams.method"
|
||||
placeholder="全部方法"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
<el-option label="DELETE" value="DELETE" />
|
||||
<el-option label="PATCH" value="PATCH" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 权限列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="400"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="method" label="方法" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.method" :type="getMethodTag(row.method)" size="small">
|
||||
{{ row.method }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 已选信息 -->
|
||||
<div class="selected-info" v-if="selectedPermission">
|
||||
<el-alert type="success" :closable="false">
|
||||
<template #title>
|
||||
<div class="selected-content">
|
||||
<span>已选择: </span>
|
||||
<el-tag v-if="selectedPermission.method" :type="getMethodTag(selectedPermission.method)" size="small" style="margin-right: 8px;">
|
||||
{{ selectedPermission.method }}
|
||||
</el-tag>
|
||||
<span class="selected-path">{{ selectedPermission.path }}</span>
|
||||
<span class="selected-name" v-if="selectedPermission.name"> - {{ selectedPermission.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedPermission">
|
||||
确认选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getPermissionList } from '@/api/admin/Permission'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentPermissionId: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
method: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const permissionList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedPermission = ref(null)
|
||||
|
||||
// 过滤后的列表
|
||||
const filteredList = computed(() => {
|
||||
let list = permissionList.value
|
||||
|
||||
// 关键词过滤
|
||||
if (searchParams.key) {
|
||||
const keyword = searchParams.key.toLowerCase()
|
||||
list = list.filter(item =>
|
||||
(item.path && item.path.toLowerCase().includes(keyword)) ||
|
||||
(item.name && item.name.toLowerCase().includes(keyword)) ||
|
||||
(item.note && item.note.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 方法过滤
|
||||
if (searchParams.method) {
|
||||
list = list.filter(item => item.method === searchParams.method)
|
||||
}
|
||||
|
||||
return list
|
||||
})
|
||||
|
||||
// 获取方法标签颜色
|
||||
const getMethodTag = (method) => {
|
||||
const tagMap = {
|
||||
'GET': 'success',
|
||||
'POST': 'primary',
|
||||
'PUT': 'warning',
|
||||
'DELETE': 'danger',
|
||||
'PATCH': 'info'
|
||||
}
|
||||
return tagMap[method?.toUpperCase()] || 'info'
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedPermission.value && row.id === selectedPermission.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
if (props.currentPermissionId && row.id === props.currentPermissionId) {
|
||||
return 'current-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取权限列表
|
||||
const fetchPermissionList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPermissionList({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
permissionList.value = res.data.data?.list || []
|
||||
total.value = permissionList.value.length
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取权限列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error)
|
||||
ElMessage.error('获取权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.method = ''
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
}
|
||||
|
||||
// 选择行
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedPermission.value = row
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedPermission.value) {
|
||||
emit('confirm', selectedPermission.value)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedPermission.value = null
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 监听弹窗打开
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
fetchPermissionList()
|
||||
// 如果有当前选中的ID,尝试预选
|
||||
if (props.currentPermissionId) {
|
||||
const found = permissionList.value.find(p => p.id === props.currentPermissionId)
|
||||
if (found) {
|
||||
selectedPermission.value = found
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-selector {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafbfc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.selected-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selected-path {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
:deep(.el-table .selected-row) {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .current-row) {
|
||||
background-color: #f0f9eb !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .selected-row td),
|
||||
:deep(.el-table .current-row td) {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.el-dialog) {
|
||||
width: 95% !important;
|
||||
margin: 2vh auto !important;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-form .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-form .el-input,
|
||||
.search-form .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择套餐" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建套餐</el-button>
|
||||
<span style="color:#909399;font-size:13px" v-if="goodId">商品 ID: {{ goodId }}</span>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="300" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="套餐名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="说明" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.disable ? 'danger' : 'success'" size="small">{{ row.disable ? '禁用' : '启用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无套餐" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }" @current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新建套餐弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新建套餐" width="680px" append-to-body destroy-on-close class="scrollable-dialog">
|
||||
<el-form :model="createForm" label-width="90px">
|
||||
<el-form-item label="套餐名称" required><el-input v-model="createForm.name" placeholder="请输入套餐名称" /></el-form-item>
|
||||
<el-form-item label="说明"><el-input v-model="createForm.note" type="textarea" :rows="2" placeholder="请输入套餐说明" /></el-form-item>
|
||||
<el-form-item label="参数配置">
|
||||
<div style="width:100%">
|
||||
<div v-if="!goodId" style="color:#c0c4cc;font-size:13px">请先选择商品</div>
|
||||
<div v-else-if="createSpecLoading" style="color:#909399;font-size:13px">加载参数中...</div>
|
||||
<div v-else-if="createSpecList.length === 0" style="color:#909399;font-size:13px">该商品暂无参数</div>
|
||||
<div v-else>
|
||||
<div v-for="spec in createSpecList" :key="spec.id" style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #f5f5f5">
|
||||
<div style="font-size:13px;font-weight:500;color:#303133;margin-bottom:6px">
|
||||
{{ spec.name }}
|
||||
<el-tag v-if="spec.must" size="small" type="danger" style="margin-left:4px">必填</el-tag>
|
||||
</div>
|
||||
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
|
||||
<el-radio-group v-model="createSpecValues[spec.id]" size="small" @change="buildCreateArgsJson">
|
||||
<el-radio-button v-for="attr in spec.attrs" :key="attr.id" :value="attr.id">{{ attr.name }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template v-else-if="spec.type === 'number'">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<el-input-number
|
||||
v-model="createDisplayValues[spec.id]"
|
||||
:min="hasUnit(spec) ? fromBaseUnit(spec.min ?? 0, createDisplayUnits[spec.id], getArgKey(spec)) : (spec.min ?? 0)"
|
||||
:max="hasUnit(spec) ? fromBaseUnit(spec.max ?? 0, createDisplayUnits[spec.id], getArgKey(spec)) : (spec.max ?? 0)"
|
||||
:step="hasUnit(spec) ? (fromBaseUnit(spec.step ?? 1, createDisplayUnits[spec.id], getArgKey(spec)) || 1) : (spec.step ?? 1)"
|
||||
:step-strictly="true"
|
||||
size="small"
|
||||
@change="onCreateNumberChange(spec)"
|
||||
style="width:180px"
|
||||
/>
|
||||
<el-select v-if="hasUnit(spec)" :model-value="createDisplayUnits[spec.id]" size="small" style="width:90px" @change="(newUnit) => onCreateUnitChange(spec, newUnit)">
|
||||
<el-option v-for="u in getParamUnits(spec)" :key="u" :label="u" :value="u" />
|
||||
</el-select>
|
||||
<span style="font-size:12px;color:#909399">范围: {{ spec.min ?? 0 }} ~ {{ spec.max ?? 0 }}
|
||||
<template v-if="hasUnit(spec)"> {{ getBaseUnit(getArgKey(spec)) }}</template>,步长: {{ spec.step ?? 1 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="createSpecValues[spec.id]" placeholder="请输入值" size="small" style="width:200px" @input="buildCreateArgsJson" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="createForm.args" style="margin-top:8px">
|
||||
<div style="font-size:12px;color:#909399;margin-bottom:4px">参数 JSON:</div>
|
||||
<el-input v-model="createForm.args" type="textarea" :rows="3" readonly style="font-family:monospace;font-size:12px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="createForm.index" :min="0" controls-position="right" style="width:120px" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getProductPlanList, createProductPlan, getProductParameterList } from '@/api/admin/product'
|
||||
import { hasUnit, getArgKey, getBaseUnit, getParamUnits, getParamDefaultUnit, toBaseUnit, fromBaseUnit } from '@/utils/dynamicUnit'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
goodId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const selected = ref(null)
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', note: '', index: 0, args: '' })
|
||||
const createSpecList = ref([])
|
||||
const createSpecLoading = ref(false)
|
||||
const createSpecValues = reactive({})
|
||||
const createDisplayValues = reactive({})
|
||||
const createDisplayUnits = reactive({})
|
||||
|
||||
watch(showCreate, (v) => {
|
||||
if (v && props.goodId) loadCreateSpec()
|
||||
})
|
||||
|
||||
const loadCreateSpec = async () => {
|
||||
createSpecLoading.value = true
|
||||
try {
|
||||
const res = await getProductParameterList({ good_id: props.goodId })
|
||||
if (res?.data?.code === 200) {
|
||||
createSpecList.value = res.data.data || []
|
||||
for (const spec of createSpecList.value) {
|
||||
if (spec.type === 'number') {
|
||||
if (createSpecValues[spec.id] === undefined) createSpecValues[spec.id] = spec.min ?? 0
|
||||
if (hasUnit(spec)) {
|
||||
createDisplayUnits[spec.id] = getParamDefaultUnit(spec)
|
||||
createDisplayValues[spec.id] = fromBaseUnit(spec.min ?? 0, createDisplayUnits[spec.id], getArgKey(spec))
|
||||
} else {
|
||||
createDisplayValues[spec.id] = spec.min ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { createSpecList.value = [] } finally { createSpecLoading.value = false }
|
||||
}
|
||||
|
||||
const onCreateNumberChange = (spec) => {
|
||||
if (hasUnit(spec)) {
|
||||
const argKey = getArgKey(spec)
|
||||
const unit = createDisplayUnits[spec.id]
|
||||
createSpecValues[spec.id] = Math.round(toBaseUnit(createDisplayValues[spec.id] || 0, unit, argKey))
|
||||
} else {
|
||||
createSpecValues[spec.id] = createDisplayValues[spec.id]
|
||||
}
|
||||
buildCreateArgsJson()
|
||||
}
|
||||
|
||||
const onCreateUnitChange = (spec, newUnit) => {
|
||||
const argKey = getArgKey(spec)
|
||||
const oldUnit = createDisplayUnits[spec.id]
|
||||
const oldDisplay = createDisplayValues[spec.id] || 0
|
||||
const baseValue = oldUnit ? toBaseUnit(oldDisplay, oldUnit, argKey) : oldDisplay
|
||||
createDisplayUnits[spec.id] = newUnit
|
||||
createDisplayValues[spec.id] = fromBaseUnit(baseValue, newUnit, argKey)
|
||||
createSpecValues[spec.id] = Math.round(baseValue)
|
||||
buildCreateArgsJson()
|
||||
}
|
||||
|
||||
const buildCreateArgsJson = () => {
|
||||
const result = []
|
||||
for (const spec of createSpecList.value) {
|
||||
const val = createSpecValues[spec.id]
|
||||
if (val === undefined || val === null || val === '') continue
|
||||
if (spec.type === 'select') {
|
||||
const attr = spec.attrs?.find(a => a.id === val)
|
||||
if (attr) result.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value, number: 0, key: getArgKey(spec) || undefined })
|
||||
} else if (spec.type === 'number') {
|
||||
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: '', number: val, key: getArgKey(spec) || undefined })
|
||||
} else {
|
||||
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val), number: 0, key: getArgKey(spec) || undefined })
|
||||
}
|
||||
}
|
||||
createForm.args = result.length > 0 ? JSON.stringify(result) : ''
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (props.goodId) params.good_id = props.goodId
|
||||
const res = await getProductPlanList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入套餐名称'); return }
|
||||
if (!props.goodId) { ElMessage.warning('请先选择商品'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('good_id', props.goodId)
|
||||
fd.append('name', createForm.name)
|
||||
if (createForm.note) fd.append('note', createForm.note)
|
||||
fd.append('index', createForm.index)
|
||||
if (createForm.args) fd.append('args', createForm.args)
|
||||
const res = await createProductPlan(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', note: '', index: 0, args: '' })
|
||||
for (const k in createSpecValues) delete createSpecValues[k]
|
||||
for (const k in createDisplayValues) delete createDisplayValues[k]
|
||||
for (const k in createDisplayUnits) delete createDisplayUnits[k]
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择商品组"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="group-selector">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索商品组名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 商品组列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="商品组名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="父级ID" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.parentId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.tag" size="small" type="info">{{ row.tag?.name || row.tag }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.disable ? 'danger' : 'success'" size="small">
|
||||
{{ row.disable ? '禁用' : '启用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="groupList.length === 0 && !loading" description="暂无商品组数据" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedGroup"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getProductGroupList } from '@/api/admin/product'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的商品组ID(用于回显)
|
||||
currentGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedGroup = ref(null)
|
||||
const keyword = ref('')
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
selectedGroup.value = null
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取商品组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
groupList.value = []
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
if (keyword.value.trim()) {
|
||||
params.keyword = keyword.value.trim()
|
||||
}
|
||||
const res = await getProductGroupList(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
const items = Array.isArray(inner) ? inner : (inner.data || inner.list || [])
|
||||
// 过滤掉已删除的
|
||||
groupList.value = items.filter(item => !item.delete)
|
||||
total.value = inner.all_count ?? inner.total ?? groupList.value.length
|
||||
|
||||
// 如果有当前选中的商品组ID,自动选中
|
||||
if (props.currentGroupId) {
|
||||
const current = groupList.value.find(
|
||||
g => g.id === Number(props.currentGroupId)
|
||||
)
|
||||
if (current) {
|
||||
selectedGroup.value = current
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(body?.message || '获取商品组列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品组列表失败:', error)
|
||||
ElMessage.error('获取商品组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择商品组
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedGroup.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedGroup.value && row.id === selectedGroup.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedGroup.value = null
|
||||
groupList.value = []
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedGroup.value) {
|
||||
emit('confirm', selectedGroup.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个商品组')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-selector {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择商品"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="product-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择商品 -->
|
||||
<el-tab-pane label="选择商品" name="selectProduct">
|
||||
<div class="product-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="商品分组">
|
||||
<el-select
|
||||
v-model="searchParams.good_group_id"
|
||||
placeholder="全部分组"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<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="searchParams.tag"
|
||||
placeholder="全部标签"
|
||||
:clearable="!defaultTag"
|
||||
:disabled="!!defaultTag"
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tagOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="productList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="商品ID" width="100" align="center" />
|
||||
<el-table-column label="商品图片" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.image || '/logo.svg'"
|
||||
fit="cover"
|
||||
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="table" label="所属表" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="价格" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="price">¥{{ (row.price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.inventory > 0 ? 'success' : 'danger'" size="small">
|
||||
{{ row.inventory }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="productList.length === 0 && !loading" description="暂无商品数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedProduct"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getProductList, getProductGroupList, getProductTagList } from '@/api/admin/product'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的商品ID(用于回显)
|
||||
currentProductId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 默认标签过滤(设置后自动锁定该标签)
|
||||
defaultTag: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectProduct')
|
||||
const loading = ref(false)
|
||||
const productList = ref([])
|
||||
const groupOptions = ref([])
|
||||
const tagOptions = ref([])
|
||||
const total = ref(0)
|
||||
const selectedProduct = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
good_group_id: '',
|
||||
tag: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
activeTab.value = 'selectProduct'
|
||||
selectedProduct.value = null
|
||||
searchParams.page = 1
|
||||
if (props.defaultTag) {
|
||||
searchParams.tag = props.defaultTag
|
||||
}
|
||||
fetchGroupList()
|
||||
fetchTagList()
|
||||
fetchProductList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取商品分组列表
|
||||
const fetchGroupList = async () => {
|
||||
try {
|
||||
const res = await getProductGroupList({ page: 1, count: 10 })
|
||||
if (res.data.code === 200) {
|
||||
groupOptions.value = res.data.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品标签列表
|
||||
const fetchTagList = async () => {
|
||||
try {
|
||||
const res = await getProductTagList()
|
||||
if (res.data.code === 200) {
|
||||
tagOptions.value = res.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
const fetchProductList = async () => {
|
||||
loading.value = true
|
||||
productList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
if (searchParams.good_group_id) {
|
||||
params.good_group_id = searchParams.good_group_id
|
||||
}
|
||||
if (searchParams.tag) {
|
||||
params.tag = searchParams.tag
|
||||
}
|
||||
|
||||
const res = await getProductList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const allData = res.data.data.data || []
|
||||
// 过滤掉已删除的数据(兼容 delete 字段不存在的情况)
|
||||
productList.value = allData.filter(item => item.delete !== true)
|
||||
total.value = res.data.data.all_count ?? allData.length
|
||||
|
||||
// cover 字段直接是图片 URL,无需再请求 file detail
|
||||
productList.value.forEach(item => {
|
||||
if (item.cover) item.image = item.cover
|
||||
})
|
||||
|
||||
// 如果有当前选中的商品ID,自动选中
|
||||
if (props.currentProductId) {
|
||||
const currentProduct = productList.value.find(
|
||||
product => product.id === props.currentProductId
|
||||
)
|
||||
if (currentProduct) {
|
||||
selectedProduct.value = currentProduct
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取商品列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
ElMessage.error('获取商品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectProduct') {
|
||||
fetchProductList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.good_group_id = ''
|
||||
searchParams.tag = props.defaultTag || ''
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 选择商品
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedProduct.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedProduct.value && row.id === selectedProduct.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedProduct.value = null
|
||||
productList.value = []
|
||||
searchParams.good_group_id = ''
|
||||
searchParams.tag = props.defaultTag || ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedProduct.value) {
|
||||
emit('confirm', selectedProduct.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个商品')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.product-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择安全组" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索安全组" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === 'in' ? 'primary' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : '出站' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="锁定" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="白名单" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建安全组</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getSecurityGroupList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; keyword.value = ''; selectedItem.value = null; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, page: page.value, page_size: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const res = await getSecurityGroupList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
list.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="adminGroup ? '选择管理员组' : '选择用户组'"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="user-group-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择用户组 -->
|
||||
<el-tab-pane :label="adminGroup ? '选择管理员组' : '选择用户组'" name="selectGroup">
|
||||
<div class="group-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
:placeholder="adminGroup ? '搜索管理员组名称' : '搜索用户组名称'"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 管理员组列表表格 -->
|
||||
<el-table
|
||||
v-if="adminGroup"
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="组名称" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="group-name">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="auth" label="权限标识" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.note || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 用户组列表表格 -->
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="组ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.group_id || row.GroupId || row.id || row.Id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="组名称" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="升级金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.floor_price || row.FloorPrice" class="price-text">
|
||||
¥{{ row.floor_price || row.FloorPrice }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下一级组ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.higher_level_id || row.HigherLevelId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
|
||||
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成员数量" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small" effect="plain">
|
||||
{{ row.member_count || row.MemberCount || 0 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="groupList.length === 0 && !loading" :description="adminGroup ? '暂无管理员组数据' : '暂无用户组数据'" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedGroup"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getUserGroupList } from '@/api/admin/user'
|
||||
import { getAdminGroupList } from '@/api/admin/group'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的用户组ID(用于回显)
|
||||
currentGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 排除的用户组ID(避免选择自己作为下一级)
|
||||
excludeGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 是否请求管理员组接口
|
||||
adminGroup: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectGroup')
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedGroup = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectGroup'
|
||||
selectedGroup.value = null
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取用户组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
groupList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
|
||||
const res = props.adminGroup ? await getAdminGroupList(params) : await getUserGroupList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
let responseData = res.data?.data || res.data
|
||||
|
||||
if (props.adminGroup) {
|
||||
groupList.value = responseData?.data || []
|
||||
total.value = responseData?.total || groupList.value.length
|
||||
} else if (Array.isArray(responseData)) {
|
||||
groupList.value = responseData
|
||||
total.value = responseData.length
|
||||
} else if (responseData.list) {
|
||||
groupList.value = responseData.list || []
|
||||
total.value = responseData.total || responseData.all_count || 0
|
||||
} else if (responseData.data && Array.isArray(responseData.data)) {
|
||||
groupList.value = responseData.data
|
||||
total.value = responseData.all_count || responseData.data.length
|
||||
} else {
|
||||
groupList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 过滤掉排除的用户组
|
||||
if (props.excludeGroupId) {
|
||||
groupList.value = groupList.value.filter(item => {
|
||||
const itemId = item.group_id || item.GroupId || item.id || item.Id
|
||||
return itemId !== props.excludeGroupId
|
||||
})
|
||||
}
|
||||
|
||||
// 关键词过滤
|
||||
if (searchParams.key) {
|
||||
const keyword = searchParams.key.toLowerCase()
|
||||
groupList.value = groupList.value.filter(item => {
|
||||
const name = (item.group_name || item.name || item.Name || '').toLowerCase()
|
||||
const id = String(item.group_id || item.GroupId || item.id || item.Id)
|
||||
return name.includes(keyword) || id.includes(keyword)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有当前选中的用户组ID,自动选中
|
||||
if (props.currentGroupId) {
|
||||
const currentGroup = groupList.value.find(item => {
|
||||
const itemId = item.group_id || item.GroupId || item.id || item.Id
|
||||
return itemId === props.currentGroupId
|
||||
})
|
||||
if (currentGroup) {
|
||||
selectedGroup.value = currentGroup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取用户组列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户组列表失败:', error)
|
||||
ElMessage.error('获取用户组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectGroup') {
|
||||
fetchGroupList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择用户组
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedGroup.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedGroup.value) {
|
||||
const selectedId = selectedGroup.value.group_id || selectedGroup.value.GroupId || selectedGroup.value.id || selectedGroup.value.Id
|
||||
const rowId = row.group_id || row.GroupId || row.id || row.Id
|
||||
if (rowId === selectedId) {
|
||||
return 'selected-row'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedGroup.value = null
|
||||
groupList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedGroup.value) {
|
||||
emit('confirm', selectedGroup.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个用户组')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-group-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.group-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择用户"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="user-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择用户 -->
|
||||
<el-tab-pane label="选择用户" name="selectUser">
|
||||
<div class="user-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索用户名或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户状态">
|
||||
<el-select
|
||||
v-model="searchParams.status"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="success" @click="switchToAdd" :icon="Plus">
|
||||
添加新用户
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" align="center" />
|
||||
<el-table-column prop="user_name" label="用户名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info-cell">
|
||||
<el-avatar :size="32" :src="row.cover">
|
||||
<el-icon><User /></el-icon>
|
||||
</el-avatar>
|
||||
<span class="user-name">{{ row.user_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" width="130" show-overflow-tooltip />
|
||||
<!-- <el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="created_at" label="注册时间" width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="userList.length === 0 && !loading" description="暂无用户数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 添加用户 -->
|
||||
<el-tab-pane label="添加用户" name="addUser">
|
||||
<div class="add-user-section">
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addFormRules"
|
||||
label-width="100px"
|
||||
class="add-user-form"
|
||||
>
|
||||
<el-form-item label="用户名" prop="user_name">
|
||||
<el-input
|
||||
v-model="addForm.user_name"
|
||||
placeholder="请输入用户名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="addForm.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
type="email"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="addForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="addForm.password"
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="addForm.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleAddUser" :loading="addLoading">
|
||||
<el-icon><Plus /></el-icon>
|
||||
立即创建
|
||||
</el-button>
|
||||
<el-button @click="resetAddForm">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置表单
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedUser"
|
||||
v-if="activeTab === 'selectUser'"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
|
||||
import { getUserList, createTask } from '@/api/admin/user'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的用户ID(用于回显)
|
||||
currentUserId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectUser')
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
const userList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedUser = ref(null)
|
||||
const addFormRef = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
status: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 添加用户表单
|
||||
const addForm = reactive({
|
||||
user_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 密码确认验证
|
||||
const validateConfirmPassword = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== addForm.password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户表单验证规则
|
||||
const addFormRules = {
|
||||
user_name: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '用户名长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{ validator: validateConfirmPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectUser'
|
||||
selectedUser.value = null
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
loading.value = true
|
||||
userList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
key: searchParams.key || ''
|
||||
}
|
||||
|
||||
const res = await getUserList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
userList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的用户ID,自动选中
|
||||
if (props.currentUserId) {
|
||||
const currentUser = userList.value.find(
|
||||
user => user.user_id === props.currentUserId
|
||||
)
|
||||
if (currentUser) {
|
||||
selectedUser.value = currentUser
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectUser') {
|
||||
fetchUserList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.status = ''
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 切换到添加用户标签页
|
||||
const switchToAdd = () => {
|
||||
activeTab.value = 'addUser'
|
||||
}
|
||||
|
||||
// 选择用户
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedUser.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedUser.value && row.user_id === selectedUser.value.user_id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
addLoading.value = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('user_name', addForm.user_name)
|
||||
formData.append('email', addForm.email)
|
||||
if (addForm.phone) {
|
||||
formData.append('phone', addForm.phone)
|
||||
}
|
||||
formData.append('password', addForm.password)
|
||||
|
||||
const res = await createTask(formData)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('用户创建成功')
|
||||
|
||||
// 获取新创建的用户信息
|
||||
const newUser = res.data.data
|
||||
|
||||
// 自动选择新创建的用户
|
||||
if (newUser) {
|
||||
selectedUser.value = {
|
||||
user_id: newUser.user_id || newUser.id,
|
||||
user_name: newUser.user_name || addForm.user_name,
|
||||
email: newUser.email || addForm.email,
|
||||
phone: newUser.phone || addForm.phone,
|
||||
...newUser
|
||||
}
|
||||
|
||||
// 触发确认事件并关闭弹窗
|
||||
emit('confirm', selectedUser.value)
|
||||
handleClose()
|
||||
} else {
|
||||
// 如果没有返回用户信息,切换到选择标签页并刷新列表
|
||||
activeTab.value = 'selectUser'
|
||||
searchParams.page = 1
|
||||
await fetchUserList()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
resetAddForm()
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '用户创建失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('用户创建失败:', error)
|
||||
ElMessage.error('用户创建失败')
|
||||
} finally {
|
||||
addLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置添加表单
|
||||
const resetAddForm = () => {
|
||||
addForm.user_name = ''
|
||||
addForm.email = ''
|
||||
addForm.phone = ''
|
||||
addForm.password = ''
|
||||
addForm.confirmPassword = ''
|
||||
addFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedUser.value = null
|
||||
userList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.status = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
resetAddForm()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedUser.value) {
|
||||
emit('confirm', selectedUser.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.user-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.user-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.add-user-section {
|
||||
padding: 30px 60px;
|
||||
}
|
||||
|
||||
.add-user-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-user-form :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-tag v-if="filterType" :type="filterType === 'bridge' ? 'success' : 'warning'" size="small" effect="dark">仅{{ filterType === 'bridge' ? '网桥' : 'NAT' }}</el-tag>
|
||||
<el-tag v-if="filterUnused" type="success" size="small" effect="dark">仅未占用</el-tag>
|
||||
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
|
||||
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="gateway" label="网关" min-width="120" />
|
||||
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button v-if="props.showCreateButton" type="success" @click="handleCreate">创建网络</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getUserVmNetworkList } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 },
|
||||
filterType: { type: String, default: '' },
|
||||
filterUnused: { type: Boolean, default: false },
|
||||
showCreateButton: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const ipVersionFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
ipVersionFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
|
||||
const res = await getUserVmNetworkList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
let all = inner.data || (Array.isArray(inner) ? inner : [])
|
||||
if (props.filterType) {
|
||||
all = all.filter(n => n.type === props.filterType)
|
||||
}
|
||||
if (props.filterUnused) {
|
||||
all = all.filter(n => !n.vm_id)
|
||||
}
|
||||
list.value = all
|
||||
total.value = inner.meta?.count ?? inner.total ?? all.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择安全组" width="640px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索安全组名称" clearable style="width:200px"
|
||||
@keyup.enter="loadList" @clear="loadList">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新增安全组</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="280" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === 'in' ? 'success' : 'warning'" size="small">
|
||||
{{ row.direction === 'in' ? '入站' : row.direction === 'out' ? '出站' : (row.direction || '-') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="白名单" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="共享" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.shared ? 'success' : 'info'" size="small">{{ row.shared ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无安全组" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增安全组弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新增安全组" width="440px" append-to-body destroy-on-close>
|
||||
<el-form :model="createForm" label-width="90px">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="createForm.name" placeholder="安全组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方向">
|
||||
<el-select v-model="createForm.direction" style="width:100%">
|
||||
<el-option label="入站 (in)" value="in" />
|
||||
<el-option label="出站 (out)" value="out" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="锁定">
|
||||
<el-switch v-model="createForm.lock" />
|
||||
</el-form-item>
|
||||
<el-form-item label="白名单">
|
||||
<el-switch v-model="createForm.drop_all" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserVmPostGroupUserList, createUserVmPostGroup } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', direction: 'in', lock: false, drop_all: false })
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { user_goods_id: props.userGoodsId, page: page.value, page_size: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const res = await getUserVmPostGroupUserList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.groups || d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const res = await createUserVmPostGroup({
|
||||
user_goods_id: props.userGoodsId,
|
||||
name: createForm.name,
|
||||
direction: createForm.direction,
|
||||
lock: createForm.lock,
|
||||
drop_all: createForm.drop_all
|
||||
})
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', direction: 'in', lock: false, drop_all: false })
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择数据卷进行挂载" width="680px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索数据卷名称" clearable style="width:200px"
|
||||
@keyup.enter="loadList" @clear="loadList">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建数据卷</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="280" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="80">
|
||||
<template #default="{ row }">{{ row.size }} GB</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂载" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_mount ? 'success' : 'info'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无数据卷" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected || !!selected.is_mount" @click="handleConfirm">
|
||||
{{ selected?.is_mount ? '已挂载' : '确定挂载' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新建数据卷弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新建数据卷" width="440px" append-to-body destroy-on-close>
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="名称" required><el-input v-model="createForm.name" placeholder="数据卷名称" /></el-form-item>
|
||||
<el-form-item label="大小">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="createForm.size" :min="1" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="createForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标设备名"><el-input v-model="createForm.target_device" placeholder="不填自动生成" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserVmVolumeList, createUserVmVolume } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserVmVolumeList({ user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const sizeGb = createForm._sizeUnit === 'TB' ? createForm.size * 1024 : createForm.size
|
||||
const res = await createUserVmVolume({
|
||||
user_goods_id: props.userGoodsId,
|
||||
name: createForm.name,
|
||||
size: sizeGb,
|
||||
target_device: createForm.target_device
|
||||
})
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => {
|
||||
if (selected.value && !selected.value.is_mount) {
|
||||
emit('confirm', selected.value)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-select { width: 90px; flex-shrink: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择虚拟机" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="hostIdFilter" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="loadList">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="配置" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.vcpu }}核 / {{ formatMem(row.memory) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { getRemoteHostList, getVmList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const hostIdFilter = ref('')
|
||||
const hostOptions = ref([])
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { loadHostOptions(); if (props.hostId) { hostIdFilter.value = props.hostId; loadList() } }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const loadHostOptions = async () => {
|
||||
try {
|
||||
const res = await getRemoteHostList({ service_id: props.serviceId, page: 1, page_size: 10 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
hostOptions.value = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
if (!hostIdFilter.value && hostOptions.value.length) hostIdFilter.value = hostOptions.value[0].id
|
||||
if (hostIdFilter.value) loadList()
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const loadList = async () => {
|
||||
if (!hostIdFilter.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVmList({ service_id: props.serviceId, host_id: hostIdFilter.value, page: 1, count: 10 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
list.value = inner.data || (Array.isArray(inner) ? inner : [])
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const formatMem = (kb) => {
|
||||
if (!kb) return '-'
|
||||
if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'
|
||||
if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'
|
||||
return kb + ' KB'
|
||||
}
|
||||
|
||||
const statusType = (s) => ({ running: 'success', ready: 'success', stopped: 'danger', error: 'danger', paused: 'warning' }[s] || 'info')
|
||||
const statusLabel = (s) => ({ running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中', stopped: '已停止', stop: '已停止', error: '错误', paused: '已暂停' }[s] || s || '-')
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择数据卷" width="750px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索数据卷" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="就绪" value="ready" />
|
||||
<el-option label="等待中" value="pending" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="90">
|
||||
<template #default="{ row }">{{ row.size ? row.size + ' GB' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂载" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_mount ? 'warning' : 'success'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建数据卷</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getVolumeList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const statusFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
statusFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId || !props.hostId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (statusFilter.value) params.status = statusFilter.value
|
||||
const res = await getVolumeList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
list.value = inner.data || inner.volumes || (Array.isArray(inner) ? inner : [])
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const statusType = (s) => ({ ready: 'success', pending: 'warning', error: 'danger' }[s] || 'info')
|
||||
const statusLabel = (s) => ({ ready: '就绪', pending: '等待中', creating: '创建中', error: '错误' }[s] || s || '-')
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择代金券"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="voucher-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择代金券 -->
|
||||
<el-tab-pane label="选择代金券" name="selectVoucher">
|
||||
<div class="voucher-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索代金券名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 代金券列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="voucherList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="代金券ID" width="100" align="center" />
|
||||
<el-table-column prop="name" label="代金券名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="code" label="代金券码" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" effect="plain">{{ row.code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
|
||||
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面值" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.percentage > 0" class="voucher-value">{{ row.percentage }}%</span>
|
||||
<span v-else class="voucher-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>无限制</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'expired': isExpired(row.endTime) }">
|
||||
{{ formatDate(row.endTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="voucherList.length === 0 && !loading" description="暂无代金券数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedVoucher"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getDiscountCodeList } from '@/api/admin/discount'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的代金券ID(用于回显)
|
||||
currentVoucherId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectVoucher')
|
||||
const loading = ref(false)
|
||||
const voucherList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedVoucher = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectVoucher'
|
||||
selectedVoucher.value = null
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchVoucherList = async () => {
|
||||
loading.value = true
|
||||
voucherList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
discount_type: 'coupon' // 代金券类型
|
||||
}
|
||||
if (searchParams.key) {
|
||||
params.key = searchParams.key
|
||||
}
|
||||
|
||||
const res = await getDiscountCodeList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
voucherList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的代金券ID,自动选中
|
||||
if (props.currentVoucherId) {
|
||||
const currentVoucher = voucherList.value.find(
|
||||
voucher => voucher.id === props.currentVoucherId
|
||||
)
|
||||
if (currentVoucher) {
|
||||
selectedVoucher.value = currentVoucher
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取代金券列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
ElMessage.error('获取代金券列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectVoucher') {
|
||||
fetchVoucherList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 选择代金券
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedVoucher.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedVoucher.value && row.id === selectedVoucher.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedVoucher.value = null
|
||||
voucherList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
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 isExpired = (endTime) => {
|
||||
if (!endTime) return false
|
||||
return new Date(endTime) < new Date()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedVoucher.value) {
|
||||
emit('confirm', selectedVoucher.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个代金券')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voucher-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.voucher-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.voucher-value {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.expired {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="selector-field-row">
|
||||
<el-input
|
||||
:model-value="displayText"
|
||||
readonly
|
||||
:placeholder="placeholder"
|
||||
style="flex:1"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="disabled"
|
||||
style="margin-left:8px"
|
||||
@click="$emit('select')"
|
||||
>{{ buttonText }}</el-button>
|
||||
<el-button
|
||||
v-if="clearable && modelValue"
|
||||
style="margin-left:4px"
|
||||
@click="$emit('update:modelValue', null); $emit('clear')"
|
||||
>清除</el-button>
|
||||
</div>
|
||||
<div v-if="hint" :style="{ fontSize: '12px', color: hintType === 'disabled' ? '#c0c4cc' : '#909399', marginTop: '4px' }">
|
||||
{{ hint }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Number, String, Object], default: null },
|
||||
displayText: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '请选择' },
|
||||
buttonText: { type: String, default: '选择' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: true },
|
||||
hint: { type: String, default: '' },
|
||||
hintType: { type: String, default: 'normal' }
|
||||
})
|
||||
|
||||
defineEmits(['select', 'clear', 'update:modelValue'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-layout" :class="{ 'sidebar-collapsed': isCollapsed, 'mobile-open': isMobileMenuOpen }">
|
||||
<!-- 移动端遮罩层 -->
|
||||
<div class="mobile-overlay" v-if="isMobileMenuOpen" @click="closeMobileMenu"></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" :class="{ 'collapsed': isCollapsed }">
|
||||
<div class="logo-container">
|
||||
<img src="@/assets/logo.png" alt="Logo" class="logo-img" />
|
||||
<img src="@/assets/logo.png" alt="Logo" class="logo-img" v-show="!isCollapsed" />
|
||||
<img src="@/assets/logo.svg" alt="Logo" class="logo-img-mini" v-show="isCollapsed" />
|
||||
</div>
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
<el-menu
|
||||
@@ -13,11 +17,20 @@
|
||||
text-color="#34495e"
|
||||
active-text-color="#2c3e50"
|
||||
:unique-opened="true"
|
||||
:collapse="isCollapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
>
|
||||
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<!-- 收缩按钮 -->
|
||||
<div class="collapse-btn" @click="toggleCollapse">
|
||||
<el-icon :size="18">
|
||||
<Fold v-if="!isCollapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主区域 -->
|
||||
@@ -25,10 +38,14 @@
|
||||
<!-- 顶部导航 -->
|
||||
<div class="navbar">
|
||||
<div class="navbar-left">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<el-button type="text" class="mobile-menu-btn" @click="toggleMobileMenu">
|
||||
<el-icon :size="22"><Menu /></el-icon>
|
||||
</el-button>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-item hidden-mobile">
|
||||
<el-tooltip content="全屏" placement="bottom">
|
||||
<el-button type="text" class="header-btn" @click="toggleFullScreen">
|
||||
<el-icon :size="18"><full-screen /></el-icon>
|
||||
@@ -39,9 +56,9 @@
|
||||
<div class="navbar-item">
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
||||
<span class="username">{{ userStore.userInfo.user_name }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
<el-avatar :size="32" :src="userStore.getUserAvatar() || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'" />
|
||||
<span class="username hidden-mobile">{{ userStore.userInfo.user_name }}</span>
|
||||
<el-icon class="el-icon--right hidden-mobile"><arrow-down /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -81,7 +98,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import SidebarMenuItem from './SidebarMenuItem.vue'
|
||||
import Breadcrumb from './Breadcrumb.vue'
|
||||
@@ -92,7 +109,10 @@ import {
|
||||
ArrowDown,
|
||||
User,
|
||||
Key,
|
||||
SwitchButton
|
||||
SwitchButton,
|
||||
Fold,
|
||||
Expand,
|
||||
Menu
|
||||
} from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
@@ -105,11 +125,46 @@ const router = useRouter()
|
||||
// 侧边栏菜单数据
|
||||
const menus = ref(menuConfig)
|
||||
|
||||
// 侧边栏收缩状态
|
||||
const isCollapsed = ref(false)
|
||||
|
||||
// 移动端菜单状态
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
// 检测是否是移动端
|
||||
const isMobile = ref(false)
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
// 移动端默认收起侧边栏
|
||||
if (isMobile.value) {
|
||||
isCollapsed.value = false
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前激活的菜单项
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 切换侧边栏收缩
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
// 保存状态到localStorage
|
||||
localStorage.setItem('sidebarCollapsed', isCollapsed.value)
|
||||
}
|
||||
|
||||
// 切换移动端菜单
|
||||
const toggleMobileMenu = () => {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
// 关闭移动端菜单
|
||||
const closeMobileMenu = () => {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
@@ -129,9 +184,35 @@ const handleLogout = () => {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('tokenExpire')
|
||||
localStorage.removeItem('userInfo')
|
||||
userStore.clearUserInfo()
|
||||
router.push('/login')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 监听路由变化,移动端自动关闭菜单
|
||||
router.afterEach(() => {
|
||||
if (isMobile.value) {
|
||||
closeMobileMenu()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 恢复侧边栏状态
|
||||
const savedState = localStorage.getItem('sidebarCollapsed')
|
||||
if (savedState !== null) {
|
||||
isCollapsed.value = savedState === 'true'
|
||||
}
|
||||
|
||||
// 检测设备类型
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -141,6 +222,18 @@ const handleLogout = () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端遮罩层 */
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
@@ -148,7 +241,15 @@ const handleLogout = () => {
|
||||
background-color: #ffffff;
|
||||
border-right: 1px solid #e1e8ed;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
z-index: 999;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
@@ -159,6 +260,7 @@ const handleLogout = () => {
|
||||
padding: 0 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
@@ -167,8 +269,15 @@ const handleLogout = () => {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-img-mini {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.sidebar-scrollbar {
|
||||
height: calc(100vh - 70px);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
@@ -178,6 +287,32 @@ const handleLogout = () => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 收缩按钮 */
|
||||
.collapse-btn {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
color: #2c3e50;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 移动端菜单按钮 */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
margin-right: 12px;
|
||||
padding: 8px;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
/* 主容器样式 */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
@@ -185,6 +320,7 @@ const handleLogout = () => {
|
||||
flex-direction: column;
|
||||
background-color: #f0f2f5;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
@@ -197,18 +333,21 @@ const handleLogout = () => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
@@ -286,6 +425,63 @@ const handleLogout = () => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 移动端隐藏元素 */
|
||||
.hidden-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 移动端响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: -260px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 260px;
|
||||
left: -260px;
|
||||
}
|
||||
|
||||
.admin-layout.mobile-open .sidebar {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.admin-layout.mobile-open .sidebar.collapsed {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu) {
|
||||
border-radius: 0;
|
||||
border: 1px solid #e1e8ed;
|
||||
@@ -346,77 +542,232 @@ const handleLogout = () => {
|
||||
/* Element Plus 菜单项样式优化 */
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 一级菜单标题(有子菜单) */
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
transition: background-color 0.2s ease;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 2px 8px;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
color: #34495e !important;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
transition: background-color 0.2s ease;
|
||||
/* 一级菜单项(无子菜单) */
|
||||
:deep(.sidebar-menu > .el-menu-item) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 2px 8px;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
color: #34495e !important;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
:deep(.sidebar-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.1) !important;
|
||||
:deep(.sidebar-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active::before) {
|
||||
:deep(.sidebar-menu > .el-menu-item.is-active::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
height: 24px;
|
||||
background-color: #2c3e50;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* 展开的一级菜单标题 */
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 600;
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
/* 二级菜单容器 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu) {
|
||||
background-color: transparent !important;
|
||||
padding: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
/* 二级菜单项 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 2px 8px 2px 16px;
|
||||
padding: 0 16px 0 28px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266 !important;
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #c0c4cc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||
/* 二级菜单中的子菜单标题(三级菜单父级) */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 2px 8px 2px 16px;
|
||||
padding: 0 16px 0 28px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266 !important;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #c0c4cc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu) {
|
||||
background-color: #fafbfc !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover::before) {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu-item) {
|
||||
margin: 0;
|
||||
padding-left: 48px !important;
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title) {
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title::before) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 三级菜单容器 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu) {
|
||||
background-color: transparent !important;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.12) !important;
|
||||
/* 三级菜单项 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin: 2px 8px 2px 28px;
|
||||
padding: 0 16px 0 24px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #909399 !important;
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
|
||||
content: '-';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #606266 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
|
||||
content: '•';
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 子菜单箭头图标 */
|
||||
:deep(.el-sub-menu__icon-arrow) {
|
||||
color: #909399 !important;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu:hover > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
color: #7f8c8d !important;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<template>
|
||||
<el-sub-menu v-if="hasChildren" :index="menu.path">
|
||||
<template #title>
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.title || menu.meta?.title }}</span>
|
||||
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
|
||||
</template>
|
||||
<sidebar-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.path"
|
||||
:menu="child"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="menu.path">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ menu.title || menu.meta?.title }}</template>
|
||||
<template #title>
|
||||
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
@@ -29,6 +32,10 @@ const props = defineProps({
|
||||
menu: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,49 +46,45 @@ const hasChildren = computed(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-icon {
|
||||
margin-right: 12px;
|
||||
/* 菜单图标样式 */
|
||||
.menu-icon {
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-menu-item .el-icon, :deep(.el-sub-menu__title .el-icon) {
|
||||
/* 菜单标题 */
|
||||
.menu-title {
|
||||
font-size: inherit;
|
||||
letter-spacing: 0.2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 图标交互状态 */
|
||||
.el-menu-item .menu-icon,
|
||||
:deep(.el-sub-menu__title .menu-icon) {
|
||||
color: #7f8c8d !important;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.el-menu-item:hover .el-icon,
|
||||
:deep(.el-sub-menu__title:hover .el-icon) {
|
||||
.el-menu-item:hover .menu-icon,
|
||||
:deep(.el-sub-menu__title:hover .menu-icon) {
|
||||
color: #34495e !important;
|
||||
}
|
||||
|
||||
/* 激活菜单项图标 */
|
||||
.el-menu-item.is-active .el-icon {
|
||||
.el-menu-item.is-active .menu-icon {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .menu-icon) {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
/* 菜单文字样式 */
|
||||
.el-menu-item span, :deep(.el-sub-menu__title span) {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
/* 子菜单项样式优化 */
|
||||
:deep(.el-sub-menu .el-menu-item) {
|
||||
font-size: 13px;
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu-item .el-icon) {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<div class="tags-view-wrapper">
|
||||
<div class="tags-view-container"
|
||||
@mouseenter="hovered = true" @mouseleave="hovered = false">
|
||||
<div class="tags-view-wrapper" ref="scrollWrapperRef"
|
||||
@wheel.prevent="handleWheel"
|
||||
@scroll="onScroll">
|
||||
<div class="tags-view-scroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
@@ -24,6 +27,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-track" :class="{ visible: hovered && hasOverflow }">
|
||||
<div class="scroll-thumb" :style="thumbStyle" @mousedown="onThumbDown"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ul v-show="visible" :style="{left: left+'px', top: top+'px'}" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
@@ -65,28 +72,20 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
// 访问过的标签 (从 store 获取)
|
||||
const visitedViews = computed(() => tagsViewStore.visitedViews)
|
||||
const affixTags = computed(() => tagsViewStore.affixTags)
|
||||
|
||||
// 右键菜单
|
||||
const visible = ref(false)
|
||||
const top = ref(0)
|
||||
const left = ref(0)
|
||||
const selectedTag = ref({})
|
||||
|
||||
// 初始化标签
|
||||
const initTags = () => {
|
||||
// 如果当前路由不在访问过的标签中,添加它
|
||||
if (route.name) {
|
||||
tagsViewStore.addVisitedView(route)
|
||||
}
|
||||
// 添加固定标签(仪表盘)
|
||||
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
||||
if (dashboardRoute) {
|
||||
// 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
|
||||
// 简单起见,我们假设 store 的 affixTags 是可以直接修改的 ref,或者我们在 store 中添加初始化逻辑
|
||||
// 但为了保持一致性,我们这里只处理 visitedViews 的添加
|
||||
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
|
||||
tagsViewStore.affixTags.push(dashboardRoute)
|
||||
}
|
||||
@@ -94,13 +93,11 @@ const initTags = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新选中的标签
|
||||
const refreshSelectedTag = (view) => {
|
||||
const { fullPath } = view
|
||||
router.replace('/redirect' + fullPath)
|
||||
}
|
||||
|
||||
// 关闭选中的标签
|
||||
const closeSelectedTag = (view) => {
|
||||
tagsViewStore.delVisitedView(view).then((visitedViews) => {
|
||||
if (isActive(view)) {
|
||||
@@ -109,15 +106,11 @@ const closeSelectedTag = (view) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭其他标签
|
||||
const closeOthersTags = () => {
|
||||
router.push(selectedTag.value)
|
||||
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
|
||||
// moveToCurrentTag() // 如果有滚动逻辑
|
||||
})
|
||||
tagsViewStore.delOthersViews(selectedTag.value)
|
||||
}
|
||||
|
||||
// 关闭左侧标签
|
||||
const closeLeftTags = () => {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
|
||||
if (!visitedViews.find(i => i.path === route.path)) {
|
||||
@@ -126,7 +119,6 @@ const closeLeftTags = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭右侧标签
|
||||
const closeRightTags = () => {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
|
||||
if (!visitedViews.find(i => i.path === route.path)) {
|
||||
@@ -135,20 +127,17 @@ const closeRightTags = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭所有标签
|
||||
const closeAllTags = () => {
|
||||
tagsViewStore.delAllViews().then((visitedViews) => {
|
||||
toLastView(visitedViews)
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到最后一个标签或首页
|
||||
const toLastView = (visitedViews, view) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
router.push(latestView)
|
||||
} else {
|
||||
// 如果没有标签,则跳转到首页
|
||||
if (view && view.name === 'Dashboard') {
|
||||
router.push('/redirect' + '/dashboard')
|
||||
} else {
|
||||
@@ -157,17 +146,14 @@ const toLastView = (visitedViews, view) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是当前激活的标签
|
||||
const isActive = (tag) => {
|
||||
return tag.path === route.path
|
||||
}
|
||||
|
||||
// 判断是否是固定标签
|
||||
const isAffix = (tag) => {
|
||||
return tag.meta && tag.meta.affix
|
||||
}
|
||||
|
||||
// 打开右键菜单
|
||||
const openMenu = (e, tag) => {
|
||||
const menuMinWidth = 125
|
||||
const offsetLeft = e.clientX
|
||||
@@ -181,30 +167,112 @@ const openMenu = (e, tag) => {
|
||||
selectedTag.value = tag
|
||||
}
|
||||
|
||||
// 关闭右键菜单
|
||||
// ---- 滚动 & 滚动条 ----
|
||||
const scrollWrapperRef = ref(null)
|
||||
const hovered = ref(false)
|
||||
const hasOverflow = ref(false)
|
||||
const thumbStyle = ref({ width: '0px', left: '0px' })
|
||||
|
||||
const handleWheel = (e) => {
|
||||
if (scrollWrapperRef.value) {
|
||||
scrollWrapperRef.value.scrollLeft += e.deltaY || e.deltaX
|
||||
}
|
||||
}
|
||||
|
||||
const refreshState = () => {
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = el
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
hasOverflow.value = maxScroll > 1
|
||||
|
||||
if (!hasOverflow.value) {
|
||||
thumbStyle.value = { width: '0px', left: '0px' }
|
||||
return
|
||||
}
|
||||
|
||||
const trackWidth = clientWidth
|
||||
const thumbW = Math.max((clientWidth / scrollWidth) * trackWidth, 30)
|
||||
const scrollRatio = maxScroll > 0 ? scrollLeft / maxScroll : 0
|
||||
const thumbLeft = scrollRatio * (trackWidth - thumbW)
|
||||
thumbStyle.value = { width: thumbW + 'px', left: thumbLeft + 'px' }
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
refreshState()
|
||||
}
|
||||
|
||||
const scrollToActiveTag = () => {
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
const activeEl = el.querySelector('.active-tag')
|
||||
if (!activeEl) return
|
||||
const wrapperRect = el.getBoundingClientRect()
|
||||
const tagRect = activeEl.getBoundingClientRect()
|
||||
if (tagRect.left < wrapperRect.left + 28) {
|
||||
el.scrollLeft -= (wrapperRect.left + 28 - tagRect.left + 12)
|
||||
} else if (tagRect.right > wrapperRect.right - 28) {
|
||||
el.scrollLeft += (tagRect.right - wrapperRect.right + 28 + 12)
|
||||
}
|
||||
}
|
||||
|
||||
const onThumbDown = (e) => {
|
||||
e.preventDefault()
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
const startX = e.clientX
|
||||
const startScroll = el.scrollLeft
|
||||
const maxScroll = el.scrollWidth - el.clientWidth
|
||||
const trackWidth = el.clientWidth
|
||||
const thumbW = Math.max((el.clientWidth / el.scrollWidth) * trackWidth, 30)
|
||||
const movable = trackWidth - thumbW
|
||||
|
||||
const onMove = (ev) => {
|
||||
const dx = ev.clientX - startX
|
||||
const scrollDelta = movable > 0 ? (dx / movable) * maxScroll : 0
|
||||
el.scrollLeft = Math.min(Math.max(startScroll + scrollDelta, 0), maxScroll)
|
||||
}
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove)
|
||||
document.removeEventListener('mouseup', onUp)
|
||||
}
|
||||
document.addEventListener('mousemove', onMove)
|
||||
document.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
watch(visitedViews, () => nextTick(() => { refreshState(); scrollToActiveTag() }), { deep: true })
|
||||
|
||||
const closeMenu = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 监听路由变化,添加标签
|
||||
watch(route, (newRoute) => {
|
||||
if (newRoute.name) {
|
||||
tagsViewStore.addVisitedView(newRoute)
|
||||
}
|
||||
nextTick(scrollToActiveTag)
|
||||
})
|
||||
|
||||
// 点击其他区域关闭右键菜单
|
||||
const handleClickOutside = () => {
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
refreshState()
|
||||
scrollToActiveTag()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
nextTick(() => { refreshState(); scrollToActiveTag() })
|
||||
window.addEventListener('resize', onResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener('resize', onResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -215,17 +283,21 @@ onBeforeUnmount(() => {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 标签滚动区域 */
|
||||
.tags-view-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.tags-view-wrapper::-webkit-scrollbar {
|
||||
@@ -233,18 +305,51 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.tags-view-scroll {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 底部自定义滚动条(在容器上,不在滚动区域内) */
|
||||
.scroll-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.scroll-track.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.scroll-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: rgba(180,188,199,0.45);
|
||||
transition: background 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scroll-thumb:hover {
|
||||
background: rgba(180,188,199,0.65);
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.tag, .active-tag {
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
@@ -252,6 +357,8 @@ onBeforeUnmount(() => {
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -326,6 +433,7 @@ onBeforeUnmount(() => {
|
||||
background-color: rgba(231, 76, 60, 0.1);
|
||||
}
|
||||
|
||||
/* 右键菜单 */
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 环境配置文件
|
||||
* 所有硬编码的 URL / 域名 / 环境变量统一在此管理
|
||||
*/
|
||||
|
||||
// 当前环境
|
||||
const isDevelopment = import.meta.env.MODE === 'development'
|
||||
|
||||
// API 基础地址
|
||||
// 开发环境使用 vite 代理 (baseUrl 为空),生产环境使用实际地址
|
||||
const API_BASE_MAP = {
|
||||
development: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren', // 直接请求后端,不走 vite proxy
|
||||
production: import.meta.env.VITE_API_BASE_URL || 'https://cloudapi.007yjs.com',
|
||||
staging: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren'
|
||||
}
|
||||
|
||||
// 获取当前环境的 API 基础地址
|
||||
const currentEnv = import.meta.env.VITE_APP_ENV || import.meta.env.MODE || 'development'
|
||||
export const baseUrl = API_BASE_MAP[currentEnv] || API_BASE_MAP.development
|
||||
|
||||
// ACS 服务基础地址
|
||||
export const acsBaseUrl = baseUrl
|
||||
|
||||
// 网站标题
|
||||
export const siteTitle = '007UI管理系统'
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
export const requestTimeout = 50000
|
||||
export const acsRequestTimeout = 30000
|
||||
|
||||
// Token 存储键名
|
||||
export const TOKEN_KEY = 'token'
|
||||
export const TOKEN_EXPIRE_KEY = 'tokenExpire'
|
||||
export const USER_INFO_KEY = 'userInfo'
|
||||
|
||||
// 不需要 token 认证的 URL 前缀
|
||||
export const noAuthUrls = [
|
||||
'/v1/user/login',
|
||||
'/v1/user/check/get_code_img',
|
||||
'/v1/user/register',
|
||||
'/v1/user/refresh_token'
|
||||
]
|
||||
|
||||
export default {
|
||||
isDevelopment,
|
||||
baseUrl,
|
||||
acsBaseUrl,
|
||||
siteTitle,
|
||||
requestTimeout,
|
||||
acsRequestTimeout,
|
||||
TOKEN_KEY,
|
||||
TOKEN_EXPIRE_KEY,
|
||||
USER_INFO_KEY,
|
||||
noAuthUrls
|
||||
}
|
||||
+54
-57
@@ -5,12 +5,18 @@ export const menus = [
|
||||
icon: 'DataBoard'
|
||||
},
|
||||
{
|
||||
path : '/ticket',
|
||||
title: '工单处理',
|
||||
icon: 'DataBoard'
|
||||
path: '/ticket',
|
||||
title: '工单管理',
|
||||
icon: 'Tickets',
|
||||
children: [
|
||||
{
|
||||
path: '/ticket/list',
|
||||
title: '工单列表'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path:'/user',
|
||||
path: '/user',
|
||||
title: '用户管理',
|
||||
icon: 'User',
|
||||
children: [
|
||||
@@ -18,10 +24,6 @@ export const menus = [
|
||||
path: '/user/list',
|
||||
title: '用户列表'
|
||||
},
|
||||
{
|
||||
path: '/user/balance',
|
||||
title: '用户余额管理'
|
||||
},
|
||||
{
|
||||
path: '/user/group',
|
||||
title: '用户组管理'
|
||||
@@ -37,18 +39,16 @@ export const menus = [
|
||||
title: '商品管理',
|
||||
icon: 'Goods',
|
||||
children: [
|
||||
{
|
||||
path: '/product/list',
|
||||
title: '商品列表'
|
||||
},
|
||||
{
|
||||
path: '/product/group',
|
||||
title: '商品分组'
|
||||
},
|
||||
{
|
||||
path: '/product/parameter',
|
||||
title: '商品参数'
|
||||
}
|
||||
{ path: '/product/manage', title: '商品管理' }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user-goods',
|
||||
title: '用户商品管理',
|
||||
icon: 'ShoppingCart',
|
||||
children: [
|
||||
{ path: '/user-goods/list', title: '所有商品' },
|
||||
{ path: '/user-goods/vm-list', title: '云服务器' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -75,36 +75,7 @@ export const menus = [
|
||||
path: '/marketing/voucher',
|
||||
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 +86,10 @@ export const menus = [
|
||||
{
|
||||
path: '/activity/signin',
|
||||
title: '签到活动'
|
||||
},
|
||||
{
|
||||
path: '/activity/groupbuy',
|
||||
title: '拼团管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -158,14 +133,32 @@ export const menus = [
|
||||
]
|
||||
},
|
||||
{
|
||||
path:'/setting',
|
||||
title:'全局设置管理',
|
||||
children:[
|
||||
{path:'/setting/global',title:'全局设置'}
|
||||
path: '/setting',
|
||||
title: '全局设置管理',
|
||||
children: [
|
||||
{ path: '/setting/global', title: '全局设置' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '虚拟化平台管理',
|
||||
icon: 'Platform',
|
||||
children: [
|
||||
{
|
||||
path: '/virtualization/kvm-service',
|
||||
title: '主控服务管理'
|
||||
},
|
||||
{
|
||||
path: '/virtualization/host-group-mapping',
|
||||
title: '宿主机组映射管理'
|
||||
},
|
||||
{
|
||||
path: '/virtualization/vnc-command',
|
||||
title: 'VNC指令管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
title: '系统管理',
|
||||
@@ -190,12 +183,16 @@ export const menus = [
|
||||
title: '域名白名单'
|
||||
},
|
||||
{
|
||||
path: '/system/setting-group',
|
||||
title: '配置组管理'
|
||||
path: '/system/setting-manage',
|
||||
title: '配置管理'
|
||||
},
|
||||
{
|
||||
path: '/system/setting-list',
|
||||
title: '配置管理'
|
||||
path: '/system/menu',
|
||||
title: '菜单管理',
|
||||
children: [
|
||||
{ path: '/system/menu-manage', title: '菜单列表' },
|
||||
{ path: '/system/menu-permission', title: '菜单权限' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+251
-71
@@ -39,7 +39,27 @@ const routes = [
|
||||
title: '工单管理',
|
||||
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管理路由
|
||||
@@ -208,35 +228,49 @@ const routes = [
|
||||
{
|
||||
path: 'product',
|
||||
name: 'Product',
|
||||
meta: {
|
||||
title: '商品管理',
|
||||
icon: 'Goods'
|
||||
},
|
||||
redirect: '/product/list',
|
||||
meta: { title: '商品管理', icon: 'Goods' },
|
||||
redirect: '/product/manage',
|
||||
children: [
|
||||
{
|
||||
path: 'manage',
|
||||
name: 'ProductManage',
|
||||
component: () => import('../views/product/ProductGroup.vue'),
|
||||
meta: { title: '商品管理' }
|
||||
},
|
||||
{ path: 'list', redirect: '/product/manage' },
|
||||
{ path: 'group', redirect: '/product/manage' }
|
||||
]
|
||||
},
|
||||
// 用户商品管理路由
|
||||
{
|
||||
path: 'user-goods',
|
||||
name: 'UserGoods',
|
||||
meta: { title: '用户商品管理', icon: 'ShoppingCart' },
|
||||
redirect: '/user-goods/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'ProductList',
|
||||
component: () => import('../views/product/ProductList.vue'),
|
||||
meta: {
|
||||
title: '商品列表'
|
||||
}
|
||||
name: 'UserGoodsList',
|
||||
component: () => import('../views/product/UserGoodsList.vue'),
|
||||
meta: { title: '所有商品' }
|
||||
},
|
||||
{
|
||||
path: 'group',
|
||||
name: 'ProductGroup',
|
||||
component: () => import('../views/product/ProductGroup.vue'),
|
||||
meta: {
|
||||
title: '商品分组'
|
||||
}
|
||||
path: 'detail/:id',
|
||||
name: 'UserGoodsDetail',
|
||||
component: () => import('../views/product/UserGoodsDetail.vue'),
|
||||
meta: { title: '用户商品详情', hidden: true, activeMenu: '/user-goods/list' }
|
||||
},
|
||||
{
|
||||
path: 'parameter',
|
||||
name: 'ProductParameter',
|
||||
component: () => import('../views/product/ProductParameter.vue'),
|
||||
meta: {
|
||||
title: '商品参数'
|
||||
}
|
||||
path: 'vm-list',
|
||||
name: 'UserVmList',
|
||||
component: () => import('../views/user-vm/UserVmList.vue'),
|
||||
meta: { title: '云服务器' }
|
||||
},
|
||||
{
|
||||
path: 'vm-detail',
|
||||
name: 'UserVmDetail',
|
||||
component: () => import('../views/user-vm/UserVmDetail.vue'),
|
||||
meta: { title: '用户虚拟机详情', hidden: true, activeMenu: '/user-goods/vm-list' }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -287,49 +321,16 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user-distribution',
|
||||
name: 'UserDistribution',
|
||||
component: () => import('../views/marketing/UserVoucher.vue'),
|
||||
path: 'voucher/:id/manage',
|
||||
name: 'VoucherManagement',
|
||||
component: () => import('../views/marketing/VoucherManagement.vue'),
|
||||
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 +350,14 @@ const routes = [
|
||||
meta: {
|
||||
title: '签到活动'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/activity/groupbuy',
|
||||
name: 'GroupBuyManage',
|
||||
component: () => import('../views/activity/GroupBuyManage.vue'),
|
||||
meta: {
|
||||
title: '拼团管理'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -394,16 +403,187 @@ const routes = [
|
||||
meta: { title: '域名白名单' }
|
||||
},
|
||||
{
|
||||
path: 'setting-group',
|
||||
name: 'SettingGroup',
|
||||
component: () => import('../views/system/SettingGroup.vue'),
|
||||
meta: { title: '配置组管理' }
|
||||
path: 'setting-manage',
|
||||
name: 'SettingManage',
|
||||
component: () => import('../views/system/SettingManage.vue'),
|
||||
meta: { title: '配置管理' }
|
||||
},
|
||||
{
|
||||
path: 'setting-list',
|
||||
name: 'SettingList',
|
||||
component: () => import('../views/system/Setting.vue'),
|
||||
meta: { title: '配置管理' }
|
||||
path: 'menu-manage',
|
||||
name: 'MenuManage',
|
||||
component: () => import('../views/system/MenuManage.vue'),
|
||||
meta: { title: '菜单管理' }
|
||||
},
|
||||
{
|
||||
path: 'menu-permission',
|
||||
name: 'MenuPermission',
|
||||
component: () => import('../views/system/MenuPermission.vue'),
|
||||
meta: { title: '菜单权限' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'virtualization',
|
||||
name: 'Virtualization',
|
||||
meta: {
|
||||
title: '虚拟化平台管理',
|
||||
icon: 'Platform'
|
||||
},
|
||||
redirect: '/virtualization/kvm-service',
|
||||
children: [
|
||||
{
|
||||
path: 'kvm-service',
|
||||
name: 'KvmService',
|
||||
component: () => import('../views/virtualization/KvmService.vue'),
|
||||
meta: {
|
||||
title: '主控服务管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'kvm-service-detail',
|
||||
name: 'KvmServiceDetail',
|
||||
component: () => import('../views/virtualization/KvmServiceDetail.vue'),
|
||||
meta: {
|
||||
title: '主控服务详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-group-mapping',
|
||||
name: 'HostGroupMapping',
|
||||
component: () => import('../views/virtualization/HostGroupMapping.vue'),
|
||||
meta: {
|
||||
title: '宿主机组映射管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-manage',
|
||||
name: 'HostManage',
|
||||
component: () => import('../views/virtualization/HostManage.vue'),
|
||||
meta: {
|
||||
title: '宿主机管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'image-manage',
|
||||
name: 'ImageManage',
|
||||
component: () => import('../views/virtualization/ImageManage.vue'),
|
||||
meta: {
|
||||
title: '镜像管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'network-manage',
|
||||
name: 'NetworkManage',
|
||||
component: () => import('../views/virtualization/NetworkManage.vue'),
|
||||
meta: {
|
||||
title: '网络管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'volume-manage',
|
||||
name: 'VolumeManage',
|
||||
component: () => import('../views/virtualization/VolumeManage.vue'),
|
||||
meta: {
|
||||
title: '数据卷管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vm-manage',
|
||||
name: 'VmManage',
|
||||
component: () => import('../views/virtualization/VmManage.vue'),
|
||||
meta: {
|
||||
title: '虚拟机管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'security-group',
|
||||
name: 'SecurityGroupManage',
|
||||
component: () => import('../views/virtualization/SecurityGroupManage.vue'),
|
||||
meta: {
|
||||
title: '安全组管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vnc-node',
|
||||
name: 'VncNodeManage',
|
||||
component: () => import('../views/virtualization/VncNodeManage.vue'),
|
||||
meta: {
|
||||
title: 'VNC节点管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vnc-command',
|
||||
name: 'VncCommandManage',
|
||||
component: () => import('../views/virtualization/VncCommandManage.vue'),
|
||||
meta: {
|
||||
title: 'VNC指令管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-detail',
|
||||
name: 'VirtHostDetail',
|
||||
component: () => import('../views/virtualization/HostDetail.vue'),
|
||||
meta: {
|
||||
title: '宿主机详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'image-detail',
|
||||
name: 'VirtImageDetail',
|
||||
component: () => import('../views/virtualization/ImageDetail.vue'),
|
||||
meta: {
|
||||
title: '镜像详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vm-detail',
|
||||
name: 'VirtVmDetail',
|
||||
component: () => import('../views/virtualization/VmDetail.vue'),
|
||||
meta: {
|
||||
title: '虚拟机详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'security-group-detail',
|
||||
name: 'VirtSecurityGroupDetail',
|
||||
component: () => import('../views/virtualization/SecurityGroupDetail.vue'),
|
||||
meta: {
|
||||
title: '安全组详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'volume-detail',
|
||||
name: 'VirtVolumeDetail',
|
||||
component: () => import('../views/virtualization/VolumeDetail.vue'),
|
||||
meta: {
|
||||
title: '数据卷详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+19
-2
@@ -4,11 +4,28 @@ import {ref} from "vue";
|
||||
|
||||
export const useUserStore = defineStore('userStore',() => {
|
||||
|
||||
let userInfo = ref({})
|
||||
// 初始化时从localStorage读取用户信息
|
||||
const savedUserInfo = localStorage.getItem('userInfo')
|
||||
let userInfo = ref(savedUserInfo ? JSON.parse(savedUserInfo) : {})
|
||||
|
||||
function setUserInfo(u){
|
||||
userInfo.value = u
|
||||
// 同步保存到localStorage
|
||||
if (u && Object.keys(u).length > 0) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(u))
|
||||
}
|
||||
}
|
||||
|
||||
return {userInfo,setUserInfo}
|
||||
// 清除用户信息
|
||||
function clearUserInfo() {
|
||||
userInfo.value = {}
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
// 获取用户头像
|
||||
function getUserAvatar() {
|
||||
return userInfo.value?.cover || ''
|
||||
}
|
||||
|
||||
return {userInfo, setUserInfo, clearUserInfo, getUserAvatar}
|
||||
})
|
||||
+374
-1
@@ -114,11 +114,384 @@ body {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 响应式工具类 */
|
||||
/* 可点击元素统一手型光标 */
|
||||
.el-button,
|
||||
.el-button--link,
|
||||
.el-tag.is-closable .el-tag__close,
|
||||
.el-dropdown,
|
||||
.el-dropdown-menu__item,
|
||||
.el-switch,
|
||||
.el-checkbox,
|
||||
.el-radio,
|
||||
.el-select .el-input__wrapper,
|
||||
.el-table__body tr.el-table__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.back-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ==================== 全局弹窗卡片样式 ==================== */
|
||||
/* 自动为所有未手动分区的弹窗表单添加卡片背景 */
|
||||
.el-dialog:not(.tk-dialog):not(.token-dialog):not(.token-result-dialog) .el-dialog__body > .el-form {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 20px 20px 4px;
|
||||
border: 1px solid #f0f2f5;
|
||||
}
|
||||
/* 统一弹窗 footer 按钮对齐 */
|
||||
.el-dialog .el-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.tk-dialog .el-dialog__body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
.tk-dialog .el-dialog__body::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
.tk-dialog .el-form {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tk-section {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 20px 20px 4px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f0f2f5;
|
||||
}
|
||||
.tk-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 18px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid #409eff;
|
||||
line-height: 1;
|
||||
}
|
||||
.tk-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.tk-resource-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 24px;
|
||||
}
|
||||
.tk-resource-grid .el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tk-resource-grid .el-form-item .el-form-item__label {
|
||||
width: 80px !important;
|
||||
}
|
||||
.tk-resource-grid .el-form-item .el-form-item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.tk-resource-grid .el-input-number {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tk-unit-select {
|
||||
width: 68px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tk-res-unit {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tk-inline-unit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
.tk-inline-unit .el-input-number,
|
||||
.tk-inline-unit .el-input,
|
||||
.tk-inline-unit .el-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ==================== 全局页面布局组件 ==================== */
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.page-header .header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-header .header-info h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
.page-header .sub-info {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.page-header .header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 嵌入式工具栏 */
|
||||
.embedded-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 通用工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 筛选区域(卡片式) */
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* 绑定选择器行 */
|
||||
.bind-selector-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 详情操作按钮组 */
|
||||
.detail-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ==================== 全局表格增强 ==================== */
|
||||
.el-table {
|
||||
--el-table-header-bg-color: #fafafa;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
--el-table-border-color: #ebeef5;
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
font-weight: 600 !important;
|
||||
color: #1d2129 !important;
|
||||
font-size: 13px !important;
|
||||
border-bottom: 2px solid #e1e8ed !important;
|
||||
}
|
||||
.el-table td.el-table__cell {
|
||||
border-bottom: 1px solid #f0f2f5 !important;
|
||||
color: #34495e !important;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.el-table .el-table__empty-block {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.el-table .el-table__empty-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* 表格固定列阴影 */
|
||||
.el-table__fixed {
|
||||
box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.el-table__fixed-right {
|
||||
box-shadow: -4px 0 8px -4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ==================== 全局骨架屏样式 ==================== */
|
||||
@keyframes tk-skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 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%, #e8e8e8 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: tk-skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 全局过渡动画 ==================== */
|
||||
.el-table,
|
||||
.el-card,
|
||||
.el-tag,
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* ==================== 通用文本类 ==================== */
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.mono-text {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ==================== 视觉增强 ==================== */
|
||||
|
||||
/* 卡片式筛选区域 */
|
||||
.filter-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 通用结果/令牌展示 */
|
||||
.tk-result-wrapper { text-align: center; }
|
||||
.tk-result-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; text-align: left; }
|
||||
.tk-result-icon { font-size: 36px; color: #e6a23c; background: #fdf6ec; border-radius: 50%; padding: 10px; }
|
||||
.tk-result-name { font-size: 16px; font-weight: 600; color: #1d2129; }
|
||||
.tk-result-meta { font-size: 13px; color: #909399; margin-top: 2px; }
|
||||
.tk-token-block { background: #1d2129; border-radius: 8px; padding: 16px; margin-bottom: 16px; text-align: left; }
|
||||
.tk-token-label { font-size: 11px; color: #909399; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.tk-token-value { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; color: #67c23a; word-break: break-all; line-height: 1.6; user-select: all; }
|
||||
.tk-copy-btn { width: 100%; }
|
||||
|
||||
/* 表单提示 */
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 资源信息标签组 */
|
||||
.resource-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 响应式工具类 ==================== */
|
||||
|
||||
/* 表格横向滚动提示 */
|
||||
.el-table {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hidden-xs {
|
||||
display: none !important;
|
||||
}
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-header .header-right {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.filter-bar .el-input,
|
||||
.filter-bar .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
.pagination-wrapper .el-pagination {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 弹窗在移动端更宽 */
|
||||
.el-dialog {
|
||||
width: 92% !important;
|
||||
margin: 5vh auto !important;
|
||||
}
|
||||
/* 表格小屏字号调整 */
|
||||
.el-table td.el-table__cell {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
/* 表单小屏行距压缩 */
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* tk-resource-grid 在移动端变为单列 */
|
||||
.tk-resource-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕适配 */
|
||||
@media (max-width: 1200px) {
|
||||
.el-table .el-table__body-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 992px) {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Dynamic Unit System
|
||||
*
|
||||
* Handles dynamic unit conversion and display for product parameters.
|
||||
* Base units: storage=GB, bandwidth=Mbps, cpu=Core
|
||||
*/
|
||||
|
||||
const UNIT_CONVERSIONS = {
|
||||
cpu: { Core: 1 },
|
||||
bandwidth_up: { Mbps: 1, Gbps: 1000 },
|
||||
bandwidth_down: { Mbps: 1, Gbps: 1000 },
|
||||
storage: { GB: 1, TB: 1024 },
|
||||
ipv4: { '个': 1 },
|
||||
ipv6: { '个': 1 },
|
||||
custom: {}
|
||||
}
|
||||
|
||||
const BASE_UNITS = {
|
||||
cpu: 'Core',
|
||||
bandwidth_up: 'Mbps',
|
||||
bandwidth_down: 'Mbps',
|
||||
storage: 'GB',
|
||||
ipv4: '个',
|
||||
ipv6: '个',
|
||||
custom: ''
|
||||
}
|
||||
|
||||
const DEFAULT_DISPLAY_UNITS = {
|
||||
cpu: 'Core',
|
||||
bandwidth_up: 'Mbps',
|
||||
bandwidth_down: 'Mbps',
|
||||
storage: 'GB',
|
||||
ipv4: '个',
|
||||
ipv6: '个',
|
||||
custom: ''
|
||||
}
|
||||
|
||||
const ARG_KEY_OPTIONS = [
|
||||
{ label: 'CPU (cpu)', value: 'cpu' },
|
||||
{ label: 'IPv4', value: 'ipv4' },
|
||||
{ label: 'IPv6', value: 'ipv6' },
|
||||
{ label: '上行带宽 (bandwidth_up)', value: 'bandwidth_up' },
|
||||
{ label: '下行带宽 (bandwidth_down)', value: 'bandwidth_down' },
|
||||
{ label: '存储空间 (storage)', value: 'storage' },
|
||||
{ label: '自定义 (custom)', value: 'custom' }
|
||||
]
|
||||
|
||||
/**
|
||||
* Convert value between units
|
||||
* @param {number} value
|
||||
* @param {string} fromUnit
|
||||
* @param {string} toUnit
|
||||
* @param {string} argKey - e.g. 'storage', 'bandwidth_up'
|
||||
*/
|
||||
export function convertUnit(value, fromUnit, toUnit, argKey) {
|
||||
if (value === null || value === undefined || fromUnit === toUnit) return value
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
if (!conversions || !conversions[fromUnit] || !conversions[toUnit]) return value
|
||||
const baseValue = value * conversions[fromUnit]
|
||||
return baseValue / conversions[toUnit]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from display unit to base unit for storage/submission
|
||||
*/
|
||||
export function toBaseUnit(value, displayUnit, argKey) {
|
||||
const baseUnit = BASE_UNITS[argKey]
|
||||
if (!baseUnit || !displayUnit) return value
|
||||
return convertUnit(value, displayUnit, baseUnit, argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from base unit to display unit for showing in UI
|
||||
*/
|
||||
export function fromBaseUnit(value, displayUnit, argKey) {
|
||||
const baseUnit = BASE_UNITS[argKey]
|
||||
if (!baseUnit || !displayUnit) return value
|
||||
return convertUnit(value, baseUnit, displayUnit, argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base unit string for a given argKey
|
||||
*/
|
||||
export function getBaseUnit(argKey) {
|
||||
return BASE_UNITS[argKey] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default display unit for a given argKey
|
||||
*/
|
||||
export function getDefaultDisplayUnit(argKey) {
|
||||
return DEFAULT_DISPLAY_UNITS[argKey] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available units for a parameter type
|
||||
*/
|
||||
export function getAvailableUnits(argKey) {
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
return conversions ? Object.keys(conversions) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get argKey select options
|
||||
*/
|
||||
export function getArgKeyOptions() {
|
||||
return ARG_KEY_OPTIONS
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a parameter has dynamic unit enabled.
|
||||
* Returns true when arg_key maps to a known unit type with multiple selectable units.
|
||||
*/
|
||||
export function hasUnit(param) {
|
||||
if (!param) return false
|
||||
const argKey = param.argKey || param.arg_key || param.key || ''
|
||||
if (!argKey || !(argKey in UNIT_CONVERSIONS)) return false
|
||||
return Object.keys(UNIT_CONVERSIONS[argKey]).length > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argKey from a parameter object (handles camelCase, snake_case, and plain key)
|
||||
*/
|
||||
export function getArgKey(param) {
|
||||
if (!param) return ''
|
||||
return param.argKey || param.arg_key || param.key || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available units from a parameter object
|
||||
*/
|
||||
export function getParamUnits(param) {
|
||||
if (!hasUnit(param)) return []
|
||||
const argKey = getArgKey(param)
|
||||
const paramUnits = param.availableUnits || param.available_units
|
||||
if (paramUnits && paramUnits.length > 0) return paramUnits
|
||||
return getAvailableUnits(argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default unit from a parameter object
|
||||
*/
|
||||
export function getParamDefaultUnit(param) {
|
||||
if (!hasUnit(param)) return ''
|
||||
const argKey = getArgKey(param)
|
||||
return param.defaultUnit || param.default_unit || getDefaultDisplayUnit(argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a unit is valid for a parameter type
|
||||
*/
|
||||
export function isValidUnit(unit, argKey) {
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
return conversions && Object.prototype.hasOwnProperty.call(conversions, unit)
|
||||
}
|
||||
|
||||
export function formatValueWithUnit(value, unit) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return unit ? `${value} ${unit}` : String(value)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
const ERROR_CODE_MAP = {
|
||||
// 主控服务
|
||||
kvm_service_list_error: '获取主控服务列表失败',
|
||||
kvm_service_detail_error: '获取主控服务详情失败',
|
||||
kvm_service_create_error: '创建主控服务失败',
|
||||
kvm_service_update_error: '修改主控服务失败',
|
||||
kvm_service_delete_error: '删除主控服务失败',
|
||||
|
||||
// 宿主机组(本地)
|
||||
kvm_host_group_list_error: '获取宿主机组列表失败',
|
||||
kvm_host_group_sync_error: '同步宿主机组失败',
|
||||
kvm_host_group_bind_error: '绑定宿主机组失败',
|
||||
kvm_host_group_update_error: '修改宿主机组失败',
|
||||
kvm_host_group_delete_error: '删除宿主机组失败',
|
||||
kvm_host_group_generate_error: '生成商品失败',
|
||||
kvm_host_group_optimal_error: '获取最优主机失败',
|
||||
|
||||
// 宿主机组(远程)
|
||||
kvm_remote_host_group_list_error: '获取远程宿主机组列表失败',
|
||||
kvm_remote_host_group_detail_error: '获取远程宿主机组详情失败',
|
||||
kvm_remote_host_group_tree_error: '获取远程宿主机组树失败',
|
||||
kvm_remote_host_group_create_error: '创建远程宿主机组失败',
|
||||
kvm_remote_host_group_update_error: '修改远程宿主机组失败',
|
||||
kvm_remote_host_group_delete_error: '删除远程宿主机组失败',
|
||||
|
||||
// 宿主机
|
||||
kvm_host_list_error: '获取宿主机列表失败',
|
||||
kvm_host_detail_error: '获取宿主机详情失败',
|
||||
kvm_host_metrics_error: '获取宿主机指标失败',
|
||||
kvm_host_add_error: '新增宿主机失败',
|
||||
kvm_host_update_error: '修改宿主机失败',
|
||||
kvm_host_delete_error: '删除宿主机失败',
|
||||
|
||||
// 镜像
|
||||
kvm_image_list_error: '获取镜像列表失败',
|
||||
kvm_image_detail_error: '获取镜像详情失败',
|
||||
kvm_image_host_status_error: '获取镜像宿主机状态失败',
|
||||
kvm_image_create_error: '创建镜像失败',
|
||||
kvm_image_update_error: '修改镜像失败',
|
||||
kvm_image_delete_error: '删除镜像失败',
|
||||
kvm_image_reload_error: '重新下载镜像失败',
|
||||
kvm_image_sync_error: '同步镜像到宿主机失败',
|
||||
kvm_image_reload_host_error: '重新下载镜像到宿主机失败',
|
||||
|
||||
// 网络
|
||||
kvm_network_list_error: '获取网络列表失败',
|
||||
kvm_network_detail_error: '获取网络详情失败',
|
||||
kvm_network_create_error: '创建网络失败',
|
||||
kvm_network_update_error: '修改网络失败',
|
||||
kvm_network_delete_error: '删除网络失败',
|
||||
|
||||
// 数据卷
|
||||
kvm_volume_list_error: '获取数据卷列表失败',
|
||||
kvm_volume_detail_error: '获取数据卷详情失败',
|
||||
kvm_volume_create_error: '创建数据卷失败',
|
||||
kvm_volume_resize_error: '调整数据卷大小失败',
|
||||
kvm_volume_mount_error: '挂载数据卷失败',
|
||||
kvm_volume_unmount_error: '卸载数据卷失败',
|
||||
kvm_volume_transfer_error: '迁移数据卷失败',
|
||||
kvm_volume_delete_error: '删除数据卷失败',
|
||||
|
||||
// 虚拟机
|
||||
kvm_vm_list_error: '获取虚拟机列表失败',
|
||||
kvm_vm_detail_error: '获取虚拟机详情失败',
|
||||
kvm_vm_status_error: '获取虚拟机状态失败',
|
||||
kvm_vm_metrics_error: '获取虚拟机指标失败',
|
||||
kvm_vm_create_error: '创建虚拟机失败',
|
||||
kvm_vm_update_error: '修改虚拟机失败',
|
||||
kvm_vm_rebuild_error: '重建虚拟机失败',
|
||||
kvm_vm_refactor_error: '重构虚拟机失败',
|
||||
kvm_vm_update_traffic_error: '修改虚拟机带宽失败',
|
||||
kvm_vm_start_error: '启动虚拟机失败',
|
||||
kvm_vm_stop_error: '停止虚拟机失败',
|
||||
kvm_vm_reboot_error: '重启虚拟机失败',
|
||||
kvm_vm_suspend_error: '暂停虚拟机失败',
|
||||
kvm_vm_resume_error: '恢复虚拟机失败',
|
||||
kvm_vm_rescue_error: '进入救援系统失败',
|
||||
kvm_vm_exit_rescue_error: '退出救援系统失败',
|
||||
kvm_vm_delete_error: '删除虚拟机失败',
|
||||
|
||||
// 安全组
|
||||
kvm_post_group_list_error: '获取安全组列表失败',
|
||||
kvm_post_group_detail_error: '获取安全组详情失败',
|
||||
kvm_post_group_create_error: '创建安全组失败',
|
||||
kvm_post_group_update_error: '修改安全组失败',
|
||||
kvm_post_group_sync_error: '同步安全组失败',
|
||||
kvm_post_group_bind_error: '绑定安全组失败',
|
||||
kvm_post_group_unbind_error: '解绑安全组失败',
|
||||
kvm_post_group_delete_error: '删除安全组失败',
|
||||
kvm_post_group_enable_whitelist_error: '开启安全组白名单失败',
|
||||
kvm_post_group_disable_whitelist_error: '关闭安全组白名单失败',
|
||||
kvm_post_group_create_rule_error: '新增安全组规则失败',
|
||||
kvm_post_group_update_rule_error: '修改安全组规则失败',
|
||||
kvm_post_group_delete_rule_error: '删除安全组规则失败',
|
||||
kvm_post_group_apply_error: '应用安全组失败',
|
||||
kvm_security_group_list_error: '获取安全组列表失败',
|
||||
kvm_security_group_detail_error: '获取安全组详情失败',
|
||||
kvm_security_group_create_error: '创建安全组失败',
|
||||
kvm_security_group_update_error: '修改安全组失败',
|
||||
kvm_security_group_delete_error: '删除安全组失败',
|
||||
|
||||
// VNC
|
||||
kvm_vnc_list_error: '获取VNC节点列表失败',
|
||||
kvm_vnc_add_error: '新增VNC节点失败',
|
||||
kvm_vnc_test_error: '测试VNC节点连接失败',
|
||||
kvm_vnc_update_error: '修改VNC节点失败',
|
||||
kvm_vnc_delete_error: '删除VNC节点失败',
|
||||
kvm_vnc_vm_vnc_error: '获取VNC连接信息失败',
|
||||
}
|
||||
|
||||
/**
|
||||
* 从嵌套的 RPC 错误字符串中提取有意义的中文描述
|
||||
*/
|
||||
function parseRpcError(err) {
|
||||
if (!err) return ''
|
||||
const descMatch = err.match(/desc\s*=\s*(.+)/)
|
||||
if (descMatch) {
|
||||
const descContent = descMatch[1]
|
||||
const jsonMatch = descContent.match(/body=(\{.+\})/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[1])
|
||||
if (parsed.message) return parsed.message
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const clean = descContent.trim()
|
||||
if (clean && !clean.startsWith('http')) return clean
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一提取 API 响应中的错误信息
|
||||
* @param {object} body - axios response.data (即 { code, message, error, data })
|
||||
* @param {string} fallback - 兜底文案
|
||||
* @returns {string} 中文错误描述
|
||||
*/
|
||||
export function extractApiError(body, fallback = '操作失败') {
|
||||
if (!body) return fallback
|
||||
|
||||
// 识别数据库唯一约束冲突
|
||||
if (body.error && body.error.includes('duplicate key value violates unique constraint')) {
|
||||
const nameMatch = body.error.match(/create \w+ \[(.+?)\] error/)
|
||||
const hint = nameMatch ? `「${nameMatch[1]}」已存在,请勿重复生成` : '数据已存在,请勿重复操作'
|
||||
return hint
|
||||
}
|
||||
|
||||
const rpcMsg = parseRpcError(body.error)
|
||||
if (rpcMsg) return rpcMsg
|
||||
|
||||
const mapped = ERROR_CODE_MAP[body.message]
|
||||
if (mapped) return mapped
|
||||
|
||||
if (body.message && !/^[a-z_]+$/.test(body.message)) return body.message
|
||||
|
||||
return fallback
|
||||
}
|
||||
+151
-29
@@ -1,26 +1,104 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
|
||||
// 基础URL
|
||||
const baseUrl = 'https://apiservertest.s1f.ren'
|
||||
// const baseUrl = 'https://cloudapi.007yjs.com'
|
||||
import {getRefreshToken,refreshAccessToken} from "@/api/login.js";
|
||||
import { baseUrl, acsBaseUrl, noAuthUrls as noAuthUrlList, requestTimeout, acsRequestTimeout, TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js'
|
||||
|
||||
// 检查URL是否需要认证
|
||||
const urlNeedAuth = (url) => {
|
||||
// 这里可以添加不需要认证的URL列表
|
||||
const noAuthUrls = ['/v1/user/login', '/v1/user/check/get_code_img', '/v1/user/register']
|
||||
return !noAuthUrls.some(noAuthUrl => url.includes(noAuthUrl))
|
||||
return !noAuthUrlList.some(noAuthUrl => url.includes(noAuthUrl))
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
const isTokenExpired = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
|
||||
if (!token) return true
|
||||
|
||||
// 这里可以添加token过期检查逻辑,如果有JWT可以解析它
|
||||
// 简单实现,仅检查token是否存在
|
||||
return false
|
||||
// 检查过期时间
|
||||
if (expire) {
|
||||
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
|
||||
const now = Date.now()
|
||||
return now >= expireTime
|
||||
}
|
||||
|
||||
// 没有过期时间时,默认认为Token已过期(因为无法验证有效性)
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查token是否即将过期(5分钟内)
|
||||
const isTokenExpiringSoon = () => {
|
||||
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
|
||||
if (!expire) return false
|
||||
|
||||
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
|
||||
const now = Date.now()
|
||||
const fiveMinutes = 5 * 60 * 1000 // 5分钟
|
||||
|
||||
// 如果已过期,返回false(由isTokenExpired处理)
|
||||
if (now >= expireTime) return false
|
||||
|
||||
// 如果在5分钟内过期,返回true
|
||||
return (expireTime - now) <= fiveMinutes
|
||||
}
|
||||
|
||||
// 正在刷新token的标志
|
||||
let isRefreshing = false
|
||||
// 等待刷新token的请求队列
|
||||
let refreshSubscribers = []
|
||||
|
||||
// 添加请求到队列
|
||||
const subscribeTokenRefresh = (callback) => {
|
||||
refreshSubscribers.push(callback)
|
||||
}
|
||||
|
||||
// 刷新token后执行队列中的请求
|
||||
const onTokenRefreshed = (newToken) => {
|
||||
refreshSubscribers.forEach(callback => callback(newToken))
|
||||
refreshSubscribers = []
|
||||
}
|
||||
|
||||
// 执行token刷新
|
||||
const doRefreshToken = async () => {
|
||||
try {
|
||||
const domain = window.location.hostname
|
||||
// 获取交换token
|
||||
const refreshTokenRes = await getRefreshToken(domain,{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (refreshTokenRes.data?.code === 200 && refreshTokenRes.data?.data?.refresh_token) {
|
||||
// 使用交换token获取新的access token
|
||||
const newTokenRes = await refreshAccessToken(refreshTokenRes.data.data.refresh_token)
|
||||
|
||||
if (newTokenRes.data?.code === 200 && newTokenRes.data?.data?.token) {
|
||||
const { token, expire } = newTokenRes.data.data
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
if (expire) {
|
||||
localStorage.setItem(TOKEN_EXPIRE_KEY, expire.toString())
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Token刷新失败:', error)
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
@@ -37,7 +115,7 @@ class Request {
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么
|
||||
// 例如:添加 token
|
||||
const token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
@@ -93,8 +171,8 @@ class Request {
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
delete(url,data={}, config = {}) {
|
||||
return this.instance.delete(url,data, config)
|
||||
delete(url, config = {}) {
|
||||
return this.instance.delete(url, config)
|
||||
}
|
||||
|
||||
// PATCH 请求
|
||||
@@ -106,7 +184,7 @@ class Request {
|
||||
// 创建默认实例
|
||||
const request = new Request({
|
||||
baseURL: baseUrl,
|
||||
timeout: 50000,
|
||||
timeout: requestTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -117,23 +195,67 @@ export const baseURL = baseUrl
|
||||
|
||||
export const http2 = axios.create({
|
||||
baseURL: baseUrl,
|
||||
timeout: 30000,
|
||||
timeout: acsRequestTimeout,
|
||||
headers: {},
|
||||
});
|
||||
|
||||
http2.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage
|
||||
if(urlNeedAuth(config.url) && isTokenExpired()){
|
||||
if (token){
|
||||
localStorage.removeItem('token');
|
||||
ElMessage.warning('登陆过期,请重新登陆')
|
||||
}
|
||||
router.push('/login')
|
||||
return Promise.reject();
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
http2.interceptors.request.use(async config => {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
|
||||
// 检查是否需要认证
|
||||
if (urlNeedAuth(config.url)) {
|
||||
// 检查token是否已过期
|
||||
if (isTokenExpired()) {
|
||||
if (token) {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
}
|
||||
router.push('/login')
|
||||
return Promise.reject(new Error('Token已过期'))
|
||||
}
|
||||
|
||||
// 检查token是否即将过期,进行无感刷新
|
||||
if (isTokenExpiringSoon() && !isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newToken = await doRefreshToken()
|
||||
if (newToken) {
|
||||
console.log('Token已无感刷新')
|
||||
onTokenRefreshed(newToken)
|
||||
config.headers.Authorization = `Bearer ${newToken}`
|
||||
} else {
|
||||
// 刷新失败,doRefreshToken已处理登出逻辑,直接拒绝请求
|
||||
return Promise.reject(new Error('Token刷新失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token刷新异常:', error)
|
||||
// 刷新异常,doRefreshToken已处理登出逻辑,直接拒绝请求
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
} else if (isRefreshing) {
|
||||
// 正在刷新,等待刷新完成
|
||||
return new Promise((resolve, reject) => {
|
||||
subscribeTokenRefresh((newToken) => {
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`
|
||||
// 重新发送原始请求
|
||||
resolve(config)
|
||||
} else {
|
||||
reject(new Error('Token刷新失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 正常情况,直接使用token
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
// 不需要认证的请求,不添加token
|
||||
|
||||
config.url = config.url
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -147,7 +269,7 @@ http2.interceptors.response.use(
|
||||
}
|
||||
const { status } = error.response;
|
||||
if (status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
ElMessage.warning('登陆过期,请重新登陆')
|
||||
router.push('/login')
|
||||
return Promise.reject();
|
||||
|
||||
+157
-2
@@ -18,7 +18,7 @@ export const formatDate = (dateStr) => {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
/**
|
||||
* 时间格式转 Unix 时间戳(毫秒级)
|
||||
* 时间格式转 Unix 时间戳(秒级)
|
||||
* @param {string|Date} time - 输入时间(支持 '2025-10-28 00:00:00'、'2025/10/28'、Date 对象等)
|
||||
* @returns {number|null} 转换后的毫秒级时间戳(失败返回 null)
|
||||
*/
|
||||
@@ -50,5 +50,160 @@ export function timeToTimestamp(time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000)
|
||||
return Math.floor(timestamp / 1000); // 返回秒级时间戳(如 1751107200000)
|
||||
}
|
||||
|
||||
|
||||
export function reducenum(num){
|
||||
return num / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 分转元显示(返回 ¥xx.xx 或 '-')
|
||||
*/
|
||||
export function formatPrice(fen, fallback = '-') {
|
||||
if (!fen && fen !== 0) return fallback
|
||||
return '¥' + (fen / 100).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 元转分(四舍五入取整)
|
||||
*/
|
||||
export function yuanToFen(yuan) {
|
||||
return Math.round((yuan || 0) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化到期时间(year < 2000 视为永久)
|
||||
*/
|
||||
export function formatExpireTime(t) {
|
||||
if (!t) return '-'
|
||||
const d = new Date(t)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
if (d.getFullYear() < 2000) return '永久'
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ISO 格式时间字符串转换为毫秒级时间戳(用于时间选择器)
|
||||
* @param {string|Date|number} time - 输入时间(支持 ISO 格式字符串如 '2023-11-08T01:10:00+08:00'、Date 对象、时间戳等)
|
||||
* @returns {number|null} 转换后的毫秒级时间戳(失败或无效时间返回 null)
|
||||
*/
|
||||
export function isoToMilliseconds(time) {
|
||||
// 处理空值
|
||||
if (!time || time === null || time === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 处理特殊的无效时间标识
|
||||
if (typeof time === 'string' && (time === '0001-01-01T00:00:00Z' || time === '0001-01-01T00:00:00+00:00')) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果已经是数字(时间戳),直接返回
|
||||
if (typeof time === 'number') {
|
||||
// 如果是秒级时间戳(小于 13 位),转换为毫秒
|
||||
if (time < 1000000000000) {
|
||||
return time * 1000
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
// 处理 Date 对象
|
||||
if (time instanceof Date) {
|
||||
const timestamp = time.getTime()
|
||||
return isNaN(timestamp) ? null : timestamp
|
||||
}
|
||||
|
||||
// 处理字符串格式
|
||||
if (typeof time === 'string') {
|
||||
try {
|
||||
const date = new Date(time)
|
||||
const timestamp = date.getTime()
|
||||
|
||||
// 检查是否为有效时间
|
||||
if (isNaN(timestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return timestamp
|
||||
} catch (error) {
|
||||
console.error('时间转换失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为 "YYYY-MM-DD HH:mm:ss" 格式(用于接口提交)
|
||||
* @param {string|Date|number} time
|
||||
* @returns {string} 格式化后的时间字符串,无效时返回 ''
|
||||
*/
|
||||
export function formatToApiTime(time) {
|
||||
if (!time) return ''
|
||||
const d = time instanceof Date ? time : new Date(time)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// ========== 虚拟机状态映射 ==========
|
||||
const VM_STATUS_MAP = {
|
||||
pending: { label: '等待中', type: 'info' },
|
||||
creating: { label: '创建中', type: 'warning' },
|
||||
ready: { label: '就绪', type: 'success' },
|
||||
running: { label: '运行中', type: 'success' },
|
||||
stopped: { label: '已停止', type: 'danger' },
|
||||
stop: { label: '已停止', type: 'danger' },
|
||||
shutoff: { label: '已关闭', type: 'danger' },
|
||||
error: { label: '错误', type: 'danger' },
|
||||
paused: { label: '已暂停', type: 'warning' },
|
||||
reboot: { label: '重启中', type: 'warning' },
|
||||
poweroff: { label: '已关机', type: 'info' },
|
||||
unknown: { label: '未知', type: 'info' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟机状态标签文字
|
||||
*/
|
||||
export function vmStatusLabel(status) {
|
||||
return VM_STATUS_MAP[status]?.label || status || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟机状态 Tag 类型
|
||||
*/
|
||||
export function vmStatusType(status) {
|
||||
return VM_STATUS_MAP[status]?.type || 'info'
|
||||
}
|
||||
|
||||
// ========== 磁盘状态映射 ==========
|
||||
const VOLUME_STATUS_MAP = {
|
||||
pending: { label: '等待中', type: 'info' },
|
||||
creating: { label: '创建中', type: 'warning' },
|
||||
ready: { label: '就绪', type: 'success' },
|
||||
in_use: { label: '使用中', type: 'success' },
|
||||
attaching: { label: '挂载中', type: 'warning' },
|
||||
detaching: { label: '卸载中', type: 'warning' },
|
||||
resizing: { label: '扩容中', type: 'warning' },
|
||||
deleting: { label: '删除中', type: 'danger' },
|
||||
error: { label: '错误', type: 'danger' },
|
||||
unknown: { label: '未知', type: 'info' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘状态标签文字
|
||||
*/
|
||||
export function volumeStatusLabel(status) {
|
||||
return VOLUME_STATUS_MAP[status]?.label || status || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘状态 Tag 类型
|
||||
*/
|
||||
export function volumeStatusType(status) {
|
||||
return VOLUME_STATUS_MAP[status]?.type || 'info'
|
||||
}
|
||||
|
||||
+9
-1
@@ -105,16 +105,24 @@ const forgetPassword = () => {
|
||||
const handleLogin = () => {
|
||||
loginFormRef.value?.validate(async valid =>{
|
||||
window.localStorage.removeItem('token')
|
||||
window.localStorage.removeItem('tokenExpire')
|
||||
window.localStorage.removeItem('userInfo')
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
let resp = await userLogin(loginForm.username, loginForm.password)
|
||||
console.log("login:",resp)
|
||||
loading.value = false
|
||||
if(resp.code === 200){
|
||||
// 保存token和过期时间
|
||||
window.localStorage.setItem('token', resp.data.token)
|
||||
if (resp.data.expire) {
|
||||
window.localStorage.setItem('tokenExpire', resp.data.expire.toString())
|
||||
}
|
||||
|
||||
window.localStorage.setItem('token',resp.data.token)
|
||||
let userInfo = await getUserInfo()
|
||||
if(userInfo.data.is_admin){
|
||||
// 保存用户信息到localStorage
|
||||
window.localStorage.setItem('userInfo', JSON.stringify(userInfo.data))
|
||||
await router.push('/dashboard')
|
||||
} else {
|
||||
ElMessage.warning('你不是管理员,不能登陆到后台控制面板')
|
||||
|
||||
@@ -666,7 +666,7 @@ const toLoad = async (data) => {
|
||||
})
|
||||
form.server_id = data
|
||||
nowserver_id.value = data
|
||||
let res = await getServerPlan({server_id:data,count:100})
|
||||
let res = await getServerPlan({server_id:data,count:10})
|
||||
planlist.value = res.data.data.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
@@ -748,7 +748,7 @@ const fetchCategoryList = async (serverId) => {
|
||||
// 编辑镜像
|
||||
const handleEdit = async (data) => {
|
||||
try {
|
||||
let res = await getServerPlan({server_id: data.server_id,count: 100})
|
||||
let res = await getServerPlan({server_id: data.server_id,count: 10})
|
||||
if (res.data && res.data.data) {
|
||||
planlist.value = res.data.data.map(item => {
|
||||
return {
|
||||
@@ -874,7 +874,7 @@ const getit = async () => {
|
||||
|
||||
// 选择图片
|
||||
const picPagin = reactive({
|
||||
count: 50,
|
||||
count: 10,
|
||||
page: 1,
|
||||
key: '',
|
||||
user_type: 1
|
||||
|
||||
@@ -262,7 +262,7 @@ const categoryRules = {
|
||||
// 素材库相关
|
||||
const picSwitch = ref(false)
|
||||
const picPagin = reactive({
|
||||
count: 50,
|
||||
count: 10,
|
||||
page: 1,
|
||||
key: '',
|
||||
user_type: 1
|
||||
|
||||
@@ -244,7 +244,7 @@ const showNewCategoryInput = ref(false)
|
||||
const picSwitch = ref(false)
|
||||
const picLoading = ref(false)
|
||||
const picPagin = reactive({
|
||||
count: 20,
|
||||
count: 10,
|
||||
page: 1,
|
||||
key: '',
|
||||
user_type: 1
|
||||
@@ -314,7 +314,7 @@ const initData = async () => {
|
||||
// Fallback: fetch list and find item
|
||||
const listRes = await getUserMirrorList({
|
||||
server_id: serverId.value,
|
||||
count: 100,
|
||||
count: 10,
|
||||
page: 1
|
||||
})
|
||||
if (listRes.data.code === 200) {
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
<h3 class="tab-title">数据卷列表</h3>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showAddVolumeDialog = true"
|
||||
@click="handleAddVolume"
|
||||
:icon="Plus"
|
||||
:disabled="vmInfo.state != 2"
|
||||
>
|
||||
@@ -671,8 +671,11 @@
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||||
<el-form-item label="大小(GB)" prop="size">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
|
||||
<el-form-item label="大小" prop="size">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -693,8 +696,11 @@
|
||||
>
|
||||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||||
|
||||
<el-form-item label="大小(GB)" prop="size">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
|
||||
<el-form-item label="大小" prop="size">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -1067,6 +1073,7 @@ const showMigrateVolumeDialog = ref(false);
|
||||
const currentVolumeToEdit = ref(null);
|
||||
const volumeForm = reactive({
|
||||
size: 10,
|
||||
_sizeUnit: 'GB'
|
||||
});
|
||||
const volumeFormRef = ref(null);
|
||||
const volumeRules = {
|
||||
@@ -2371,6 +2378,7 @@ const handleAddVolume = () => {
|
||||
showAddVolumeDialog.value = true;
|
||||
// 重置表单
|
||||
volumeForm.size = 10;
|
||||
volumeForm._sizeUnit = 'GB';
|
||||
};
|
||||
|
||||
// 编辑数据卷
|
||||
@@ -2378,6 +2386,7 @@ const handleEditVolume = (volume) => {
|
||||
currentVolumeToEdit.value = volume;
|
||||
// 填充表单
|
||||
volumeForm.size = volume.size;
|
||||
volumeForm._sizeUnit = 'GB';
|
||||
showEditVolumeDialog.value = true;
|
||||
};
|
||||
|
||||
@@ -2404,9 +2413,10 @@ const submitAddVolume = async () => {
|
||||
if (valid) {
|
||||
addingVolume.value = true;
|
||||
try {
|
||||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||||
const res = await addVolume({
|
||||
instance_id: route.query.instance_id,
|
||||
size: String(volumeForm.size),
|
||||
size: String(sizeGb),
|
||||
user_id: user_id.value
|
||||
});
|
||||
console.log("添加数据卷112",res)
|
||||
@@ -2438,9 +2448,10 @@ const submitEditVolume = async () => {
|
||||
editingVolume.value = true;
|
||||
try {
|
||||
// 这里应该调用修改数据卷的API
|
||||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||||
const res = await updateVolume({
|
||||
volume_id: currentVolumeToEdit.value.id,
|
||||
size: volumeForm.size
|
||||
size: sizeGb
|
||||
});
|
||||
console.log("编辑数据卷数据:",res)
|
||||
|
||||
@@ -2770,4 +2781,7 @@ const fetchServersList = async () => {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-select { width: 90px; flex-shrink: 0; }
|
||||
</style>
|
||||
|
||||
@@ -618,7 +618,7 @@ const fetchPlanList = async () => {
|
||||
try {
|
||||
const response = await getServerPlan({
|
||||
server_id: props.ID,
|
||||
count: 100
|
||||
count: 10
|
||||
});
|
||||
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
|
||||
@@ -315,7 +315,11 @@
|
||||
class="data-table"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="size" label="空间大小(MB)" width="140" />
|
||||
<el-table-column prop="size" label="空间大小(MB)" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.size != null && row.size !== '' ? `${row.size} MB` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mount_path" label="挂载路径" min-width="200" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
</el-table>
|
||||
|
||||
@@ -1901,7 +1901,7 @@ const GetSpecs = async () => {
|
||||
try {
|
||||
let plans = await getServerPlan({
|
||||
server_id: route.query.server_id,
|
||||
count: 30
|
||||
count: 10
|
||||
});
|
||||
spec_list.value = plans.data.data;
|
||||
} catch (error) {
|
||||
@@ -2407,7 +2407,7 @@ const fetchContainerPlanList = async () => {
|
||||
try {
|
||||
const response = await getServerPlan({
|
||||
server_id: route.query.server_id,
|
||||
count: 100
|
||||
count: 10
|
||||
});
|
||||
console.log("获取容器套餐列表1111:",response);
|
||||
|
||||
@@ -2430,7 +2430,7 @@ const fetchContainerMirrorList = async () => {
|
||||
|
||||
containerMirrorLoading.value = true;
|
||||
try {
|
||||
const response = await getMirrorList({server_id: route.query.server_id, page: 1, count: 999,key: '',class_id: ''});
|
||||
const response = await getMirrorList({server_id: route.query.server_id, page: 1, count: 10,key: '',class_id: ''});
|
||||
console.log("获取镜像列表1111:",response);
|
||||
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
<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 label="用户ID" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.userId" type="primary" :underline="false" @click="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 { useRouter } from 'vue-router'
|
||||
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 router = useRouter()
|
||||
|
||||
// 数据状态
|
||||
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: 10, 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>
|
||||
@@ -0,0 +1,921 @@
|
||||
<template>
|
||||
<div class="group-buy-manage-container">
|
||||
<el-card class="main-container" shadow="never">
|
||||
<el-tabs v-model="activeTab" class="group-buy-tabs">
|
||||
<!-- 拼团活动标签页 -->
|
||||
<el-tab-pane label="拼团活动" name="activity">
|
||||
<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="activityLoading">
|
||||
刷新列表
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleClearAll">
|
||||
清除所有队伍
|
||||
</el-button>
|
||||
<el-button type="warning" @click="showClearUserDialog = true">
|
||||
清除用户队伍
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<el-table :data="groupList" v-loading="activityLoading" 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>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 拼团类型标签页 -->
|
||||
<el-tab-pane label="拼团类型" name="type">
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" icon="Plus" @click="handleAddType">新增类型</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="fetchTypeList" />
|
||||
<el-button type="info" icon="Refresh" @click="fetchTypeList" :loading="typeLoading" :disabled="!searchTag" style="margin-left: 12px">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<el-empty v-if="!searchTag" description="请先选择标签" />
|
||||
<template v-else>
|
||||
<el-table :data="typeTableData" v-loading="typeLoading" 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="handleEditType(row)">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDeleteType(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination v-model:current-page="typePage" v-model:page-size="typePageSize" :total="typeTotal" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="fetchTypeList" @current-change="fetchTypeList" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</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="handleCreateTagChange">
|
||||
<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 createTypeList" :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 label="用户ID" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.userId" type="primary" :underline="false" @click="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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>
|
||||
|
||||
<!-- 拼团类型表单对话框 -->
|
||||
<el-dialog v-model="typeDialogVisible" :title="isEditType ? '编辑拼团类型' : '新增拼团类型'" width="500px" :close-on-click-modal="false">
|
||||
<el-form :model="typeForm" :rules="typeRules" ref="typeFormRef" label-width="100px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="typeForm.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格" prop="price">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="typeForm.price" :min="0" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="续费价格" prop="renewPrice">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="typeForm.renewPrice" :min="0" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="拼团人数" prop="maxPerson">
|
||||
<el-input-number v-model="typeForm.maxPerson" :min="2" :max="100" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标签" prop="tag">
|
||||
<el-select v-model="typeForm.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="typeForm.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="typeForm.noteFields" border size="small" v-if="typeForm.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="typeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleTypeSubmit" :loading="typeSubmitLoading">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getGroupBuyList,
|
||||
getGroupBuyDetail,
|
||||
addRandomUser,
|
||||
addRandomGroup,
|
||||
exportIdcInfo,
|
||||
setOrder
|
||||
} from '@/api/admin/activity'
|
||||
import {
|
||||
getGroupBuyTypeList,
|
||||
getGroupBuyTypeTags,
|
||||
removeGroupBuy,
|
||||
clearAllGroupBuy,
|
||||
clearUserGroupBuy,
|
||||
addGroupBuyType,
|
||||
updateGroupBuyType,
|
||||
deleteGroupBuyType
|
||||
} from '@/api/groupBuy'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('activity')
|
||||
|
||||
// ==================== 拼团活动相关 ====================
|
||||
const activityLoading = 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 createTypeList = 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 typeLoading = ref(false)
|
||||
const typeTableData = ref([])
|
||||
const typeTotal = ref(0)
|
||||
const typePage = ref(1)
|
||||
const typePageSize = ref(10)
|
||||
const searchKey = ref('')
|
||||
const searchTag = ref('')
|
||||
const typeDialogVisible = ref(false)
|
||||
const isEditType = ref(false)
|
||||
const typeSubmitLoading = ref(false)
|
||||
const typeFormRef = ref(null)
|
||||
|
||||
const typeForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
price: 0,
|
||||
renewPrice: 0,
|
||||
maxPerson: 5,
|
||||
tag: '',
|
||||
expireTime: null,
|
||||
noteFields: []
|
||||
})
|
||||
|
||||
const typeRules = {
|
||||
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 formatTime = (timeStr) => {
|
||||
if (!timeStr) return '-'
|
||||
return new Date(timeStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取标签列表
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const res = await getGroupBuyTypeTags()
|
||||
if (res.code === 200) {
|
||||
tagList.value = res.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 拼团活动方法 ====================
|
||||
// 根据 tag 获取拼团类型列表(用于创建对话框)
|
||||
const fetchCreateTypeListByTag = async (tag) => {
|
||||
try {
|
||||
const res = await getGroupBuyTypeList({ page: 1, count: 10, tag })
|
||||
if (res.code === 200) {
|
||||
createTypeList.value = res.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取拼团类型失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// tag 变化时获取对应的拼团类型(创建对话框)
|
||||
const handleCreateTagChange = (tag) => {
|
||||
createForm.groupBuyTypeId = ''
|
||||
createTypeList.value = []
|
||||
if (tag) {
|
||||
fetchCreateTypeListByTag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开创建对话框
|
||||
const openCreateDialog = () => {
|
||||
createForm.name = ''
|
||||
createForm.tag = ''
|
||||
createForm.groupBuyTypeId = ''
|
||||
createTypeList.value = []
|
||||
fetchTags()
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
// 获取队伍列表
|
||||
const fetchGroupList = async () => {
|
||||
activityLoading.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 || []
|
||||
|
||||
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 {
|
||||
activityLoading.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
|
||||
try {
|
||||
const res = await addRandomGroup({ name: createForm.name, group_buy_type_id: String(createForm.groupBuyTypeId) })
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 拼团类型方法 ====================
|
||||
const fetchTypeList = async () => {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await getGroupBuyTypeList({ page: typePage.value, count: typePageSize.value, key: searchKey.value || undefined, tag: searchTag.value || undefined })
|
||||
if (res.code === 200) {
|
||||
typeTableData.value = res.data?.data || []
|
||||
typeTotal.value = res.data?.all_count || 0
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
ElMessage.error('网络错误')
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTagChange = (tag) => {
|
||||
typePage.value = 1
|
||||
searchKey.value = ''
|
||||
typeTableData.value = []
|
||||
typeTotal.value = 0
|
||||
if (tag) {
|
||||
fetchTypeList()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddType = () => {
|
||||
isEditType.value = false
|
||||
Object.assign(typeForm, { id: '', name: '', price: 0, renewPrice: 0, maxPerson: 5, tag: '', expireTime: null, noteFields: [] })
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditType = (row) => {
|
||||
isEditType.value = true
|
||||
let noteFields = []
|
||||
try {
|
||||
noteFields = row.note ? JSON.parse(row.note) : []
|
||||
} catch { noteFields = [] }
|
||||
Object.assign(typeForm, { id: row.id, name: row.name, price: row.price, renewPrice: row.renewPrice, maxPerson: row.maxPerson, tag: row.tag || '', expireTime: row.expireTime || null, noteFields })
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const addNoteField = () => {
|
||||
typeForm.noteFields.push({ label: '', defaultValue: '' })
|
||||
}
|
||||
|
||||
const removeNoteField = (index) => {
|
||||
typeForm.noteFields.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleTypeSubmit = async () => {
|
||||
if (!typeFormRef.value) return
|
||||
await typeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
typeSubmitLoading.value = true
|
||||
try {
|
||||
const noteJson = JSON.stringify(typeForm.noteFields.filter(f => f.label))
|
||||
const data = {
|
||||
name: typeForm.name,
|
||||
price: String(typeForm.price),
|
||||
renew_price: String(typeForm.renewPrice),
|
||||
max_person: String(typeForm.maxPerson),
|
||||
tag: typeForm.tag,
|
||||
expire_time: typeForm.expireTime ? Math.floor(new Date(typeForm.expireTime).getTime() / 1000) : 0,
|
||||
note: noteJson
|
||||
}
|
||||
if (isEditType.value) data.id = String(typeForm.id)
|
||||
const res = isEditType.value ? await updateGroupBuyType(data) : await addGroupBuyType(data)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEditType.value ? '修改成功' : '新增成功')
|
||||
typeDialogVisible.value = false
|
||||
fetchTypeList()
|
||||
fetchTags()
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('网络错误')
|
||||
} finally {
|
||||
typeSubmitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteType = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该拼团类型吗?', '确认删除', { type: 'warning' })
|
||||
const res = await deleteGroupBuyType(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchTypeList()
|
||||
fetchTags()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch { /* 取消 */ }
|
||||
}
|
||||
|
||||
// 监听标签页切换
|
||||
watch(activeTab, (newVal) => {
|
||||
if (newVal === 'activity') {
|
||||
fetchGroupList()
|
||||
} else if (newVal === 'type') {
|
||||
fetchTags()
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
fetchTags()
|
||||
|
||||
// 检查路由参数决定初始标签页
|
||||
if (route.query.tab === 'type') {
|
||||
activeTab.value = 'type'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-buy-manage-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.group-buy-tabs {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
margin: 0 -20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.note-fields-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0 0 0 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<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">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="form.price" :min="0" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="续费价格" prop="renewPrice">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="form.renewPrice" :min="0" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</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%; }
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
</style>
|
||||
@@ -67,7 +67,12 @@
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column label="容器ID" width="280" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.container_id" type="primary" :underline="false" @click="router.push({ path: '/servers/container', query: { container_id: row.container_id } })">{{ row.container_id }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" label="访问地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="primary" v-if="row.url">
|
||||
@@ -146,6 +151,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||||
import {
|
||||
Refresh, Download, Search, Delete, View, Warning,
|
||||
@@ -159,6 +165,8 @@ import {
|
||||
|
||||
} from '@/utils/acs/audit'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
domain: '',
|
||||
@@ -419,7 +427,7 @@ const getFullStatsData = async () => {
|
||||
// 获取第一页大量数据来进行统计,或者调用专门的统计接口
|
||||
const statsParams = {
|
||||
page: 1,
|
||||
count: 1000, // 获取大量数据进行统计
|
||||
count: 10, // 获取大量数据进行统计
|
||||
server_id: '',
|
||||
user_id: '',
|
||||
key: queryParams.domain || ''
|
||||
|
||||
@@ -35,7 +35,12 @@
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column label="容器ID" width="280" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.container_id" type="primary" :underline="false" @click="router.push({ path: '/servers/container', query: { container_id: row.container_id } })">{{ row.container_id }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" label="违规地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="danger" v-if="row.url">
|
||||
@@ -195,6 +200,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||||
import {
|
||||
Refresh, Download, Search, Delete, View, Warning,
|
||||
@@ -208,6 +214,8 @@ import {
|
||||
|
||||
} from '@/utils/acs/audit'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
domain: '',
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<el-icon><component :is="card.icon" /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<!-- <div class="card-footer">
|
||||
<span>较昨日</span>
|
||||
<span :class="card.trend > 0 ? 'up' : 'down'">
|
||||
{{ card.trend > 0 ? '+' : '' }}{{ card.trend }}%
|
||||
@@ -23,13 +23,13 @@
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-inner" :style="{width: card.progress + '%', background: card.progressColor}"></div>
|
||||
</div>
|
||||
</div> -->
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表部分 -->
|
||||
<el-row :gutter="24" class="chart-row">
|
||||
<!-- <el-row :gutter="24" class="chart-row">
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<div class="chart-header">
|
||||
@@ -116,10 +116,137 @@
|
||||
<div class="chart-container" ref="customerChartRef"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
<!-- 数据列表区域 -->
|
||||
<el-row :gutter="24" class="list-row">
|
||||
<!-- 最近用户 -->
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card class="list-card" shadow="hover" v-loading="listLoading">
|
||||
<div class="card-header-custom">
|
||||
<div class="header-left">
|
||||
<el-icon class="header-icon user-icon"><User /></el-icon>
|
||||
<h3>最近用户</h3>
|
||||
</div>
|
||||
<el-link type="primary" :underline="false" class="view-all" @click="goToUserList">
|
||||
查看全部 <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div v-if="recentUsers.length === 0" class="empty-tip">暂无数据</div>
|
||||
<div v-for="item in recentUsers" :key="item.id" class="list-item" @click="goToUserDetail(item.id)">
|
||||
<div class="item-main">
|
||||
<div class="item-title">{{ item.name }}</div>
|
||||
<div class="item-sub">{{ item.email }}</div>
|
||||
</div>
|
||||
<div class="item-extra">
|
||||
<div class="item-id">ID: {{ item.id }}</div>
|
||||
<div class="item-time">{{ formatDate(item.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 最近订单 -->
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card class="list-card" shadow="hover" v-loading="listLoading">
|
||||
<div class="card-header-custom">
|
||||
<div class="header-left">
|
||||
<el-icon class="header-icon order-icon"><ShoppingCart /></el-icon>
|
||||
<h3>最近订单</h3>
|
||||
</div>
|
||||
<el-link type="primary" :underline="false" class="view-all" @click="goToOrderList">
|
||||
查看全部 <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div v-if="recentOrders.length === 0" class="empty-tip">暂无数据</div>
|
||||
<div v-for="item in recentOrders" :key="item.id" class="list-item" @click="showOrderDetail(item)">
|
||||
<div class="item-main">
|
||||
<div class="item-title">{{ item.name }}</div>
|
||||
<div class="item-sub">用户ID: {{ item.userId }}</div>
|
||||
</div>
|
||||
<div class="item-extra">
|
||||
<el-tag :type="getOrderStatusType(item.state)" size="small">
|
||||
{{ getOrderStatusText(item.state) }}
|
||||
</el-tag>
|
||||
<div class="item-price">¥{{ (item.price / 100).toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 最近工单 -->
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card class="list-card" shadow="hover" v-loading="listLoading">
|
||||
<div class="card-header-custom">
|
||||
<div class="header-left">
|
||||
<el-icon class="header-icon ticket-icon"><Tickets /></el-icon>
|
||||
<h3>最近工单</h3>
|
||||
</div>
|
||||
<el-link type="primary" :underline="false" class="view-all" @click="goToTicketList">
|
||||
查看全部 <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div v-if="recentTickets.length === 0" class="empty-tip">暂无数据</div>
|
||||
<div v-for="item in recentTickets" :key="item.id" class="list-item" @click="goToTicketDetail(item.id)">
|
||||
<div class="item-main">
|
||||
<div class="item-title">{{ item.title || '工单 #' + item.id }}</div>
|
||||
<div class="item-sub">用户ID: {{ item.userId }}</div>
|
||||
</div>
|
||||
<div class="item-extra">
|
||||
<el-tag :type="getTicketStatusType(item.status)" size="small">
|
||||
{{ getTicketStatusText(item.status) }}
|
||||
</el-tag>
|
||||
<div class="item-time">{{ formatDate(item.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 订单详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="orderDetailVisible"
|
||||
title="订单详情"
|
||||
width="600px"
|
||||
append-to-body
|
||||
class="order-detail-dialog"
|
||||
>
|
||||
<el-descriptions :column="2" border v-if="currentOrder">
|
||||
<el-descriptions-item label="订单ID">{{ currentOrder.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单名称">{{ currentOrder.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ currentOrder.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品ID">{{ currentOrder.commodityId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">
|
||||
<span class="detail-price">¥{{ (currentOrder.price / 100).toFixed(2) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="续费价格">
|
||||
<span class="detail-renew-price">¥{{ (currentOrder.renewPrice / 100).toFixed(2) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ currentOrder.payNum }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<el-tag :type="getOrderStatusType(currentOrder.state)">
|
||||
{{ getOrderStatusText(currentOrder.state) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付方式">{{ currentOrder.payType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.createdAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="orderDetailVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="goToOrderList">查看全部订单</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 最近活动和待办事项 -->
|
||||
<el-row :gutter="24" class="activity-row">
|
||||
<!-- <el-row :gutter="24" class="activity-row">
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
|
||||
<el-card class="activity-card" shadow="hover">
|
||||
<div class="card-header-custom">
|
||||
@@ -207,28 +334,45 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
User, ShoppingCart, Money, DataAnalysis,
|
||||
MoreFilled, ArrowUp, ArrowDown, Right,
|
||||
Download, Refresh, Check, Delete, Plus,
|
||||
Setting, Calendar, Filter
|
||||
Setting, Calendar, Filter, Tickets, View
|
||||
} from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import Qrcode from '@/components/Qrcode.vue'
|
||||
import {useUserStore} from "@/store/userStore.js";
|
||||
import { getUserList } from '@/api/admin/user'
|
||||
import { getOrderList } from '@/api/admin/order'
|
||||
import { getTicketCount, getTickerList } from '@/api/ticket'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 统计数据
|
||||
const userCount = ref(0)
|
||||
const orderCount = ref(0)
|
||||
const ticketCount = ref(0)
|
||||
|
||||
// 列表数据
|
||||
const recentUsers = ref([])
|
||||
const recentOrders = ref([])
|
||||
const recentTickets = ref([])
|
||||
const listLoading = ref(false)
|
||||
|
||||
// 数据统计卡片
|
||||
const statisticsCards = ref([
|
||||
const statisticsCards = computed(() => [
|
||||
{
|
||||
title: '访问量',
|
||||
value: '8,846',
|
||||
title: '用户量',
|
||||
value: userCount.value.toLocaleString(),
|
||||
icon: 'User',
|
||||
trend: 12.5,
|
||||
class: 'visitors',
|
||||
@@ -237,7 +381,7 @@ const statisticsCards = ref([
|
||||
},
|
||||
{
|
||||
title: '订单量',
|
||||
value: '1,257',
|
||||
value: orderCount.value.toLocaleString(),
|
||||
icon: 'ShoppingCart',
|
||||
trend: 5.2,
|
||||
class: 'orders',
|
||||
@@ -245,9 +389,9 @@ const statisticsCards = ref([
|
||||
progressColor: 'rgba(82, 196, 26, 0.8)'
|
||||
},
|
||||
{
|
||||
title: '销售额',
|
||||
value: '¥ 125,430',
|
||||
icon: 'Money',
|
||||
title: '工单量',
|
||||
value: ticketCount.value.toLocaleString(),
|
||||
icon: 'Tickets',
|
||||
trend: -2.3,
|
||||
class: 'sales',
|
||||
progress: 52,
|
||||
@@ -264,6 +408,150 @@ const statisticsCards = ref([
|
||||
}
|
||||
])
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
// 获取用户数量
|
||||
const userRes = await getUserList({ page: 1, count: 10, key: '' })
|
||||
console.log("用户数量,",userRes)
|
||||
if (userRes.data?.code === 200) {
|
||||
userCount.value = userRes.data.data.all_count || 0
|
||||
}
|
||||
|
||||
// 获取订单数量
|
||||
const orderRes = await getOrderList({ page: 1, count: 10 })
|
||||
console.log("订单数量,",orderRes)
|
||||
if (orderRes.data?.code === 200) {
|
||||
orderCount.value = orderRes.data.data.all_count || 0
|
||||
}
|
||||
|
||||
// 获取工单数量
|
||||
const ticketRes = await getTicketCount()
|
||||
console.log("工单数量,",ticketRes)
|
||||
if (ticketRes.code === 200) {
|
||||
ticketCount.value = ticketRes.data?.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最近列表数据
|
||||
const fetchRecentLists = async () => {
|
||||
listLoading.value = true
|
||||
try {
|
||||
// 获取最近用户
|
||||
const userRes = await getUserList({ page: 1, count: 10, key: '' })
|
||||
if (userRes.data?.code === 200) {
|
||||
recentUsers.value = (userRes.data.data.data || []).map(user => ({
|
||||
id: user.user_id,
|
||||
name: user.user_name,
|
||||
email: user.email || '未设置',
|
||||
phone: user.phone || '未设置',
|
||||
createdAt: user.created_at
|
||||
}))
|
||||
}
|
||||
|
||||
// 获取最近订单
|
||||
const orderRes = await getOrderList({ page: 1, count: 10 })
|
||||
if (orderRes.data?.code === 200) {
|
||||
recentOrders.value = (orderRes.data.data.list || []).map(order => ({
|
||||
id: order.id,
|
||||
name: order.name,
|
||||
userId: order.userId,
|
||||
commodityId: order.commodityId,
|
||||
price: order.price,
|
||||
renewPrice: order.renewPrice || 0,
|
||||
payNum: order.payNum || 1,
|
||||
state: order.state,
|
||||
payType: order.payType,
|
||||
createdAt: order.CreatedAt
|
||||
}))
|
||||
}
|
||||
|
||||
// 获取最近工单
|
||||
const ticketRes = await getTickerList(5, 1)
|
||||
console.log("最近工单,",ticketRes)
|
||||
if (ticketRes.code === 200) {
|
||||
recentTickets.value = (ticketRes.data.data?.list || ticketRes.data.data || []).map(ticket => ({
|
||||
id: ticket.work_id || ticket.id,
|
||||
title: ticket.title,
|
||||
status: ticket.status,
|
||||
userId: ticket.user?.userId,
|
||||
createdAt: ticket.created_at || ticket.CreatedAt
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取列表数据失败:', error)
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取订单状态
|
||||
const getOrderStatusText = (state) => {
|
||||
const statusMap = { 0: '待支付', 1: '已支付', 2: '已失效' }
|
||||
return statusMap[state] || '未知'
|
||||
}
|
||||
|
||||
const getOrderStatusType = (state) => {
|
||||
const typeMap = { 0: 'warning', 1: 'success', 2: 'info' }
|
||||
return typeMap[state] || 'info'
|
||||
}
|
||||
|
||||
// 获取工单状态
|
||||
const getTicketStatusText = (status) => {
|
||||
const statusMap = { 0: '待处理', 1: '处理中', 2: '已回复', 3: '已解决' }
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getTicketStatusType = (status) => {
|
||||
const typeMap = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'success' }
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 跳转到详情页
|
||||
const goToUserDetail = (userId) => {
|
||||
router.push({ path: '/user/detail', query: { user_id: userId } })
|
||||
}
|
||||
|
||||
const goToUserList = () => {
|
||||
router.push('/user/list')
|
||||
}
|
||||
|
||||
const goToOrderList = () => {
|
||||
router.push('/order/list')
|
||||
}
|
||||
|
||||
const goToTicketList = () => {
|
||||
router.push('/ticket/list')
|
||||
}
|
||||
|
||||
const goToTicketDetail = (ticketId) => {
|
||||
router.push({ path: '/ticket/detail', query: { work_id: ticketId } })
|
||||
}
|
||||
|
||||
// 订单详情弹窗
|
||||
const orderDetailVisible = ref(false)
|
||||
const currentOrder = ref(null)
|
||||
|
||||
const showOrderDetail = (order) => {
|
||||
currentOrder.value = order
|
||||
orderDetailVisible.value = true
|
||||
}
|
||||
|
||||
// 客户构成数据
|
||||
const customerData = ref([
|
||||
{ name: '企业客户', value: 1048, percentage: 33, color: '#1890ff' },
|
||||
@@ -331,6 +619,10 @@ const getPriorityType = (priority) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取统计数据和列表数据
|
||||
fetchStatistics()
|
||||
fetchRecentLists()
|
||||
|
||||
initSalesChart()
|
||||
initCustomerChart()
|
||||
|
||||
@@ -531,15 +823,19 @@ watch(salesRange, (newVal) => {
|
||||
/* 统计卡片样式 */
|
||||
.stat-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
border-left: 3px solid transparent !important;
|
||||
}
|
||||
|
||||
.stat-card.visitors { border-left-color: #1890ff !important; }
|
||||
.stat-card.orders { border-left-color: #52c41a !important; }
|
||||
.stat-card.sales { border-left-color: #faad14 !important; }
|
||||
.stat-card.conversion { border-left-color: #722ed1 !important; }
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
@@ -570,10 +866,10 @@ watch(salesRange, (newVal) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
font-size: 28px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -635,8 +931,6 @@ watch(salesRange, (newVal) => {
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -744,8 +1038,6 @@ watch(salesRange, (newVal) => {
|
||||
|
||||
.activity-card, .todo-card {
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-header-custom {
|
||||
@@ -870,6 +1162,149 @@ watch(salesRange, (newVal) => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 列表卡片样式 */
|
||||
.list-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
margin-bottom: 24px;
|
||||
height: 410px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.order-icon {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.ticket-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 0 20px 20px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background-color: #fafafa;
|
||||
margin: 0 -20px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.item-extra {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.item-id {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 订单详情弹窗样式 */
|
||||
.order-detail-dialog :deep(.el-descriptions__label) {
|
||||
width: 100px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.order-detail-dialog :deep(.el-descriptions__content) {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-renew-price {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-container {
|
||||
padding: 12px;
|
||||
@@ -888,5 +1323,30 @@ watch(salesRange, (newVal) => {
|
||||
.todo-list {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
min-height: 200px;
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.order-detail-dialog :deep(.el-dialog) {
|
||||
width: 90% !important;
|
||||
margin: 5vh auto !important;
|
||||
}
|
||||
|
||||
.order-detail-dialog :deep(.el-descriptions) {
|
||||
--el-descriptions-item-bordered-label-background: #fafafa;
|
||||
}
|
||||
|
||||
.order-detail-dialog :deep(.el-descriptions__label),
|
||||
.order-detail-dialog :deep(.el-descriptions__content) {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -141,17 +141,26 @@
|
||||
<el-radio label="percentage">百分比折扣</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额(元)" prop="amount">
|
||||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="width: 100%" />
|
||||
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额" prop="amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="discountForm.discount_mode === 'percentage'" label="优惠百分比(%)" prop="percentage">
|
||||
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="请输入百分比(1-100)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最低消费(元)" prop="min_amount">
|
||||
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
|
||||
<el-form-item label="最低消费" prop="min_amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大抵扣(元)" prop="max_amount">
|
||||
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
|
||||
<el-form-item label="最大抵扣" prop="max_amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_times">
|
||||
<el-input-number v-model="discountForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
@@ -651,6 +660,9 @@ onMounted(() => {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="代金卷">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金卷" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
@@ -71,7 +71,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
{{ row.goodId || row.goodGroupId || '-' }}
|
||||
@@ -149,7 +149,7 @@
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="dialogType === 'edit'"
|
||||
:disabled="dialogType === 'edit' || !!codeId"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
@@ -234,7 +234,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -249,13 +249,27 @@ import {
|
||||
getProductGroupList
|
||||
} from '@/api/admin/product'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
code_id: '',
|
||||
code_id: props.codeId || '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchGoodsList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
@@ -375,7 +389,7 @@ const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
count: 10,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
@@ -393,7 +407,7 @@ const fetchProductList = async () => {
|
||||
try {
|
||||
const res = await getProductList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
count: 10
|
||||
})
|
||||
console.log('获取商品列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
@@ -410,7 +424,7 @@ const fetchProductGroupList = async () => {
|
||||
try {
|
||||
const res = await getProductGroupList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
count: 10
|
||||
})
|
||||
console.log('获取商品组列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
@@ -784,33 +798,6 @@ onMounted(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="代金卷">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金卷" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
@@ -68,10 +68,21 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="discountId" label="代金券ID" width="120" />
|
||||
<el-table-column label="关联对象ID" width="130">
|
||||
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
|
||||
<el-table-column label="用户名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.userId || row.userGroupId || '-' }}
|
||||
<el-link v-if="row.userId && row?.user?.user_name" type="primary" :underline="false" @click="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.user.user_name }}</el-link>
|
||||
<span v-else>{{ row?.user?.user_name || '-' }}</span>
|
||||
</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>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
@@ -81,11 +92,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
@@ -224,82 +230,17 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="userSelectorVisible"
|
||||
title="选择用户"
|
||||
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>
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -313,14 +254,31 @@ import {
|
||||
getUserList,
|
||||
getUserGroupList
|
||||
} from '@/api/admin/user'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
code_id: '',
|
||||
code_id: props.codeId || '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchUsersList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
@@ -364,15 +322,6 @@ const userGroupOptions = ref([]) // 用户组列表选项
|
||||
|
||||
// 用户选择弹窗相关
|
||||
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) => {
|
||||
@@ -388,25 +337,24 @@ const formatDate = (dateStr) => {
|
||||
|
||||
// 获取用户类型名称(根据行数据)
|
||||
const getUserTypeNameByRow = (row) => {
|
||||
// userId 不为 0 说明是用户
|
||||
if (row.userId && row.userId !== 0) {
|
||||
|
||||
//通过看是否有user对象参数判断是否为用户还是用户组类型
|
||||
if(row.user){
|
||||
return '用户'
|
||||
}
|
||||
// userGroupId 不为 0 说明是用户组
|
||||
if (row.userGroupId && row.userGroupId !== 0) {
|
||||
}else{
|
||||
return '用户组'
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 获取用户类型标签(根据行数据)
|
||||
const getUserTypeTagByRow = (row) => {
|
||||
// 用户用蓝色
|
||||
if (row.userId && row.userId !== 0) {
|
||||
|
||||
|
||||
if(row.user){
|
||||
return 'primary'
|
||||
}
|
||||
// 用户组用橙色
|
||||
if (row.userGroupId && row.userGroupId !== 0) {
|
||||
}else{
|
||||
return 'warning'
|
||||
}
|
||||
return 'info'
|
||||
@@ -417,7 +365,7 @@ const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
count: 10,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
@@ -453,7 +401,7 @@ const fetchUserGroupList = async () => {
|
||||
try {
|
||||
const res = await getUserGroupList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
console.log('获取用户组列表:', res.data)
|
||||
@@ -469,70 +417,19 @@ const fetchUserGroupList = async () => {
|
||||
// 打开用户选择器
|
||||
const openUserSelector = () => {
|
||||
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 = () => {
|
||||
if (!selectedUserTemp.value) {
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
form.selected_user = selectedUserTemp.value.UserId
|
||||
form.user_id = selectedUserTemp.value.UserId
|
||||
form.selected_user = user.UserId
|
||||
form.user_id = user.UserId
|
||||
// 将选中的用户添加到 userOptions 中(如果不存在)
|
||||
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
||||
userOptions.value.push(selectedUserTemp.value)
|
||||
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
userOptions.value.push(user)
|
||||
}
|
||||
userSelectorVisible.value = false
|
||||
ElMessage.success('用户选择成功')
|
||||
@@ -691,6 +588,7 @@ const handleEdit = (row) => {
|
||||
})
|
||||
//点击编辑需要初始化加载用户列表
|
||||
fetchUserList()
|
||||
fetchUserGroupList()
|
||||
}
|
||||
|
||||
// 删除用户关联
|
||||
@@ -909,33 +807,6 @@ onMounted(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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: 10 })
|
||||
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>
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="user-voucher-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="代金券">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金券" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
@@ -54,37 +54,37 @@
|
||||
{{ row.Id || row.id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户ID" width="100">
|
||||
<el-table-column label="用户ID" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.UserId || row.userId }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券ID" width="100">
|
||||
<el-table-column label="代金券ID" width="100" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discountId }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券名称" min-width="150">
|
||||
<el-table-column label="代金券名称" min-width="150" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discount?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面额" width="120">
|
||||
<el-table-column label="面额" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已使用/最大次数" width="150">
|
||||
<el-table-column label="已使用/最大次数" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" width="180">
|
||||
<el-table-column label="过期时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expireAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
@@ -134,7 +134,7 @@
|
||||
<el-select
|
||||
v-model="addForm.voucher_id"
|
||||
placeholder="请选择代金券"
|
||||
:disabled="addForm.discount_type === 'code'"
|
||||
:disabled="addForm.discount_type === 'code' || !!codeId"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@@ -177,33 +177,36 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<el-select
|
||||
v-model="addForm.user_id"
|
||||
placeholder="请选择用户"
|
||||
:disabled="addForm.target_type === 'group'"
|
||||
filterable
|
||||
clearable
|
||||
remote
|
||||
:remote-method="searchUsers"
|
||||
:loading="userSearchLoading"
|
||||
style="width: 100%"
|
||||
@change="handleUserChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.UserId"
|
||||
:label="`${item.UserName} (ID: ${item.UserId})`"
|
||||
:value="item.UserId"
|
||||
/>
|
||||
</el-select>
|
||||
<el-form-item label="用户" prop="user_id" v-if="addForm.target_type === 'user'">
|
||||
<div class="user-selector-wrapper">
|
||||
<el-input
|
||||
:model-value="getSelectedUserName()"
|
||||
placeholder="请选择用户"
|
||||
readonly
|
||||
@click="openUserSelector"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="openUserSelector">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
v-if="addForm.user_id"
|
||||
type="danger"
|
||||
link
|
||||
@click="clearSelectedUser"
|
||||
class="clear-btn"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户组" prop="group_id">
|
||||
<el-form-item label="用户组" prop="group_id" v-if="addForm.target_type === 'group'">
|
||||
<el-select
|
||||
v-model="addForm.group_id"
|
||||
placeholder="请选择用户组"
|
||||
:disabled="addForm.target_type === 'user'"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@@ -268,13 +271,19 @@
|
||||
<el-button type="primary" @click="submitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
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 {
|
||||
getUserVoucherList,
|
||||
addUserVoucher,
|
||||
@@ -285,14 +294,29 @@ import {
|
||||
allocateVoucher
|
||||
} from '@/api/admin/discount'
|
||||
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({
|
||||
code_id: undefined,
|
||||
code_id: props.codeId || undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
})
|
||||
|
||||
// 添加表单
|
||||
const addForm = reactive({
|
||||
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
|
||||
@@ -321,6 +345,7 @@ const groupOptions = ref([]) // 用户组选项
|
||||
const userSearchLoading = ref(false) // 用户搜索加载状态
|
||||
const submitLoading = ref(false) // 提交加载状态
|
||||
const dataList = ref([]) // 优惠列表
|
||||
const userSelectorVisible = ref(false)
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
@@ -459,7 +484,7 @@ const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
count: 10,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
@@ -477,7 +502,7 @@ const fetchDiscountList = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
count: 10,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
@@ -488,7 +513,7 @@ const fetchDiscountList = async () => {
|
||||
}
|
||||
const res2 = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
count: 10,
|
||||
discount_type: 'code'
|
||||
})
|
||||
console.log('获取优惠码列表:', res2.data)
|
||||
@@ -508,7 +533,7 @@ const fetchVoucherOptions = async () => {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 100
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
voucherOptions.value = res.data.data?.data || []
|
||||
@@ -524,7 +549,7 @@ const fetchCodeOptions = async () => {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'code',
|
||||
page: 1,
|
||||
count: 100
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
codeOptions.value = res.data.data?.data || []
|
||||
@@ -541,7 +566,7 @@ const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
console.log('获取用户列表:', res.data)
|
||||
@@ -563,7 +588,7 @@ const fetchGroupOptions = async () => {
|
||||
try {
|
||||
const res = await getUserGroupList({
|
||||
page: 1,
|
||||
count: 100
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
groupOptions.value = res.data.data?.data || []
|
||||
@@ -623,6 +648,37 @@ const handleGroupChange = (val) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开用户选择器
|
||||
const openUserSelector = () => {
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
addForm.user_id = user.user_id
|
||||
// 将选中的用户添加到 userOptions 中(如果不存在)
|
||||
if (!userOptions.value.find(u => u.user_id === user.user_id)) {
|
||||
userOptions.value.push(user)
|
||||
}
|
||||
userSelectorVisible.value = false
|
||||
}
|
||||
|
||||
// 清除选中的用户
|
||||
const clearSelectedUser = () => {
|
||||
addForm.user_id = undefined
|
||||
}
|
||||
|
||||
// 获取选中用户的显示名称
|
||||
const getSelectedUserName = () => {
|
||||
if (!addForm.user_id) return ''
|
||||
const user = userOptions.value.find(u => u.user_id === addForm.user_id)
|
||||
return user ? `${user.user_name} (ID: ${user.user_id})` : `用户ID: ${addForm.user_id}`
|
||||
}
|
||||
|
||||
// 添加用户代金券
|
||||
const handleAdd = async () => {
|
||||
addDialogVisible.value = true
|
||||
@@ -630,7 +686,7 @@ const handleAdd = async () => {
|
||||
// 重置表单
|
||||
Object.assign(addForm, {
|
||||
discount_type: 'coupon',
|
||||
voucher_id: undefined,
|
||||
voucher_id: props.codeId || undefined,
|
||||
code_id: undefined,
|
||||
target_type: 'user',
|
||||
user_id: undefined,
|
||||
@@ -837,6 +893,9 @@ onMounted(() => {
|
||||
// 加载代金券列表供选择
|
||||
fetchVoucherListOptions()
|
||||
fetchDiscountList()
|
||||
if (queryParams.code_id) {
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -888,5 +947,21 @@ onMounted(() => {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 用户选择器样式 */
|
||||
.user-selector-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-selector-wrapper .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-selector-wrapper .clear-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -63,9 +63,10 @@
|
||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
@@ -104,14 +105,23 @@
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="voucherForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="面额(元)" prop="amount">
|
||||
<el-input-number v-model="voucherForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="width: 100%" />
|
||||
<el-form-item label="面额" prop="amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="voucherForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低消费(元)" prop="min_amount">
|
||||
<el-input-number v-model="voucherForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
|
||||
<el-form-item label="最低消费" prop="min_amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="voucherForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大抵扣(元)" prop="max_amount">
|
||||
<el-input-number v-model="voucherForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
|
||||
<el-form-item label="最大抵扣" prop="max_amount">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="voucherForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_times">
|
||||
<el-input-number v-model="voucherForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
@@ -119,8 +129,11 @@
|
||||
<el-form-item label="单用户最大次数" prop="user_times">
|
||||
<el-input-number v-model="voucherForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期(天)" prop="duration_days">
|
||||
<el-input-number v-model="voucherForm.duration_days" :min="1" placeholder="代金券有效天数" style="width: 100%" />
|
||||
<el-form-item label="有效期" prop="duration_days">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="voucherForm.duration_days" :min="1" placeholder="代金券有效天数" style="flex:1" />
|
||||
<span class="unit-text">天</span>
|
||||
</div>
|
||||
<div class="form-tip">代金券领取后的有效持续时间</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="发放时间范围" prop="timeRange">
|
||||
@@ -166,8 +179,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -180,6 +195,8 @@ import {
|
||||
import { timeToTimestamp } from '@/utils/tool'
|
||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
discount_type: 'coupon', // 固定为coupon表示代金券
|
||||
@@ -290,9 +307,39 @@ const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 转换日期字符串为日期选择器格式
|
||||
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
console.log('编辑代金券原始数据:', row)
|
||||
|
||||
// 转换时间为日期字符串(YYYY-MM-DD HH:mm:ss 格式)
|
||||
let startTime = ''
|
||||
let endTime = ''
|
||||
|
||||
if (row.startTime) {
|
||||
// 处理字符串格式的时间(如 "2026-01-08T00:00:00+08:00")
|
||||
const start = new Date(row.startTime)
|
||||
if (!isNaN(start.getTime())) {
|
||||
startTime = start.getFullYear() + '-' +
|
||||
String(start.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(start.getDate()).padStart(2, '0') + ' ' +
|
||||
String(start.getHours()).padStart(2, '0') + ':' +
|
||||
String(start.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(start.getSeconds()).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
if (row.endTime) {
|
||||
// 处理字符串格式的时间(如 "2026-02-25T00:00:00+08:00")
|
||||
const end = new Date(row.endTime)
|
||||
if (!isNaN(end.getTime())) {
|
||||
endTime = end.getFullYear() + '-' +
|
||||
String(end.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(end.getDate()).padStart(2, '0') + ' ' +
|
||||
String(end.getHours()).padStart(2, '0') + ':' +
|
||||
String(end.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(end.getSeconds()).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('转换后的时间:', { startTime, endTime })
|
||||
|
||||
Object.assign(voucherForm, {
|
||||
code_id: row.id,
|
||||
@@ -304,12 +351,19 @@ const handleEdit = (row) => {
|
||||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
||||
max_times: row.maxTimes || 0,
|
||||
user_times: row.userTimes || 0,
|
||||
duration_days: row.duration ? row.duration / 86400 : 30, // 秒转天
|
||||
duration_days: row.duration ? Math.round(row.duration / 86400) : 30, // 秒转天
|
||||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
||||
renew: row.renew || false,
|
||||
can_stacking: row.canStacking || false,
|
||||
can_combine: row.canCombine || false
|
||||
})
|
||||
|
||||
console.log('表单数据:', voucherForm)
|
||||
}
|
||||
|
||||
// 管理代金券
|
||||
const handleManage = (row) => {
|
||||
router.push(`/marketing/voucher/${row.id}/manage`)
|
||||
}
|
||||
|
||||
// 查看代金券详情
|
||||
@@ -497,6 +551,9 @@ onMounted(() => {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -43,11 +43,16 @@
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="id" label="记录ID" width="80" fixed="left" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column label="用户ID" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.user_id" type="primary" :underline="false" @click="router.push({ path: '/user/detail', query: { user_id: row.user_id } })">{{ row.user_id }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="discount_id" label="代金券ID" width="120" />
|
||||
<el-table-column prop="discount_name" label="代金券名称" min-width="180" />
|
||||
<el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
|
||||
<el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
|
||||
<el-table-column label="优惠金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
@@ -58,7 +63,12 @@
|
||||
<span>¥{{ row.order_amount ? (row.order_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_id" label="订单ID" width="150" />
|
||||
<el-table-column label="订单ID" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.order_id" type="primary" :underline="false" @click="router.push({ path: '/order/list', query: { key: row.order_id } })">{{ row.order_id }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
@@ -133,74 +143,10 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="userSelectorVisible"
|
||||
title="选择用户"
|
||||
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>
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
@@ -237,20 +183,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, Download } from '@element-plus/icons-vue'
|
||||
import { getUserVoucherHistory, getDiscountCodeList } 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 router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
user_id: undefined,
|
||||
code_id: props.codeId || undefined,
|
||||
id: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchHistoryList()
|
||||
}
|
||||
})
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const historyList = ref([])
|
||||
@@ -261,15 +226,6 @@ const currentDetail = ref({})
|
||||
const discountOptions = ref([])
|
||||
const selectorType = ref('query')
|
||||
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([])
|
||||
|
||||
// 格式化日期
|
||||
@@ -371,86 +327,41 @@ const resetUserSearch = () => {
|
||||
// fetchUserSelectorList()
|
||||
}
|
||||
|
||||
|
||||
// 打开查询用户选择器
|
||||
const openQueryUserSelector = () => {
|
||||
selectorType.value = 'query'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
// 打开编辑用户选择器
|
||||
const openEditUserSelector = () => {
|
||||
selectorType.value = 'edit'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = () => {
|
||||
if (!selectedUserTemp.value) {
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
// 查询表单选择
|
||||
queryParams.user_id = selectedUserTemp.value.UserId
|
||||
queryParams.user_id = user.UserId
|
||||
} else {
|
||||
// 编辑表单选择
|
||||
editForm.user_id = selectedUserTemp.value.UserId
|
||||
editForm.user_id = user.UserId
|
||||
}
|
||||
|
||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
||||
UserOptions.value.push(selectedUserTemp.value)
|
||||
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
UserOptions.value.push(user)
|
||||
}
|
||||
|
||||
userSelectorVisible.value = false
|
||||
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 = () => {
|
||||
@@ -461,7 +372,7 @@ const handleQuery = () => {
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.user_id = undefined
|
||||
queryParams.discount_id = undefined
|
||||
queryParams.code_id = undefined
|
||||
queryParams.id = ''
|
||||
queryParams.page = 1
|
||||
fetchHistoryList()
|
||||
@@ -499,7 +410,7 @@ const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
UserOptions.value = res.data.data?.data || []
|
||||
@@ -514,7 +425,7 @@ const fetchDiscountList = async () => {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 1000
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
discountOptions.value = res.data.data?.data || []
|
||||
|
||||
@@ -40,51 +40,56 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="Id" label="ID" width="80" />
|
||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
||||
<el-table-column label="代金券ID" width="120">
|
||||
<el-table-column label="用户ID" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.UserId" type="primary" :underline="false" @click="router.push({ path: '/user/detail', query: { user_id: row.UserId } })">{{ row.UserId }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券ID" min-width="110" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discountId || '-' }}
|
||||
</template>
|
||||
</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 }">
|
||||
{{ row.discount?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券编码" width="150">
|
||||
<el-table-column label="代金券编码" min-width="150" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discount?.code || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面额" width="120">
|
||||
<el-table-column label="面额" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="useTimes" label="已使用次数" width="120" />
|
||||
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<el-table-column prop="useTimes" label="已使用" min-width="100" />
|
||||
<el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row)">
|
||||
<el-tag :type="getStatusType(row)" size="small">
|
||||
{{ getStatusText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" width="180">
|
||||
<el-table-column label="过期时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expireAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleView(row)">查看详情</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -203,81 +208,18 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="userSelectorVisible"
|
||||
title="选择用户"
|
||||
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>
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 {
|
||||
getUserVoucherList,
|
||||
allocateVoucher,
|
||||
@@ -285,14 +227,36 @@ import {
|
||||
deleteUserVoucher,
|
||||
getDiscountCodeList
|
||||
} 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 router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
user_id: undefined,
|
||||
code_id: props.codeId || undefined,
|
||||
page: 1,
|
||||
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 holdersList = ref([])
|
||||
@@ -307,16 +271,7 @@ const discountOptions = ref([])
|
||||
|
||||
// 用户选择弹窗相关
|
||||
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 userSearchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
@@ -448,101 +403,36 @@ const handleExport = () => {
|
||||
ElMessage.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
key: ''
|
||||
})
|
||||
UserOptions.value = res.data.data?.data || []
|
||||
}
|
||||
|
||||
// 打开查询用户选择器
|
||||
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 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 = () => {
|
||||
if (!selectedUserTemp.value) {
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
// 查询表单选择
|
||||
queryParams.user_id = selectedUserTemp.value.UserId
|
||||
queryParams.user_id = user.UserId
|
||||
} else {
|
||||
// 编辑表单选择
|
||||
editForm.user_id = selectedUserTemp.value.UserId
|
||||
editForm.user_id = user.UserId
|
||||
}
|
||||
|
||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
||||
UserOptions.value.push(selectedUserTemp.value)
|
||||
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
UserOptions.value.push(user)
|
||||
}
|
||||
|
||||
userSelectorVisible.value = false
|
||||
@@ -577,7 +467,7 @@ const fetchDiscountList = async () => {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 1000
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
discountOptions.value = res.data.data?.data || []
|
||||
@@ -692,7 +582,6 @@ const submitEditForm = () => {
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
fetchDiscountList()
|
||||
if (queryParams.user_id) {
|
||||
fetchHoldersList()
|
||||
|
||||
@@ -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>
|
||||
+437
-38
@@ -5,6 +5,36 @@
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<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 label="错误信息">
|
||||
<el-select v-model="queryParams.error" placeholder="全部" clearable style="width: 140px">
|
||||
<el-option label="有错误的订单" :value="true" />
|
||||
<el-option label="无错误的订单" :value="false" />
|
||||
</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>新增订单
|
||||
@@ -43,8 +73,18 @@
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="订单ID" width="100" />
|
||||
<el-table-column prop="name" label="订单名称" min-width="180" />
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column prop="commodityId" label="商品ID" width="100" />
|
||||
<el-table-column label="用户ID" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.userId" type="primary" :underline="false" @click.stop="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品ID" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.commodityId" type="primary" :underline="false" @click.stop="router.push({ path: '/user-goods/list', query: { good_id: row.commodityId } })">{{ row.commodityId }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="表名" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.table || '未知' }}</el-tag>
|
||||
@@ -65,11 +105,22 @@
|
||||
<span>{{ row.payNum }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="100">
|
||||
<el-table-column label="订单状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.state)">
|
||||
{{ getStatusText(row.state) }}
|
||||
</el-tag>
|
||||
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
|
||||
<el-tag :type="getStatusType(row.state)">
|
||||
{{ getStatusText(row.state) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.error" type="danger" size="small">异常</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误信息" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.error" :content="row.error" placement="top" :show-after="300">
|
||||
<span class="error-text">{{ row.error }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付方式" width="100">
|
||||
@@ -87,11 +138,12 @@
|
||||
<span>{{ formatDate(row.CreatedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.error" type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -138,6 +190,10 @@
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.UpdatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参数信息">{{ orderDetail.args || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="orderDetail.error" label="错误信息" :span="2">
|
||||
<el-tag type="danger" size="small" style="margin-right: 6px;">异常</el-tag>
|
||||
<span style="color: #f56c6c;">{{ orderDetail.error }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ orderDetail.note || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
@@ -162,28 +218,149 @@
|
||||
<el-input v-model="orderForm.table" placeholder="请输入所属表" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID" prop="user_id">
|
||||
<el-input-number v-model="orderForm.user_id" :min="1" placeholder="请输入用户ID" style="width: 100%" />
|
||||
<el-input
|
||||
v-if="selectedUserInfo"
|
||||
:model-value="`${selectedUserInfo.user_name} (ID: ${orderForm.user_id})`"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clear-icon" @click="clearUser"><Close /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="userSelectorVisible = true">
|
||||
<el-icon><User /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
placeholder="请选择用户"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
@click="userSelectorVisible = true"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="userSelectorVisible = true">
|
||||
<el-icon><User /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品ID" prop="commodity_id">
|
||||
<el-input-number v-model="orderForm.commodity_id" :min="0" placeholder="请输入商品ID" style="width: 100%" />
|
||||
<el-input
|
||||
v-if="selectedProductInfo"
|
||||
:model-value="`${selectedProductInfo.name} (ID: ${orderForm.commodity_id})`"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clear-icon" @click="clearProduct"><Close /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="productSelectorVisible = true">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
placeholder="请选择商品"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
@click="productSelectorVisible = true"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="productSelectorVisible = true">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="购买数量" prop="pay_num">
|
||||
<el-input-number v-model="orderForm.pay_num" :min="1" placeholder="请输入数量" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格(分)" prop="price">
|
||||
<el-input-number v-model="orderForm.price" :min="0" placeholder="请输入价格(分)" style="width: 100%" />
|
||||
<el-form-item label="价格" prop="price">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="orderForm.price" :min="0" placeholder="请输入价格(分)" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="续费价格(分)" prop="renew_price">
|
||||
<el-input-number v-model="orderForm.renew_price" :min="0" placeholder="请输入续费价格(分)" style="width: 100%" />
|
||||
<el-form-item label="续费价格" prop="renew_price">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="orderForm.renew_price" :min="0" placeholder="请输入续费价格(分)" style="flex:1" />
|
||||
<span class="unit-text">分</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_time">
|
||||
<el-input-number v-model="orderForm.expire_time" :min="0" placeholder="请输入过期时间(时间戳)" style="width: 100%" />
|
||||
<el-date-picker
|
||||
v-model="orderForm.expire_time"
|
||||
type="datetime"
|
||||
placeholder="请选择过期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="x"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠码ID" prop="discount_code_id">
|
||||
<el-input-number v-model="orderForm.discount_code_id" :min="0" placeholder="请输入优惠码ID" style="width: 100%" />
|
||||
<el-input
|
||||
v-if="selectedDiscountCodeInfo"
|
||||
:model-value="`${selectedDiscountCodeInfo.name || selectedDiscountCodeInfo.code} (ID: ${orderForm.discount_code_id})`"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clear-icon" @click="clearDiscountCode"><Close /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="discountCodeSelectorVisible = true">
|
||||
<el-icon><Ticket /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
placeholder="请选择优惠码(可选)"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
@click="discountCodeSelectorVisible = true"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="discountCodeSelectorVisible = true">
|
||||
<el-icon><Ticket /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="代金券ID" prop="coupon_id">
|
||||
<el-input-number v-model="orderForm.coupon_id" :min="0" placeholder="请输入代金券ID (必填)" style="width: 100%" />
|
||||
<el-input
|
||||
v-if="selectedVoucherInfo"
|
||||
:model-value="`${selectedVoucherInfo.name || selectedVoucherInfo.code} (ID: ${orderForm.coupon_id})`"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clear-icon" @click="clearVoucher"><Close /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="voucherSelectorVisible = true">
|
||||
<el-icon><Money /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
placeholder="请选择代金券(可选)"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
@click="voucherSelectorVisible = true"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="voucherSelectorVisible = true">
|
||||
<el-icon><Money /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态" prop="state">
|
||||
<el-radio-group v-model="orderForm.state">
|
||||
@@ -209,20 +386,61 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户选择器 -->
|
||||
<UserListSelector
|
||||
v-model="userSelectorVisible"
|
||||
:current-user-id="orderForm.user_id"
|
||||
@confirm="handleUserSelect"
|
||||
/>
|
||||
|
||||
<!-- 商品选择器 -->
|
||||
<ProductSelector
|
||||
v-model="productSelectorVisible"
|
||||
:current-product-id="orderForm.commodity_id"
|
||||
@confirm="handleProductSelect"
|
||||
/>
|
||||
|
||||
<!-- 优惠码选择器 -->
|
||||
<DiscountCodeSelector
|
||||
v-model="discountCodeSelectorVisible"
|
||||
:current-code-id="orderForm.discount_code_id"
|
||||
@confirm="handleDiscountCodeSelect"
|
||||
/>
|
||||
|
||||
<!-- 代金券选择器 -->
|
||||
<VoucherSelector
|
||||
v-model="voucherSelectorVisible"
|
||||
:current-voucher-id="orderForm.coupon_id"
|
||||
@confirm="handleVoucherSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Search, Download, Refresh } from '@element-plus/icons-vue'
|
||||
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } from '@/api/admin/order'
|
||||
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close } from '@element-plus/icons-vue'
|
||||
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
|
||||
import VoucherSelector from '@/components/admin/VoucherSelector.vue'
|
||||
import { isoToMilliseconds, timeToTimestamp, formatDate as formatDateTool } from '@/utils/tool'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
|
||||
page: 1,
|
||||
count: 10
|
||||
count: 10,
|
||||
key: '',
|
||||
state: '',
|
||||
user_id: '',
|
||||
user_key: '',
|
||||
error: null
|
||||
})
|
||||
|
||||
// 订单表单
|
||||
@@ -278,14 +496,43 @@ const detailDialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const orderFormRef = ref(null)
|
||||
|
||||
// 选择器弹窗状态
|
||||
const userSelectorVisible = ref(false)
|
||||
const productSelectorVisible = ref(false)
|
||||
const discountCodeSelectorVisible = ref(false)
|
||||
const voucherSelectorVisible = ref(false)
|
||||
|
||||
// 选择的显示信息
|
||||
const selectedUserInfo = ref(null)
|
||||
const selectedProductInfo = ref(null)
|
||||
const selectedDiscountCodeInfo = ref(null)
|
||||
const selectedVoucherInfo = ref(null)
|
||||
|
||||
// 获取订单列表
|
||||
const fetchOrderList = async () => {
|
||||
loading.value = true
|
||||
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)
|
||||
if (res.data.code === 200) {
|
||||
orderList.value = res.data.data.list || []
|
||||
// 处理时间数据:将ISO格式转换为毫秒级时间戳(用于时间选择器)
|
||||
const list = (res.data.data.list || []).map(item => {
|
||||
if (item.expireTime) {
|
||||
// 保存原始时间用于显示
|
||||
item._originalExpireTime = item.expireTime
|
||||
// 转换为毫秒级时间戳用于时间选择器
|
||||
item._expireTimeMs = isoToMilliseconds(item.expireTime)
|
||||
}
|
||||
return item
|
||||
})
|
||||
orderList.value = list
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -296,16 +543,9 @@ const fetchOrderList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
// 格式化日期 - 使用工具函数
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
return formatDateTool(dateStr)
|
||||
}
|
||||
|
||||
// 获取订单状态类型
|
||||
@@ -337,10 +577,11 @@ const handleQuery = () => {
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.order_no = ''
|
||||
queryParams.key = ''
|
||||
queryParams.state = ''
|
||||
queryParams.user_id = ''
|
||||
queryParams.status = ''
|
||||
queryParams.dateRange = []
|
||||
queryParams.user_key = ''
|
||||
queryParams.error = null
|
||||
queryParams.page = 1
|
||||
fetchOrderList()
|
||||
}
|
||||
@@ -365,6 +606,7 @@ const handleCurrentChange = (page) => {
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
clearAllSelections()
|
||||
Object.assign(orderForm, {
|
||||
order_id: undefined,
|
||||
name: '',
|
||||
@@ -403,6 +645,16 @@ const handleView = async (row) => {
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
clearAllSelections()
|
||||
|
||||
// 处理过期时间:优先使用已转换的时间戳,否则转换ISO格式
|
||||
let expireTimeMs = null
|
||||
if (row._expireTimeMs !== undefined) {
|
||||
expireTimeMs = row._expireTimeMs
|
||||
} else if (row.expireTime) {
|
||||
expireTimeMs = isoToMilliseconds(row.expireTime)
|
||||
}
|
||||
|
||||
Object.assign(orderForm, {
|
||||
order_id: row.id,
|
||||
name: row.name,
|
||||
@@ -412,7 +664,7 @@ const handleEdit = (row) => {
|
||||
pay_num: row.payNum,
|
||||
price: row.price,
|
||||
renew_price: row.renewPrice,
|
||||
expire_time: row.expireTime ? new Date(row.expireTime).getTime() / 1000 : 0,
|
||||
expire_time: expireTimeMs,
|
||||
discount_code_id: 0, // 从详情接口获取
|
||||
coupon_id: 0, // 从详情接口获取
|
||||
state: row.state,
|
||||
@@ -420,6 +672,40 @@ const handleEdit = (row) => {
|
||||
args: row.args || '',
|
||||
note: row.note || ''
|
||||
})
|
||||
|
||||
// 设置显示信息(只显示ID,名称需要从选择器中获取)
|
||||
if (row.userId) {
|
||||
selectedUserInfo.value = { user_id: row.userId, user_name: `用户${row.userId}` }
|
||||
}
|
||||
if (row.commodityId) {
|
||||
selectedProductInfo.value = { id: row.commodityId, name: `商品${row.commodityId}` }
|
||||
}
|
||||
}
|
||||
|
||||
// 重试订单流程
|
||||
const handleRetryOrder = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确认对订单「${row.name}」(ID: ${row.id}) 重试流程吗?`,
|
||||
'重试订单流程',
|
||||
{
|
||||
confirmButtonText: '确认重试',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await retryOrderHook({ order_id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('重试流程已触发')
|
||||
fetchOrderList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '重试失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重试订单流程失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '重试订单流程失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
@@ -463,6 +749,13 @@ const submitForm = () => {
|
||||
orderFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 处理过期时间:将毫秒级时间戳转换为秒级时间戳
|
||||
let expireTimeSeconds = 0
|
||||
if (orderForm.expire_time) {
|
||||
const timestamp = timeToTimestamp(new Date(orderForm.expire_time))
|
||||
expireTimeSeconds = timestamp || 0
|
||||
}
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
name: orderForm.name,
|
||||
@@ -472,7 +765,7 @@ const submitForm = () => {
|
||||
pay_num: Number(orderForm.pay_num),
|
||||
price: Number(orderForm.price),
|
||||
renew_price: Number(orderForm.renew_price),
|
||||
expire_time: Number(orderForm.expire_time),
|
||||
expire_time: expireTimeSeconds,
|
||||
discount_code_id: Number(orderForm.discount_code_id),
|
||||
coupon_id: Number(orderForm.coupon_id),
|
||||
state: Number(orderForm.state),
|
||||
@@ -508,8 +801,67 @@ const submitForm = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 用户选择处理
|
||||
const handleUserSelect = (user) => {
|
||||
orderForm.user_id = user.user_id
|
||||
selectedUserInfo.value = user
|
||||
}
|
||||
|
||||
const clearUser = () => {
|
||||
orderForm.user_id = undefined
|
||||
selectedUserInfo.value = null
|
||||
}
|
||||
|
||||
// 商品选择处理
|
||||
const handleProductSelect = (product) => {
|
||||
orderForm.commodity_id = product.id
|
||||
selectedProductInfo.value = product
|
||||
// 自动填充表名
|
||||
if (product.table) {
|
||||
orderForm.table = product.table
|
||||
}
|
||||
}
|
||||
|
||||
const clearProduct = () => {
|
||||
orderForm.commodity_id = 0
|
||||
selectedProductInfo.value = null
|
||||
}
|
||||
|
||||
// 优惠码选择处理
|
||||
const handleDiscountCodeSelect = (code) => {
|
||||
orderForm.discount_code_id = code.id
|
||||
selectedDiscountCodeInfo.value = code
|
||||
}
|
||||
|
||||
const clearDiscountCode = () => {
|
||||
orderForm.discount_code_id = 0
|
||||
selectedDiscountCodeInfo.value = null
|
||||
}
|
||||
|
||||
// 代金券选择处理
|
||||
const handleVoucherSelect = (voucher) => {
|
||||
orderForm.coupon_id = voucher.id
|
||||
selectedVoucherInfo.value = voucher
|
||||
}
|
||||
|
||||
const clearVoucher = () => {
|
||||
orderForm.coupon_id = 0
|
||||
selectedVoucherInfo.value = null
|
||||
}
|
||||
|
||||
// 清除所有选择信息
|
||||
const clearAllSelections = () => {
|
||||
selectedUserInfo.value = null
|
||||
selectedProductInfo.value = null
|
||||
selectedDiscountCodeInfo.value = null
|
||||
selectedVoucherInfo.value = null
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (route.query.key) queryParams.key = String(route.query.key)
|
||||
if (route.query.user_id) queryParams.user_id = String(route.query.user_id)
|
||||
if (route.query.state) queryParams.state = String(route.query.state)
|
||||
fetchOrderList()
|
||||
})
|
||||
</script>
|
||||
@@ -532,13 +884,29 @@ onMounted(() => {
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -651,4 +1019,35 @@ onMounted(() => {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* 选择器清除图标样式 */
|
||||
.clear-icon {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.clear-icon:hover {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
|
||||
.error-text {
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
+2073
-66
File diff suppressed because it is too large
Load Diff
+2335
-43
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="goods-detail-page">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<el-button @click="goBack" link class="back-btn">
|
||||
<el-icon><ArrowLeft /></el-icon> 返回所有商品列表
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="page-title">所有商品详情</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" plain @click="loadDetail" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon> 刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="main-content" v-loading="loading">
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && !detail" description="未找到商品数据" :image-size="160">
|
||||
<el-button type="primary" @click="loadDetail">重新加载</el-button>
|
||||
</el-empty>
|
||||
|
||||
<el-card class="profile-card" shadow="hover" v-if="detail">
|
||||
<div class="profile-header">
|
||||
<div class="profile-basic">
|
||||
<div class="icon-wrapper">
|
||||
<el-icon :size="48" color="#409eff"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="identity">
|
||||
<div class="name-row">
|
||||
<h1 class="name">{{ detail.good?.name || '用户商品 #' + goodsId }}</h1>
|
||||
<el-button size="small" type="primary" plain @click="openEdit">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<span class="label">ID:</span>
|
||||
<span class="value">{{ detail.id || goodsId }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="label">用户ID:</span>
|
||||
<span class="value">{{ detail.userId || detail.user_id || '-' }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="label">到期:</span>
|
||||
<span class="value">{{ formatExpireTime(detail.expireTime || detail.expire_time) }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="label">续费价:</span>
|
||||
<span class="value">{{ detail.renewPrice ? '¥' + (detail.renewPrice / 100).toFixed(2) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">套餐ID</div>
|
||||
<div class="stat-value">{{ detail.goodPlanId || detail.good_plan_id || '-' }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">备注</div>
|
||||
<div class="stat-value note-value">{{ detail.note || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 16px 0 12px" />
|
||||
<el-descriptions :column="3" border size="small" style="width:100%">
|
||||
<el-descriptions-item label="商品ID">{{ detail.goodId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单ID">{{ detail.orderId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="归属项ID">{{ detail.itemId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="基础价格">{{ (detail.basePrice || detail.base_price) ? '¥' + ((detail.basePrice || detail.base_price) / 100).toFixed(2) : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(detail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatTime(detail.UpdatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="hover" v-if="detail" class="related-card">
|
||||
<template #header>
|
||||
<span class="card-title">关联信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="商品名称">{{ detail.good?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品Table">{{ detail.good?.table || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品标签">{{ detail.good?.tag || detail.tag || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单名称">{{ detail.order?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<el-tag v-if="detail.order" :type="detail.order.state === 1 ? 'success' : detail.order.state === 0 ? 'warning' : 'info'" size="small">
|
||||
{{ detail.order.state === 1 ? '已支付' : detail.order.state === 0 ? '待支付' : '已失效' }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ detail.userId || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editVisible" title="编辑用户商品" width="520px" destroy-on-close>
|
||||
<el-form :model="editForm" label-width="110px">
|
||||
<el-form-item label="备注"><el-input v-model="editForm.note" /></el-form-item>
|
||||
<el-form-item label="续费价格">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editForm.renew_price" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="基础价格">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editForm.base_price" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="到期时间"><el-date-picker v-model="editForm.expire_time" type="datetime" placeholder="选择到期时间" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="归属项">
|
||||
<div style="width:100%">
|
||||
<template v-if="detail?.good?.table === 'kvm_service'">
|
||||
<div class="selector-row" style="margin-bottom:8px">
|
||||
<el-input :model-value="editForm._serviceName || (editForm._serviceId ? `主控服务 #${editForm._serviceId}` : '')"
|
||||
readonly placeholder="1. 选择主控服务" style="flex:1" />
|
||||
<el-button type="primary" @click="showServiceSelector = true" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="editForm._serviceId" @click="editForm._serviceId = 0; editForm._serviceName = ''; editForm.item_id = 0; editForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="editForm._itemName || (editForm.item_id ? `虚拟机 #${editForm.item_id}` : '')"
|
||||
readonly placeholder="2. 选择虚拟机" style="flex:1" />
|
||||
<el-button type="primary" @click="showVmSelector = true" :disabled="!editForm._serviceId" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="editForm.item_id" @click="editForm.item_id = 0; editForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
<div style="font-size:12px;color:#909399;margin-top:4px">归属项为虚拟机ID,需先选择主控服务</div>
|
||||
</template>
|
||||
<el-input-number v-else v-model="editForm.item_id" :min="0" controls-position="right" style="width:100%" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<VmSelectorPopup v-model="showVmSelector" :service-id="editForm._serviceId || 0"
|
||||
@confirm="vm => { editForm.item_id = vm.id; editForm._itemName = vm.name }" />
|
||||
<KvmServiceSelector v-model="showServiceSelector"
|
||||
@confirm="s => { editForm._serviceId = s.id; editForm._serviceName = s.name; editForm.item_id = 0; editForm._itemName = '' }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, Monitor } from '@element-plus/icons-vue'
|
||||
import { getUserGoodsDetail, updateUserGoods, deleteUserGoods } from '@/api/admin/userVm'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
|
||||
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const goodsId = computed(() => parseInt(route.params.id) || 0)
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||
const formatExpireTime = (t) => {
|
||||
if (!t) return '-'
|
||||
const d = dayjs(t)
|
||||
if (d.year() < 2000) return '永久'
|
||||
return d.format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const goBack = () => router.push('/user-goods/list')
|
||||
|
||||
const loadDetail = async () => {
|
||||
if (!goodsId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserGoodsDetail({ id: goodsId.value })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
detail.value = res.data.data.data ?? res.data.data
|
||||
} else ElMessage.error(extractApiError(res?.data, '加载失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({ note: '', renew_price: 0, base_price: 0, expire_time: '', item_id: 0, _serviceId: 0, _serviceName: '', _itemName: '' })
|
||||
const showVmSelector = ref(false)
|
||||
const showServiceSelector = ref(false)
|
||||
|
||||
const openEdit = () => {
|
||||
const rawRenew = detail.value?.renewPrice || detail.value?.renew_price || 0
|
||||
const rawBase = detail.value?.basePrice || detail.value?.base_price || 0
|
||||
Object.assign(editForm, {
|
||||
note: detail.value?.note || '',
|
||||
renew_price: rawRenew / 100,
|
||||
base_price: rawBase / 100,
|
||||
expire_time: detail.value?.expireTime || detail.value?.expire_time
|
||||
? dayjs(detail.value?.expireTime || detail.value?.expire_time).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '',
|
||||
item_id: detail.value?.itemId || detail.value?.item_id || 0,
|
||||
_serviceId: 0,
|
||||
_serviceName: '',
|
||||
_itemName: detail.value?.itemId ? `虚拟机 #${detail.value.itemId}` : ''
|
||||
})
|
||||
if (detail.value?.good?.table === 'kvm_service') { /* 通过选择器弹窗选择,无需预加载 */ }
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const submitEdit = async () => {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data = { id: goodsId.value }
|
||||
if (editForm.note !== undefined) data.note = editForm.note
|
||||
if (editForm.renew_price) data.renew_price = Math.round(editForm.renew_price * 100)
|
||||
if (editForm.base_price) data.base_price = Math.round(editForm.base_price * 100)
|
||||
if (editForm.expire_time) data.expire_time = editForm.expire_time
|
||||
if (editForm.item_id) data.item_id = editForm.item_id
|
||||
const res = await updateUserGoods(data)
|
||||
if (res?.data?.code === 200) { ElMessage.success('修改成功'); editVisible.value = false; loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '修改失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) }
|
||||
finally { submitLoading.value = false }
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
ElMessageBox.confirm('确定删除该用户商品吗?', '删除确认', { type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await deleteUserGoods({ id: goodsId.value })
|
||||
if (res?.data?.code === 200) { ElMessage.success('删除成功'); goBack() }
|
||||
else ElMessage.error(extractApiError(res?.data, '删除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
watch(goodsId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) { detail.value = null; loadDetail() }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.goods-detail-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
margin-bottom: 0;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.profile-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d6eaff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.id-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.id-row .label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.id-row .value {
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
padding: 10px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.note-value {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.selector-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="tagQueryParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="tagQueryParams.key" placeholder="搜索标签名称" clearable style="width: 180px" @keyup.enter="fetchTagList" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchTagList">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetTagQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAddTag">
|
||||
<el-icon><Plus /></el-icon>新增标签
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchTagList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="tagLoading"
|
||||
:data="tagList"
|
||||
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="index" label="排序" width="100" sortable />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleEditTag(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDeleteTag(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无标签数据" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="tagQueryParams.page"
|
||||
v-model:page-size="tagQueryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="tagTotal"
|
||||
@size-change="handleTagSizeChange"
|
||||
@current-change="handleTagCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分组标签表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="tagDialogVisible"
|
||||
:title="tagDialogType === 'add' ? '新增分组标签' : '编辑分组标签'"
|
||||
width="500px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="tagFormRef"
|
||||
:model="tagForm"
|
||||
:rules="tagRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="tagForm.name" placeholder="请输入标签名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序索引" prop="index">
|
||||
<el-input-number v-model="tagForm.index" :min="0" :max="9999" placeholder="数值越小越靠前" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="tagDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitTagForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProductGroupTagList,
|
||||
createProductGroupTag,
|
||||
updateProductGroupTag,
|
||||
deleteProductGroupTag
|
||||
} from '@/api/admin/product'
|
||||
|
||||
const tagQueryParams = reactive({ page: 1, count: 10, key: '' })
|
||||
const tagLoading = ref(false)
|
||||
const tagList = ref([])
|
||||
const tagTotal = ref(0)
|
||||
const tagDialogVisible = ref(false)
|
||||
const tagDialogType = ref('add')
|
||||
const tagFormRef = ref(null)
|
||||
const tagForm = reactive({ id: undefined, name: '', index: 0 })
|
||||
const tagRules = { name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] }
|
||||
|
||||
const fetchTagList = async () => {
|
||||
tagLoading.value = true
|
||||
try {
|
||||
const params = { ...tagQueryParams }
|
||||
if (!params.key) delete params.key
|
||||
const res = await getProductGroupTagList(params)
|
||||
if (res.data.code === 200) {
|
||||
const data = res.data.data
|
||||
if (Array.isArray(data)) {
|
||||
tagList.value = data
|
||||
tagTotal.value = data.length
|
||||
} else if (data && data.list) {
|
||||
tagList.value = data.list
|
||||
tagTotal.value = data.all_count || data.total || data.list.length
|
||||
} else if (data && data.data) {
|
||||
tagList.value = data.data
|
||||
tagTotal.value = data.total || data.data.length
|
||||
} else {
|
||||
tagList.value = []
|
||||
tagTotal.value = 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
ElMessage.error('获取标签列表失败')
|
||||
} finally {
|
||||
tagLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetTagQuery = () => { tagQueryParams.key = ''; tagQueryParams.page = 1; fetchTagList() }
|
||||
const handleTagSizeChange = (size) => { tagQueryParams.count = size; fetchTagList() }
|
||||
const handleTagCurrentChange = (page) => { tagQueryParams.page = page; fetchTagList() }
|
||||
|
||||
const handleAddTag = () => {
|
||||
tagDialogType.value = 'add'
|
||||
tagDialogVisible.value = true
|
||||
Object.assign(tagForm, { id: undefined, name: '', index: 0 })
|
||||
tagFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleEditTag = (row) => {
|
||||
tagDialogType.value = 'edit'
|
||||
tagDialogVisible.value = true
|
||||
Object.assign(tagForm, { id: row.id, name: row.name, index: row.index || 0 })
|
||||
}
|
||||
|
||||
const handleDeleteTag = (row) => {
|
||||
ElMessageBox.confirm(`确认删除标签 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteProductGroupTag({ id: row.id })
|
||||
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchTagList() }
|
||||
} catch (error) { ElMessage.error('删除失败') }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const submitTagForm = () => {
|
||||
tagFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
let res
|
||||
if (tagDialogType.value === 'add') {
|
||||
res = await createProductGroupTag({ name: tagForm.name, index: tagForm.index })
|
||||
} else {
|
||||
res = await updateProductGroupTag({ id: tagForm.id, name: tagForm.name, index: tagForm.index })
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(tagDialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
tagDialogVisible.value = false
|
||||
fetchTagList()
|
||||
}
|
||||
} catch (error) { ElMessage.error('操作失败') }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ fetchTagList })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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 { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0; }
|
||||
.search-form :deep(.el-form-item) { margin-bottom: 0; margin-right: 0; }
|
||||
.action-bar { display: flex; gap: 12px; flex-shrink: 0; flex-wrap: wrap; align-items: center; }
|
||||
.table-section { padding: 0; }
|
||||
.action-buttons { display: flex; gap: 4px; align-items: center; flex-wrap: nowrap; }
|
||||
.action-buttons .el-button { padding: 4px 8px; }
|
||||
.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; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -184,25 +184,57 @@ import {
|
||||
User, Edit, Document, Timer, EditPen, Message,
|
||||
Phone, OfficeBuilding, UploadFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/store/userStore.js'
|
||||
|
||||
// 是否处于编辑模式
|
||||
const isEditing = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 从localStorage或store获取用户信息
|
||||
const getSavedUserInfo = () => {
|
||||
const savedInfo = userStore.userInfo
|
||||
if (savedInfo && Object.keys(savedInfo).length > 0) {
|
||||
return {
|
||||
username: savedInfo.user_name || '',
|
||||
realName: savedInfo.real_name?.Name || savedInfo.user_name || '',
|
||||
email: savedInfo.email || '',
|
||||
phone: savedInfo.phone || '',
|
||||
department: savedInfo.admin_group?.name || '',
|
||||
position: savedInfo.is_admin ? '管理员' : '普通用户',
|
||||
role: savedInfo.admin_group?.name || '普通用户',
|
||||
createTime: savedInfo.created_at || '',
|
||||
lastLogin: savedInfo.created_at || '',
|
||||
bio: savedInfo.admin_group?.note || '',
|
||||
avatar: savedInfo.cover || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
||||
sex: savedInfo.sex || '',
|
||||
age: savedInfo.age || '',
|
||||
userId: savedInfo.user_id || '',
|
||||
userGroup: savedInfo.user_group?.Name || ''
|
||||
}
|
||||
}
|
||||
return {
|
||||
username: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
department: '',
|
||||
position: '',
|
||||
role: '',
|
||||
createTime: '',
|
||||
lastLogin: '',
|
||||
bio: '',
|
||||
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
||||
sex: '',
|
||||
age: '',
|
||||
userId: '',
|
||||
userGroup: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息数据
|
||||
const userInfo = reactive({
|
||||
username: 'admin',
|
||||
realName: '管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
department: '技术部',
|
||||
position: '系统管理员',
|
||||
role: '超级管理员',
|
||||
createTime: '2023-01-01 00:00:00',
|
||||
lastLogin: '2023-06-15 10:30:45',
|
||||
bio: '系统管理员,负责系统的日常维护和管理工作。拥有丰富的系统管理经验,精通Linux服务器配置和维护,熟悉网络安全,对系统性能优化有独到见解。',
|
||||
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
|
||||
})
|
||||
const userInfo = reactive(getSavedUserInfo())
|
||||
|
||||
// 表单数据
|
||||
const userForm = reactive({...userInfo})
|
||||
@@ -296,9 +328,9 @@ const handleAvatarSuccess = (res) => {
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 实际项目中,应该从后端获取用户信息并更新userInfo
|
||||
// 从store获取最新用户信息
|
||||
const savedInfo = getSavedUserInfo()
|
||||
Object.assign(userInfo, savedInfo)
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户信息失败')
|
||||
console.error(error)
|
||||
|
||||
@@ -349,33 +349,6 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="menu-manage-container">
|
||||
<el-card class="main-container" shadow="never">
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="queryParams.key" placeholder="菜单名称/路径" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单">
|
||||
<el-select v-model="queryParams.parent_id" placeholder="全部" clearable style="width: 160px">
|
||||
<el-option v-for="m in parentMenuOptions" :key="m.id" :label="m.title" :value="m.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-radio-group v-model="viewMode" size="default" @change="handleViewModeChange">
|
||||
<el-radio-button value="list">
|
||||
<el-icon style="vertical-align: -2px;"><Grid /></el-icon> 列表视图
|
||||
</el-radio-button>
|
||||
<el-radio-button value="tree">
|
||||
<el-icon style="vertical-align: -2px;"><Connection /></el-icon> 树状视图
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button v-if="viewMode === 'list'" type="primary" @click="handleAdd(null)">
|
||||
<el-icon><Plus /></el-icon>新增顶级菜单
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<div v-if="viewMode === 'list'" class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="menuList"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="菜单名称" min-width="180" />
|
||||
<el-table-column prop="path" label="路径" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info">{{ row.path || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.icon" style="display: flex; align-items: center; gap: 6px;">
|
||||
<el-icon><component :is="row.icon" /></el-icon>
|
||||
<span>{{ row.icon }}</span>
|
||||
</div>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="parentId" label="父级ID" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.parentId ?? '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</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="handleAdd(row)">添加子菜单</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无菜单数据" :image-size="80" />
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<!-- 树状视图 -->
|
||||
<div v-if="viewMode === 'tree'" v-loading="myPermLoading" class="tree-section">
|
||||
<el-tree
|
||||
v-if="myPermTree.length > 0"
|
||||
:data="myPermTree"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="perm-tree-node">
|
||||
<el-icon v-if="data.icon" style="margin-right: 6px; flex-shrink: 0;"><component :is="data.icon" /></el-icon>
|
||||
<span class="perm-tree-title">{{ data.title }}</span>
|
||||
<el-tag size="small" type="info" style="margin-left: 8px;">{{ data.path || '-' }}</el-tag>
|
||||
<el-tag :type="data.enable ? 'success' : 'danger'" size="small" style="margin-left: 6px;">
|
||||
{{ data.enable ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
<el-empty v-if="!myPermLoading && myPermTree.length === 0" description="暂无菜单权限数据" :image-size="80" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 菜单表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增菜单' : '编辑菜单'"
|
||||
width="550px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="formRef" :model="menuForm" :rules="menuRules" label-width="100px">
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input v-model="menuForm.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单路径" prop="path">
|
||||
<MenuPathSelector v-model="menuForm.path" />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单图标" prop="icon">
|
||||
<IconSelector v-model="menuForm.icon" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单">
|
||||
<el-select v-model="menuForm.parent_id" placeholder="无(顶级菜单)" clearable style="width: 100%">
|
||||
<el-option label="无(顶级菜单)" :value="0" />
|
||||
<el-option v-for="m in parentMenuOptions" :key="m.id" :label="m.title" :value="m.id" />
|
||||
</el-select>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Refresh, Grid, Connection } from '@element-plus/icons-vue'
|
||||
import { getWebRoutsList, addWebRouts, updateWebRouts, deleteWebRouts, getMyWebRoutsPermission } from '@/api/admin/webRouts'
|
||||
import { formatDate as formatDateTool } from '@/utils/tool'
|
||||
import IconSelector from '@/components/admin/IconSelector.vue'
|
||||
import MenuPathSelector from '@/components/admin/MenuPathSelector.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const menuList = ref([])
|
||||
const parentMenuOptions = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref(null)
|
||||
const viewMode = ref('list')
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
count: 10,
|
||||
key: '',
|
||||
parent_id: null
|
||||
})
|
||||
|
||||
const menuForm = reactive({
|
||||
id: undefined,
|
||||
title: '',
|
||||
path: '',
|
||||
icon: '',
|
||||
parent_id: 0
|
||||
})
|
||||
|
||||
const menuRules = {
|
||||
title: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
path: [{ required: true, message: '请输入菜单路径', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => formatDateTool(dateStr)
|
||||
|
||||
const flattenForParent = (list) => {
|
||||
const result = []
|
||||
for (const item of list) {
|
||||
result.push(item)
|
||||
if (item.children?.length) {
|
||||
result.push(...flattenForParent(item.children))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const fetchMenuList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
Object.keys(queryParams).forEach(key => {
|
||||
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
|
||||
params[key] = queryParams[key]
|
||||
}
|
||||
})
|
||||
const res = await getWebRoutsList(params)
|
||||
if (res.data.code === 200) {
|
||||
menuList.value = res.data.data?.list || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
parentMenuOptions.value = flattenForParent(menuList.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败:', error)
|
||||
ElMessage.error('获取菜单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchMenuList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.parent_id = null
|
||||
queryParams.page = 1
|
||||
fetchMenuList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchMenuList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchMenuList()
|
||||
}
|
||||
|
||||
const handleAdd = (parentRow) => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(menuForm, {
|
||||
id: undefined,
|
||||
title: '',
|
||||
path: '',
|
||||
icon: '',
|
||||
parent_id: parentRow?.id || 0
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
Object.assign(menuForm, {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
path: row.path,
|
||||
icon: row.icon,
|
||||
parent_id: row.parentId || 0
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除菜单「${row.title}」吗?删除后其子菜单也将受到影响。`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteWebRouts({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchMenuList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
const submitData = {
|
||||
title: menuForm.title,
|
||||
path: menuForm.path,
|
||||
icon: menuForm.icon
|
||||
}
|
||||
if (menuForm.parent_id) {
|
||||
submitData.parent_id = menuForm.parent_id
|
||||
}
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addWebRouts(submitData)
|
||||
} else {
|
||||
submitData.id = menuForm.id
|
||||
res = await updateWebRouts(submitData)
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchMenuList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const myPermLoading = ref(false)
|
||||
const myPermTree = ref([])
|
||||
|
||||
const fetchMyPermission = async () => {
|
||||
myPermLoading.value = true
|
||||
try {
|
||||
const res = await getMyWebRoutsPermission()
|
||||
if (res.data.code === 200) {
|
||||
myPermTree.value = res.data.data || []
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取我的菜单权限失败:', error)
|
||||
ElMessage.error('获取我的菜单权限失败')
|
||||
} finally {
|
||||
myPermLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewModeChange = (mode) => {
|
||||
if (mode === 'list') {
|
||||
fetchMenuList()
|
||||
} else {
|
||||
fetchMyPermission()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (viewMode.value === 'list') {
|
||||
fetchMenuList()
|
||||
} else {
|
||||
fetchMyPermission()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchMenuList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-manage-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: flex-start; padding: 16px 20px; gap: 20px; 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 { display: flex; gap: 12px; flex-shrink: 0; }
|
||||
.table-section { padding: 0; }
|
||||
.action-buttons { display: flex; gap: 8px; align-items: center; }
|
||||
.text-muted { color: #c0c4cc; }
|
||||
.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; }
|
||||
:deep(.el-card__body) { padding: 0; }
|
||||
: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; }
|
||||
.tree-section { padding: 16px 20px; min-height: 300px; }
|
||||
.perm-tree-node { display: flex; align-items: center; padding: 2px 0; width: 100%; }
|
||||
.perm-tree-title { font-size: 13px; font-weight: 500; }
|
||||
.tree-section :deep(.el-tree-node__content) { height: 38px; }
|
||||
.tree-section :deep(.el-tree-node__content:hover) { background-color: #f5f7fa; }
|
||||
.action-bar { display: flex; gap: 12px; flex-shrink: 0; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,610 @@
|
||||
<template>
|
||||
<div class="menu-permission-container">
|
||||
<el-card class="main-container" shadow="never">
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 130px" @change="handleOwnerTypeChange">
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="管理员组" value="group" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
|
||||
<div class="selector-inline">
|
||||
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
|
||||
{{ queryUserName || `用户 #${queryParams.user_id}` }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openUserSelector('query')" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'">
|
||||
<div class="selector-inline">
|
||||
<el-tag v-if="queryParams.admin_group_id" type="success" closable @close="clearQueryGroup" style="margin-right: 8px;">
|
||||
{{ queryGroupName || `组 #${queryParams.admin_group_id}` }}
|
||||
</el-tag>
|
||||
<el-button type="success" plain @click="openGroupSelector('query')" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.admin_group_id ? '重新选择' : '选择管理员组' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</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="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="permissionList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="所属类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOwnerType(row) === 'user' ? 'primary' : 'success'" size="small">
|
||||
{{ getOwnerType(row) === 'user' ? '用户' : '管理员组' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属对象" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.userId">用户 #{{ row.userId }}</span>
|
||||
<span v-else-if="row.adminGroupId">管理员组 #{{ row.adminGroupId }}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="菜单" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.webRouts">
|
||||
<span style="font-weight: 500;">{{ row.webRouts.title }}</span>
|
||||
<el-tag size="small" type="info" style="margin-left: 8px;">{{ row.webRouts.path }}</el-tag>
|
||||
</div>
|
||||
<span v-else class="text-muted">菜单ID: {{ row.webRoutsId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enable ? 'success' : 'danger'" size="small">
|
||||
{{ row.enable ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleToggleEnable(row)">
|
||||
{{ row.enable ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无权限数据" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserListSelector
|
||||
v-model="userSelectorVisible"
|
||||
:current-user-id="selectorTarget === 'query' ? queryParams.user_id : permForm.user_id"
|
||||
@confirm="handleUserConfirm"
|
||||
/>
|
||||
|
||||
<!-- 管理员组选择弹窗 -->
|
||||
<UserGroupSelector
|
||||
v-model="groupSelectorVisible"
|
||||
:current-group-id="selectorTarget === 'query' ? queryParams.admin_group_id : permForm.admin_group_id"
|
||||
admin-group
|
||||
@confirm="handleGroupConfirm"
|
||||
/>
|
||||
|
||||
<!-- 菜单选择弹窗 -->
|
||||
<el-dialog v-model="menuSelectorVisible" title="选择菜单" width="700px" append-to-body @open="openMenuSelector">
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
||||
<el-input v-model="menuSearchKey" placeholder="搜索菜单名称或路径" clearable style="flex: 1;" @keyup.enter="fetchMenuSelectorList">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="fetchMenuSelectorList">搜索</el-button>
|
||||
<el-button type="success" @click="fetchMenuSelectorList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="menuSelectorLoading"
|
||||
:data="menuSelectorFlatList"
|
||||
highlight-current-row
|
||||
@current-change="handleMenuCurrentChange"
|
||||
:height="400"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="title" label="菜单名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ paddingLeft: (row._level || 0) * 20 + 'px' }">
|
||||
<span v-if="row._level" style="color: #c0c4cc; margin-right: 4px;">└</span>
|
||||
{{ row.title }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info">{{ row.path || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="row.icon"><component :is="row.icon" /></el-icon>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="menuSelectorVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!menuSelectorTemp" @click="confirmMenuSelect">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配/编辑权限对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '分配菜单权限' : '编辑菜单权限'"
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="formRef" :model="permForm" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="所属类型" prop="owner_type">
|
||||
<el-select v-model="permForm.owner_type" placeholder="请选择" style="width: 100%" :disabled="dialogType === 'edit'" @change="handleFormOwnerTypeChange">
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="管理员组" value="group" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" prop="user_id" v-if="permForm.owner_type === 'user'">
|
||||
<div class="selector-inline" style="width: 100%;">
|
||||
<el-tag v-if="permForm.user_id" type="primary" closable @close="permForm.user_id = null" style="margin-right: 8px;">
|
||||
{{ formUserName || `用户 #${permForm.user_id}` }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openUserSelector('form')">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ permForm.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" prop="admin_group_id" v-if="permForm.owner_type === 'group'">
|
||||
<div class="selector-inline" style="width: 100%;">
|
||||
<el-tag v-if="permForm.admin_group_id" type="success" closable @close="permForm.admin_group_id = null" style="margin-right: 8px;">
|
||||
{{ formGroupName || `组 #${permForm.admin_group_id}` }}
|
||||
</el-tag>
|
||||
<el-button type="success" plain @click="openGroupSelector('form')">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ permForm.admin_group_id ? '重新选择' : '选择管理员组' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单" prop="web_routs_id">
|
||||
<div class="selector-inline" style="width: 100%;">
|
||||
<el-tag v-if="permForm.web_routs_id" closable @close="clearFormMenu" style="margin-right: 8px;">
|
||||
{{ formMenuName || `菜单 #${permForm.web_routs_id}` }}
|
||||
</el-tag>
|
||||
<el-button plain @click="menuSelectorVisible = true">
|
||||
<el-icon><Menu /></el-icon>
|
||||
{{ permForm.web_routs_id ? '重新选择' : '选择菜单' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="permForm.enable" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Refresh, User, Menu } from '@element-plus/icons-vue'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
|
||||
import {
|
||||
getWebRoutsList,
|
||||
getWebRoutsPermissionList,
|
||||
addWebRoutsPermission,
|
||||
updateWebRoutsPermission,
|
||||
deleteWebRoutsPermission
|
||||
} from '@/api/admin/webRouts'
|
||||
import { formatDate as formatDateTool } from '@/utils/tool'
|
||||
|
||||
const loading = ref(false)
|
||||
const permissionList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref(null)
|
||||
|
||||
const userSelectorVisible = ref(false)
|
||||
const groupSelectorVisible = ref(false)
|
||||
const menuSelectorVisible = ref(false)
|
||||
const menuSelectorLoading = ref(false)
|
||||
const menuSelectorTemp = ref(null)
|
||||
const menuSearchKey = ref('')
|
||||
const selectorTarget = ref('query')
|
||||
|
||||
const queryUserName = ref('')
|
||||
const queryGroupName = ref('')
|
||||
const formUserName = ref('')
|
||||
const formGroupName = ref('')
|
||||
const formMenuName = ref('')
|
||||
|
||||
const queryParams = reactive({
|
||||
owner_type: 'group',
|
||||
user_id: null,
|
||||
admin_group_id: null
|
||||
})
|
||||
|
||||
const permForm = reactive({
|
||||
id: undefined,
|
||||
web_routs_id: null,
|
||||
enable: true,
|
||||
owner_type: 'group',
|
||||
admin_group_id: null,
|
||||
user_id: null
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
owner_type: [{ required: true, message: '请选择所属类型', trigger: 'change' }],
|
||||
web_routs_id: [{ required: true, message: '请选择菜单', trigger: 'change' }],
|
||||
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||
admin_group_id: [{ required: true, message: '请选择管理员组', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => formatDateTool(dateStr)
|
||||
|
||||
const getOwnerType = (row) => {
|
||||
if (row.userId) return 'user'
|
||||
if (row.adminGroupId) return 'group'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const fetchPermissionList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
if (queryParams.owner_type) params.owner_type = queryParams.owner_type
|
||||
if (queryParams.owner_type === 'user' && queryParams.user_id) params.user_id = queryParams.user_id
|
||||
if (queryParams.owner_type === 'group' && queryParams.admin_group_id) params.admin_group_id = queryParams.admin_group_id
|
||||
const res = await getWebRoutsPermissionList(params)
|
||||
if (res.data.code === 200) {
|
||||
permissionList.value = res.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error)
|
||||
ElMessage.error('获取权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const flattenMenuTree = (list, level = 0) => {
|
||||
const result = []
|
||||
for (const item of list) {
|
||||
result.push({ ...item, _level: level, children: undefined })
|
||||
if (item.children?.length) {
|
||||
result.push(...flattenMenuTree(item.children, level + 1))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const menuSelectorFlatList = ref([])
|
||||
|
||||
const openMenuSelector = () => {
|
||||
menuSelectorTemp.value = null
|
||||
menuSearchKey.value = ''
|
||||
fetchMenuSelectorList()
|
||||
}
|
||||
|
||||
const fetchMenuSelectorList = async () => {
|
||||
menuSelectorLoading.value = true
|
||||
try {
|
||||
const params = { page: 1, count: 10 }
|
||||
if (menuSearchKey.value) params.key = menuSearchKey.value
|
||||
const res = await getWebRoutsList(params)
|
||||
if (res.data.code === 200) {
|
||||
const treeList = res.data.data?.list || []
|
||||
menuSelectorFlatList.value = flattenMenuTree(treeList)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败:', error)
|
||||
} finally {
|
||||
menuSelectorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuCurrentChange = (row) => {
|
||||
menuSelectorTemp.value = row
|
||||
}
|
||||
|
||||
const confirmMenuSelect = () => {
|
||||
if (!menuSelectorTemp.value) return
|
||||
permForm.web_routs_id = menuSelectorTemp.value.id
|
||||
formMenuName.value = `${menuSelectorTemp.value.title} (${menuSelectorTemp.value.path})`
|
||||
menuSelectorVisible.value = false
|
||||
menuSelectorTemp.value = null
|
||||
menuSearchKey.value = ''
|
||||
}
|
||||
|
||||
const clearFormMenu = () => {
|
||||
permForm.web_routs_id = null
|
||||
formMenuName.value = ''
|
||||
}
|
||||
|
||||
const handleOwnerTypeChange = () => {
|
||||
queryParams.user_id = null
|
||||
queryParams.admin_group_id = null
|
||||
queryUserName.value = ''
|
||||
queryGroupName.value = ''
|
||||
}
|
||||
|
||||
const handleFormOwnerTypeChange = () => {
|
||||
permForm.user_id = null
|
||||
permForm.admin_group_id = null
|
||||
formUserName.value = ''
|
||||
formGroupName.value = ''
|
||||
}
|
||||
|
||||
const canQuery = () => {
|
||||
if (queryParams.owner_type === 'user' && !queryParams.user_id) {
|
||||
ElMessage.warning('请先选择用户')
|
||||
return false
|
||||
}
|
||||
if (queryParams.owner_type === 'group' && !queryParams.admin_group_id) {
|
||||
ElMessage.warning('请先选择管理员组')
|
||||
return false
|
||||
}
|
||||
if (!queryParams.owner_type) {
|
||||
ElMessage.warning('请先选择类型')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
if (!canQuery()) return
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!canQuery()) return
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.owner_type = 'group'
|
||||
queryParams.user_id = null
|
||||
queryParams.admin_group_id = null
|
||||
queryUserName.value = ''
|
||||
queryGroupName.value = ''
|
||||
permissionList.value = []
|
||||
}
|
||||
|
||||
const clearQueryUser = () => {
|
||||
queryParams.user_id = null
|
||||
queryUserName.value = ''
|
||||
}
|
||||
|
||||
const clearQueryGroup = () => {
|
||||
queryParams.admin_group_id = null
|
||||
queryGroupName.value = ''
|
||||
}
|
||||
|
||||
const openUserSelector = (target) => {
|
||||
selectorTarget.value = target
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
const openGroupSelector = (target) => {
|
||||
selectorTarget.value = target
|
||||
groupSelectorVisible.value = true
|
||||
}
|
||||
|
||||
const handleUserConfirm = (user) => {
|
||||
const id = user.user_id || user.UserId || user.userId
|
||||
const name = user.user_name || user.UserName || user.userName || `用户 #${id}`
|
||||
if (selectorTarget.value === 'query') {
|
||||
queryParams.user_id = id
|
||||
queryUserName.value = name
|
||||
} else {
|
||||
permForm.user_id = id
|
||||
formUserName.value = name
|
||||
}
|
||||
}
|
||||
|
||||
const handleGroupConfirm = (group) => {
|
||||
const name = group.name || group.groupName || `组 #${group.id}`
|
||||
const id = group.id
|
||||
if (selectorTarget.value === 'query') {
|
||||
queryParams.admin_group_id = id
|
||||
queryGroupName.value = name
|
||||
} else {
|
||||
permForm.admin_group_id = id
|
||||
formGroupName.value = name
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(permForm, {
|
||||
id: undefined,
|
||||
web_routs_id: null,
|
||||
enable: true,
|
||||
owner_type: 'group',
|
||||
admin_group_id: null,
|
||||
user_id: null
|
||||
})
|
||||
formUserName.value = ''
|
||||
formGroupName.value = ''
|
||||
formMenuName.value = ''
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
const ownerType = row.userId ? 'user' : 'group'
|
||||
Object.assign(permForm, {
|
||||
id: row.id,
|
||||
web_routs_id: row.webRoutsId,
|
||||
enable: row.enable,
|
||||
owner_type: ownerType,
|
||||
admin_group_id: row.adminGroupId || null,
|
||||
user_id: row.userId || null
|
||||
})
|
||||
formUserName.value = row.userId ? `用户 #${row.userId}` : ''
|
||||
formGroupName.value = row.adminGroupId ? `组 #${row.adminGroupId}` : ''
|
||||
if (row.webRouts) {
|
||||
formMenuName.value = `${row.webRouts.title} (${row.webRouts.path})`
|
||||
} else {
|
||||
formMenuName.value = row.webRoutsId ? `菜单 #${row.webRoutsId}` : ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleEnable = async (row) => {
|
||||
const newEnable = !row.enable
|
||||
const action = newEnable ? '启用' : '禁用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}该菜单权限吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await updateWebRoutsPermission({
|
||||
id: row.id,
|
||||
enable: newEnable
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(`${action}成功`)
|
||||
fetchPermissionList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || `${action}失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.response?.data?.message || `${action}失败`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确认删除该菜单权限吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteWebRoutsPermission({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchPermissionList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
const submitData = {
|
||||
web_routs_id: permForm.web_routs_id,
|
||||
enable: permForm.enable,
|
||||
owner_type: permForm.owner_type
|
||||
}
|
||||
if (permForm.owner_type === 'user') {
|
||||
submitData.user_id = permForm.user_id
|
||||
} else {
|
||||
submitData.admin_group_id = permForm.admin_group_id
|
||||
}
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addWebRoutsPermission(submitData)
|
||||
} else {
|
||||
submitData.id = permForm.id
|
||||
res = await updateWebRoutsPermission(submitData)
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '分配成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchPermissionList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-permission-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: flex-start; padding: 16px 20px; gap: 20px; 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 { display: flex; gap: 12px; flex-shrink: 0; }
|
||||
.selector-inline { display: flex; align-items: center; }
|
||||
.table-section { padding: 0; }
|
||||
.action-buttons { display: flex; gap: 8px; align-items: center; }
|
||||
.text-muted { color: #c0c4cc; }
|
||||
.dialog-footer { display: flex; justify-content: flex-end; gap: 12px; }
|
||||
:deep(.el-card__body) { padding: 0; }
|
||||
: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; }
|
||||
</style>
|
||||
@@ -24,9 +24,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'">
|
||||
<el-select v-model="queryParams.admin_group_id" placeholder="请选择管理员组" clearable filterable style="width: 200px">
|
||||
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</el-select>
|
||||
<div class="selector-inline">
|
||||
<el-tag v-if="queryParams.admin_group_id" type="success" closable @close="clearQueryGroup" style="margin-right: 8px;">
|
||||
{{ getQueryGroupName() }}
|
||||
</el-tag>
|
||||
<el-button type="success" plain @click="openQueryGroupSelector" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.admin_group_id ? '重新选择' : '选择管理员组' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
@@ -115,14 +121,35 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<el-dialog
|
||||
<!-- 用户选择弹窗 - 使用UserListSelector组件 -->
|
||||
<UserListSelector
|
||||
v-model="userSelectorVisible"
|
||||
:current-user-id="selectorType === 'query' ? queryParams.user_id : permissionForm.user_id"
|
||||
@confirm="handleUserSelectorConfirm"
|
||||
/>
|
||||
|
||||
<!-- 管理员组选择弹窗 - 使用UserGroupSelector组件 -->
|
||||
<UserGroupSelector
|
||||
v-model="groupSelectorVisible"
|
||||
:current-group-id="selectorType === 'query' ? queryParams.admin_group_id : permissionForm.admin_group_id"
|
||||
admin-group
|
||||
@confirm="handleGroupSelectorConfirm"
|
||||
/>
|
||||
|
||||
<!-- 路径权限选择弹窗 -->
|
||||
<PermissionPathSelector
|
||||
v-model="permissionSelectorVisible"
|
||||
:current-permission-id="permissionForm.permission_id"
|
||||
@confirm="handlePermissionSelectorConfirm"
|
||||
/>
|
||||
|
||||
<!-- 旧的用户选择弹窗 - 已废弃 -->
|
||||
<!-- <el-dialog
|
||||
v-model="userSelectorVisibleOld"
|
||||
title="选择用户"
|
||||
width="800px"
|
||||
class="user-selector-dialog"
|
||||
>
|
||||
<!-- 搜索栏 -->
|
||||
<div class="selector-search">
|
||||
<el-input
|
||||
v-model="userSearchParams.key"
|
||||
@@ -142,7 +169,6 @@
|
||||
<el-button @click="resetUserSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table
|
||||
v-loading="userSelectorLoading"
|
||||
:data="userSelectorList"
|
||||
@@ -164,7 +190,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="userSearchParams.page"
|
||||
v-model:page-size="userSearchParams.count"
|
||||
@@ -178,12 +204,12 @@
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="userSelectorVisible = false">取消</el-button>
|
||||
<el-button @click="userSelectorVisibleOld = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
|
||||
确定选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog> -->
|
||||
<!-- 分配权限对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -204,45 +230,80 @@
|
||||
<div class="form-tip">如果是 user 则填写 user_id,如果是 group 则填写 admin_group_id</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" prop="user_id" v-if="permissionForm.owner_type === 'user'" >
|
||||
<div class="user_selector-inline">
|
||||
<el-tag v-if="permissionForm.user_id" type="primary" closable @close="clearFormUser" style="margin-right: 8px;">
|
||||
{{ getFormUserName() }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openFormUserSelector" size="default" :disabled="permissionForm.user_id">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ permissionForm.user_id ? '重新选择' : '选择用户' }}
|
||||
<div class="recommend-user-selector">
|
||||
<el-input
|
||||
:model-value="getFormUserName()"
|
||||
placeholder="点击选择用户"
|
||||
readonly
|
||||
@click="openFormUserSelector"
|
||||
:disabled="!!permissionForm.id"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="openFormUserSelector" :disabled="!!permissionForm.id">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
v-if="permissionForm.user_id && !permissionForm.id"
|
||||
type="danger"
|
||||
link
|
||||
@click="clearFormUser"
|
||||
class="clear-btn"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" prop="admin_group_id" v-if="permissionForm.owner_type === 'group'">
|
||||
<el-select v-model="permissionForm.admin_group_id" placeholder="请选择管理员组" filterable style="width: 100%">
|
||||
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</el-select>
|
||||
<div class="recommend-user-selector">
|
||||
<el-input
|
||||
:model-value="getFormGroupName()"
|
||||
placeholder="点击选择管理员组"
|
||||
readonly
|
||||
@click="openFormGroupSelector"
|
||||
:disabled="!!permissionForm.id"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="openFormGroupSelector" :disabled="!!permissionForm.id">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
v-if="permissionForm.admin_group_id && !permissionForm.id"
|
||||
type="danger"
|
||||
link
|
||||
@click="clearFormGroup"
|
||||
class="clear-btn"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="路径权限" prop="permission_id">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-select
|
||||
v-model="permissionForm.permission_id"
|
||||
placeholder="请选择路径权限"
|
||||
filterable
|
||||
style="flex: 1"
|
||||
:loading="permissionLoading"
|
||||
<div class="recommend-user-selector">
|
||||
<el-input
|
||||
:model-value="getFormPermissionName()"
|
||||
placeholder="点击选择路径权限"
|
||||
readonly
|
||||
@click="openPermissionSelector"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in permissionOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
<el-tag v-if="item.method" :type="getMethodTag(item.method)" size="small" style="margin-right: 8px;">{{ item.method }}</el-tag>
|
||||
{{ item.path }}
|
||||
</span>
|
||||
<span style="color: #999; font-size: 12px; margin-left: 12px;">{{ item.note || item.name || `ID: ${item.id}` }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button @click="fetchPermissionList" :loading="permissionLoading" :icon="Refresh">刷新</el-button>
|
||||
<template #append>
|
||||
<el-button @click="openPermissionSelector">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
v-if="permissionForm.permission_id"
|
||||
type="danger"
|
||||
link
|
||||
@click="clearFormPermission"
|
||||
class="clear-btn"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="form-tip">共 {{ permissionOptions.length }} 个路径权限可选</div>
|
||||
</el-form-item>
|
||||
@@ -283,6 +344,9 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, User } from '@element-plus/icons-vue'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
|
||||
import PermissionPathSelector from '@/components/admin/PermissionPathSelector.vue'
|
||||
import {
|
||||
getPermissionListByAdmin,
|
||||
addPermissionAdmin,
|
||||
@@ -297,6 +361,8 @@ import { formatDate ,timeToTimestamp} from '@/utils/tool'
|
||||
|
||||
const selectorType = ref('query')
|
||||
const userSelectorVisible = ref(false)
|
||||
const groupSelectorVisible = ref(false)
|
||||
const permissionSelectorVisible = ref(false)
|
||||
const userSelectorList = ref([])
|
||||
const userSelectorTotal = ref(0)
|
||||
const userSearchParams = reactive({
|
||||
@@ -307,6 +373,8 @@ const userSearchParams = reactive({
|
||||
const selectedUserTemp = ref(null)
|
||||
const userSelectorLoading = ref(false)
|
||||
const UserOptions = ref([])
|
||||
const GroupOptions = ref([])
|
||||
const selectedPermission = ref(null)
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
owner_type: '',
|
||||
@@ -324,16 +392,125 @@ const getQueryUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
|
||||
}
|
||||
// 清除查询管理员组
|
||||
const clearQueryGroup = () => {
|
||||
queryParams.admin_group_id = undefined
|
||||
}
|
||||
// 获取查询管理员组名称
|
||||
const getQueryGroupName = () => {
|
||||
const group = GroupOptions.value.find(g => g.id === queryParams.admin_group_id) ||
|
||||
adminGroupOptions.value.find(g => g.id === queryParams.admin_group_id)
|
||||
return group ? `${group.name} (ID: ${group.id})` : `管理员组ID: ${queryParams.admin_group_id}`
|
||||
}
|
||||
// 打开查询管理员组选择器
|
||||
const openQueryGroupSelector = () => {
|
||||
selectorType.value = 'query'
|
||||
groupSelectorVisible.value = true
|
||||
}
|
||||
// 表单:清除用户
|
||||
const clearFormUser = () => {
|
||||
permissionForm.user_id = undefined
|
||||
}
|
||||
// 表单:获取显示名称
|
||||
const getFormUserName = () => {
|
||||
if (!permissionForm.user_id) return ''
|
||||
const user = UserOptions.value.find(u => u.UserId === permissionForm.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${permissionForm.user_id}`
|
||||
}
|
||||
// 确认用户选择
|
||||
|
||||
// 表单:获取管理员组显示名称
|
||||
const getFormGroupName = () => {
|
||||
if (!permissionForm.admin_group_id) return ''
|
||||
const group = GroupOptions.value.find(g => g.id === permissionForm.admin_group_id)
|
||||
return group ? `${group.name} (ID: ${group.id})` : `管理员组ID: ${permissionForm.admin_group_id}`
|
||||
}
|
||||
|
||||
// 表单:获取路径权限显示名称
|
||||
const getFormPermissionName = () => {
|
||||
if (!permissionForm.permission_id) return ''
|
||||
if (selectedPermission.value && selectedPermission.value.id === permissionForm.permission_id) {
|
||||
const p = selectedPermission.value
|
||||
return `${p.method || ''} ${p.path}${p.name ? ' - ' + p.name : ''}`
|
||||
}
|
||||
const perm = permissionOptions.value.find(p => p.id === permissionForm.permission_id)
|
||||
return perm ? `${perm.method || ''} ${perm.path}${perm.name ? ' - ' + perm.name : ''}` : `权限ID: ${permissionForm.permission_id}`
|
||||
}
|
||||
|
||||
// 清除表单管理员组
|
||||
const clearFormGroup = () => {
|
||||
permissionForm.admin_group_id = undefined
|
||||
}
|
||||
|
||||
// 清除表单路径权限
|
||||
const clearFormPermission = () => {
|
||||
permissionForm.permission_id = undefined
|
||||
selectedPermission.value = null
|
||||
}
|
||||
|
||||
// 打开管理员组选择器
|
||||
const openFormGroupSelector = () => {
|
||||
selectorType.value = 'form'
|
||||
groupSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 打开路径权限选择器
|
||||
const openPermissionSelector = () => {
|
||||
permissionSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 管理员组选择确认
|
||||
const handleGroupSelectorConfirm = (group) => {
|
||||
if (group) {
|
||||
const groupId = group.id || group.Id
|
||||
const groupName = group.name || group.Name
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
queryParams.admin_group_id = groupId
|
||||
if (!GroupOptions.value.find(g => g.id === groupId)) {
|
||||
GroupOptions.value.push({ id: groupId, name: groupName })
|
||||
}
|
||||
fetchAdminPermissionList()
|
||||
} else {
|
||||
permissionForm.admin_group_id = groupId
|
||||
if (!GroupOptions.value.find(g => g.id === groupId)) {
|
||||
GroupOptions.value.push({ id: groupId, name: groupName })
|
||||
}
|
||||
}
|
||||
}
|
||||
groupSelectorVisible.value = false
|
||||
}
|
||||
|
||||
// 路径权限选择确认
|
||||
const handlePermissionSelectorConfirm = (permission) => {
|
||||
if (permission) {
|
||||
permissionForm.permission_id = permission.id
|
||||
selectedPermission.value = permission
|
||||
}
|
||||
permissionSelectorVisible.value = false
|
||||
}
|
||||
// UserListSelector 组件确认回调
|
||||
const handleUserSelectorConfirm = (user) => {
|
||||
if (user) {
|
||||
const userId = user.user_id || user.UserId
|
||||
const userName = user.user_name || user.UserName
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
queryParams.user_id = userId
|
||||
if (!UserOptions.value.find(u => u.UserId === userId)) {
|
||||
UserOptions.value.push({ UserId: userId, UserName: userName })
|
||||
}
|
||||
fetchAdminPermissionList()
|
||||
} else if (selectorType.value === 'form') {
|
||||
permissionForm.user_id = userId
|
||||
if (!UserOptions.value.find(u => u.UserId === userId)) {
|
||||
UserOptions.value.push({ UserId: userId, UserName: userName })
|
||||
}
|
||||
}
|
||||
}
|
||||
userSelectorVisible.value = false
|
||||
}
|
||||
|
||||
// 确认用户选择(旧方法,保留兼容)
|
||||
const confirmUserSelection = () => {
|
||||
if (!selectedUserTemp.value) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
@@ -684,7 +861,7 @@ const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
@@ -700,7 +877,7 @@ const fetchAdminGroupList = async () => {
|
||||
try {
|
||||
const res = await getAdminGroupList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
adminGroupOptions.value = res.data.data?.data || []
|
||||
@@ -716,7 +893,7 @@ const fetchPermissionList = async () => {
|
||||
try {
|
||||
const res = await getPermissionList({
|
||||
page: 1,
|
||||
count: 10000
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
permissionOptions.value = res.data.data?.list || []
|
||||
@@ -837,4 +1014,34 @@ onMounted(() => {
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 推介人选择器样式 - 与UserList.vue保持一致 */
|
||||
.recommend-user-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recommend-user-selector .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recommend-user-selector .clear-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selector-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user_selector-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,600 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="配置组">
|
||||
<el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery">
|
||||
<el-option
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词筛选">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||
</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="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="settingList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="150" />
|
||||
<el-table-column prop="value" label="值" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'bool'">{{ row.value ? '是' : '否' }}</span>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeColor(row.type)">
|
||||
{{ row.type || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="settingGroupID" label="配置组" width="150" />
|
||||
<el-table-column label="是否开放" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.open"
|
||||
@change="handleToggleOpen(row)"
|
||||
:disabled="toggleLoading === row.id"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</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="dialogTitle"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="settingFormRef"
|
||||
:model="settingForm"
|
||||
:rules="settingRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="配置组" prop="settingGroupID">
|
||||
<el-select v-model="settingForm.settingGroupID" placeholder="请选择配置组" style="width: 100%">
|
||||
<el-option
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="settingForm.name" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="settingForm.type" placeholder="请选择类型" style="width: 100%" @change="handleTypeChange">
|
||||
<el-option label="字符串 (string)" value="string" />
|
||||
<el-option label="整数 (int)" value="int" />
|
||||
<el-option label="浮点数 (float)" value="float" />
|
||||
<el-option label="布尔值 (bool)" value="bool" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="value">
|
||||
<el-input
|
||||
v-if="settingForm.type === 'string'"
|
||||
v-model="settingForm.value"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="settingForm.type === 'int'"
|
||||
v-model="settingForm.value"
|
||||
:controls="false"
|
||||
placeholder="请输入整数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="settingForm.type === 'float'"
|
||||
v-model="settingForm.value"
|
||||
:controls="false"
|
||||
:precision="2"
|
||||
placeholder="请输入浮点数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="settingForm.type === 'bool'"
|
||||
v-model="settingForm.value"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="settingForm.value"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开放访问">
|
||||
<el-switch v-model="settingForm.open" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||||
开启后允许公开访问
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input
|
||||
v-model="settingForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSettingList,
|
||||
getSettingInfo,
|
||||
createSetting,
|
||||
updateSetting,
|
||||
setSettingOpen,
|
||||
deleteSetting
|
||||
} from '@/api/admin/setting'
|
||||
import { getSettingGroupList } from '@/api/admin/setting'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
group_id: undefined,
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 配置表单
|
||||
const settingForm = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'string',
|
||||
settingGroupID: undefined,
|
||||
open: false,
|
||||
note: ''
|
||||
})
|
||||
|
||||
const settingRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置名称', trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置值', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||
],
|
||||
settingGroupID: [
|
||||
{ required: true, message: '请选择配置组', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const settingList = ref([])
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增配置')
|
||||
const settingFormRef = ref(null)
|
||||
const toggleLoading = ref(null)
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'string': 'primary',
|
||||
'int': 'success',
|
||||
'float': 'warning',
|
||||
'bool': 'info'
|
||||
}
|
||||
return colorMap[type] || ''
|
||||
}
|
||||
|
||||
// 获取配置组列表
|
||||
const fetchGroupList = async () => {
|
||||
try {
|
||||
const res = await getSettingGroupList({ page: 1, count: 1000 })
|
||||
if (res.data.code === 200) {
|
||||
groupList.value = res.data.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置列表
|
||||
const fetchSettingList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
if (!params.group_id) {
|
||||
delete params.group_id
|
||||
}
|
||||
const res = await getSettingList(params)
|
||||
console.log('配置列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
settingList.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 handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.group_id = undefined
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 类型变化
|
||||
const handleTypeChange = (type) => {
|
||||
// 根据类型重置值
|
||||
if (type === 'bool') {
|
||||
settingForm.value = false
|
||||
} else if (type === 'int' || type === 'float') {
|
||||
settingForm.value = 0
|
||||
} else {
|
||||
settingForm.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 新增配置
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增配置'
|
||||
Object.assign(settingForm, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'string',
|
||||
setting_group_id: undefined,
|
||||
open: false,
|
||||
note: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑配置'
|
||||
try {
|
||||
const res = await getSettingInfo({ id: row.id })
|
||||
console.log('配置详情数据:', res)
|
||||
if (res.data.code === 200) {
|
||||
const data = res.data.data
|
||||
Object.assign(settingForm, {
|
||||
id: data.id,
|
||||
name: data.name || '',
|
||||
value: data.value,
|
||||
type: data.type || 'string',
|
||||
settingGroupID: data.settingGroupID,
|
||||
open: data.open || false,
|
||||
note: data.note || ''
|
||||
})
|
||||
console.log('配置详情数据:', settingForm)
|
||||
// 根据类型转换值
|
||||
if (data.type === 'bool') {
|
||||
settingForm.value = data.value === true || data.value === 'true' || data.value === 1
|
||||
} else if (data.type === 'int') {
|
||||
settingForm.value = parseInt(data.value) || 0
|
||||
} else if (data.type === 'float') {
|
||||
settingForm.value = parseFloat(data.value) || 0
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置详情失败:', error)
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换开放状态
|
||||
const handleToggleOpen = async (row) => {
|
||||
toggleLoading.value = row.id
|
||||
try {
|
||||
const res = await setSettingOpen({
|
||||
id: row.id,
|
||||
open: row.open
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
// 恢复原状态
|
||||
row.open = !row.open
|
||||
ElMessage.error(res.data.message || '修改失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复原状态
|
||||
row.open = !row.open
|
||||
console.error('修改失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '修改失败')
|
||||
} finally {
|
||||
toggleLoading.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除配置 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteSetting({ id: row.id })
|
||||
console.log('删除配置响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchSettingList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteSetting({ id: row.id })
|
||||
)
|
||||
await Promise.all(deletePromises)
|
||||
ElMessage.success('批量删除成功')
|
||||
fetchSettingList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
settingFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
name: settingForm.name,
|
||||
value: String(settingForm.value),
|
||||
type: settingForm.type,
|
||||
setting_group_id: settingForm.settingGroupID,
|
||||
open: settingForm.open,
|
||||
note: settingForm.note
|
||||
}
|
||||
if (settingForm.id) {
|
||||
submitData.id = settingForm.id
|
||||
}
|
||||
console.log('提交配置数据:', submitData)
|
||||
const res = settingForm.id
|
||||
? await updateSetting(submitData)
|
||||
: await createSetting(submitData)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(settingForm.id ? '修改成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchSettingList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '提交失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
fetchSettingList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-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;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
@@ -1,409 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-group-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="关键词筛选">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||
</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="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置组列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="200" />
|
||||
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.UpdatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</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="dialogTitle"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="groupFormRef"
|
||||
:model="groupForm"
|
||||
:rules="groupRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="groupForm.name" placeholder="请输入配置组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input
|
||||
v-model="groupForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSettingGroupList,
|
||||
getSettingGroupInfo,
|
||||
createSettingGroup,
|
||||
updateSettingGroup,
|
||||
deleteSettingGroup
|
||||
} from '@/api/admin/setting'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 配置组表单
|
||||
const groupForm = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置组名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增配置组')
|
||||
const groupFormRef = ref(null)
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取配置组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSettingGroupList(queryParams)
|
||||
console.log('配置组列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
groupList.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 handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 新增配置组
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增配置组'
|
||||
Object.assign(groupForm, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑配置组
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑配置组'
|
||||
try {
|
||||
const res = await getSettingGroupInfo({ setting_group_id: row.id })
|
||||
console.log('配置组详情数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
Object.assign(groupForm, {
|
||||
id: res.data.data.id,
|
||||
name: res.data.data.name || '',
|
||||
note: res.data.data.note || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置组详情失败:', error)
|
||||
ElMessage.error('获取配置组详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置组
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除配置组 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteSettingGroup({ setting_group_id: row.id })
|
||||
console.log('删除配置组响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteSettingGroup({ setting_group_id: row.id })
|
||||
)
|
||||
await Promise.all(deletePromises)
|
||||
ElMessage.success('批量删除成功')
|
||||
fetchGroupList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
groupFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
name: groupForm.name,
|
||||
note: groupForm.note
|
||||
}
|
||||
if (groupForm.id) {
|
||||
submitData.id = groupForm.id
|
||||
}
|
||||
console.log('提交配置组数据:', submitData)
|
||||
const res = groupForm.id
|
||||
? await updateSettingGroup(submitData)
|
||||
: await createSettingGroup(submitData)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(groupForm.id ? '修改成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '提交失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-group-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;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user