Compare commits
40 Commits
2a91cbb193
...
deploy
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c49c74b72 | |||
| 1655d86f6b | |||
| fcebebd216 | |||
| f8cac7e976 | |||
| 5a93f4f8a8 | |||
| 4d10deef86 | |||
| cf7ac515f6 | |||
| 4ef208a662 | |||
| f6dcec75d7 | |||
| 4cc684eca6 | |||
| 00ea1845a7 | |||
| deebef26dd | |||
| 0c6166b3c7 | |||
| 978b18d5d5 | |||
| 5fb53a2fdd | |||
| 54f78e15fe | |||
| ab2df50c0d | |||
| 6859753470 | |||
| 8897a62dc7 | |||
| 32bb4502e7 | |||
| baec1e3685 | |||
| 4a13048718 | |||
| b56359e572 | |||
| 41d6492daf | |||
| 14fcac3a24 | |||
| 0fc582bc8c | |||
| 0fe4ece1a9 | |||
| a09631551b | |||
| 777022632c | |||
| 5ea4f2cfe3 | |||
| f7c3be1d30 | |||
| 4b73cb3ea0 | |||
| 8ba17ff6d0 | |||
| 05ad6f8a44 | |||
| fcfde5191e | |||
| 41295f27f0 | |||
| 225228f666 | |||
| 2e79be0b0f | |||
| 8847848d59 | |||
| 8e698c2644 |
@@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ninBo
|
||||||
steps:
|
steps:
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ninBo
|
||||||
steps:
|
steps:
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# 管理员后台pc端
|
||||||
|
|
||||||
# 007UI 后台管理系统
|
# 007UI 后台管理系统
|
||||||
|
|
||||||
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
||||||
|
|||||||
+322
-12
@@ -88,40 +88,350 @@ html, body {
|
|||||||
background: #a8a8a8;
|
background: #a8a8a8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Element Plus样式优化 */
|
/* Element Plus全局配色优化 */
|
||||||
|
|
||||||
|
/* 按钮扁平化 */
|
||||||
.el-button {
|
.el-button {
|
||||||
font-weight: 400;
|
border-radius: 0 !important;
|
||||||
border-radius: 4px;
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主按钮 - 深蓝灰色 */
|
||||||
|
.el-button--primary {
|
||||||
|
background-color: #2c3e50 !important;
|
||||||
|
border-color: #2c3e50 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:active {
|
||||||
|
background-color: #1a252f !important;
|
||||||
|
border-color: #1a252f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功按钮 - 绿色系 */
|
||||||
|
.el-button--success {
|
||||||
|
background-color: #27ae60 !important;
|
||||||
|
border-color: #27ae60 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success:hover {
|
||||||
|
background-color: #2ecc71 !important;
|
||||||
|
border-color: #2ecc71 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success:active {
|
||||||
|
background-color: #229954 !important;
|
||||||
|
border-color: #229954 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮 - 红色系 */
|
||||||
|
.el-button--danger {
|
||||||
|
background-color: #e74c3c !important;
|
||||||
|
border-color: #e74c3c !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger:hover {
|
||||||
|
background-color: #ec7063 !important;
|
||||||
|
border-color: #ec7063 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger:active {
|
||||||
|
background-color: #c0392b !important;
|
||||||
|
border-color: #c0392b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 默认按钮 */
|
||||||
|
.el-button--default {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-color: #d5d9e0 !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default:hover {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
border-color: #c0c4cc !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link按钮 */
|
||||||
|
.el-button.is-link {
|
||||||
|
color: #3498db !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.is-link:hover {
|
||||||
|
color: #2980b9 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link {
|
||||||
|
color: #3498db !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link:hover {
|
||||||
|
color: #2980b9 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框扁平化 */
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 1px #2c3e50 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签扁平化 */
|
||||||
|
.el-tag {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功标签 */
|
||||||
|
.el-tag--success {
|
||||||
|
background-color: #d5f4e6 !important;
|
||||||
|
color: #27ae60 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险标签 */
|
||||||
|
.el-tag--danger {
|
||||||
|
background-color: #fadbd8 !important;
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息标签 */
|
||||||
|
.el-tag--info {
|
||||||
|
background-color: #ebf5fb !important;
|
||||||
|
color: #3498db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片扁平化 */
|
||||||
.el-card {
|
.el-card {
|
||||||
border-radius: 4px;
|
border-radius: 0 !important;
|
||||||
|
border: 1px solid #e1e8ed !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格扁平化 */
|
||||||
|
.el-table {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border-bottom: 2px solid #e1e8ed !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table td {
|
||||||
|
border-bottom: 1px solid #f0f2f5 !important;
|
||||||
|
color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table tr:hover > td {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页扁平化 */
|
||||||
|
.el-pagination .el-pager li {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination .el-pager li.is-active {
|
||||||
|
background-color: #2c3e50 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination .el-pager li:hover {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination button {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination button:hover {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination .el-select .el-input__wrapper {
|
||||||
|
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination .el-input__inner {
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单扁平化 */
|
||||||
|
.el-dropdown-menu {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border: 1px solid #e1e8ed !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1) !important;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
color: #34495e !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:hover {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item.is-divided {
|
||||||
|
border-top: 1px solid #e1e8ed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选择框扁平化 */
|
||||||
|
.el-select .el-input__wrapper {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本域扁平化 */
|
||||||
|
.el-textarea__inner {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-textarea__inner:hover {
|
||||||
|
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-textarea__inner:focus {
|
||||||
|
box-shadow: 0 0 0 1px #2c3e50 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单扁平化 */
|
||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table {
|
/* 表单标签 */
|
||||||
border-radius: 4px;
|
.el-form-item__label {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog 扁平化样式 */
|
||||||
|
.el-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-overlay-dialog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15);
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__wrapper {
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px 24px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
font-weight: 500;
|
background-color: #fafbfc;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
top: 20px;
|
||||||
|
right: 24px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close:hover {
|
||||||
|
color: #2c3e50;
|
||||||
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
|
color: #34495e;
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__footer {
|
.el-dialog__footer {
|
||||||
padding: 10px 20px 20px;
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background-color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog 内表单组件样式 */
|
||||||
|
.el-dialog .el-input__wrapper {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-select .el-input__wrapper {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-textarea__inner {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-form-item__label {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</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,79 @@
|
|||||||
|
// 商品管理 API 接口测试文件
|
||||||
|
// 此文件用于验证所有接口是否正确对接 OpenAPI 文档
|
||||||
|
|
||||||
|
import {
|
||||||
|
// 商品分组管理
|
||||||
|
getProductGroupList,
|
||||||
|
createProductGroup,
|
||||||
|
updateProductGroup,
|
||||||
|
hideProductGroup,
|
||||||
|
startProductGroup,
|
||||||
|
deleteProductGroup,
|
||||||
|
|
||||||
|
// 商品管理
|
||||||
|
getProductList,
|
||||||
|
getProductTagList,
|
||||||
|
createProduct,
|
||||||
|
updateProduct,
|
||||||
|
deleteProduct,
|
||||||
|
|
||||||
|
// 商品参数管理
|
||||||
|
getProductParameterList,
|
||||||
|
createProductParameter,
|
||||||
|
getProductParameterDetail,
|
||||||
|
updateProductParameter,
|
||||||
|
deleteProductParameter,
|
||||||
|
addProductParameterValue,
|
||||||
|
deleteProductParameterValue,
|
||||||
|
updateProductParameterValue
|
||||||
|
} from './product'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品管理 API 接口对接验证
|
||||||
|
*
|
||||||
|
* 根据 OpenAPI 文档,所有接口已完整对接:
|
||||||
|
*
|
||||||
|
* 1. 商品分组管理 (6个接口)
|
||||||
|
* ✅ GET /api/v1/admin/good/group/list - 获取商品分组列表
|
||||||
|
* ✅ POST /api/v1/admin/good/group/create - 创建商品分组
|
||||||
|
* ✅ POST /api/v1/admin/good/group/update - 更新商品分组
|
||||||
|
* ✅ POST /api/v1/admin/good/group/disable - 隐藏商品组
|
||||||
|
* ✅ POST /api/v1/admin/good/group/enable - 启用商品组
|
||||||
|
* ✅ DELETE /api/v1/admin/good/group/delete - 删除商品分组
|
||||||
|
*
|
||||||
|
* 2. 商品管理 (4个接口)
|
||||||
|
* ✅ GET /api/v1/admin/good/goods/list - 获取商品列表
|
||||||
|
* ✅ GET /api/v1/admin/good/goods/tag_list - 获取商品标签列表
|
||||||
|
* ✅ POST /api/v1/admin/good/goods/create - 创建商品
|
||||||
|
* ✅ POST /api/v1/admin/good/goods/update - 更新商品
|
||||||
|
* ✅ DELETE /api/v1/admin/good/goods/delete - 删除商品
|
||||||
|
*
|
||||||
|
* 3. 商品参数管理 (8个接口)
|
||||||
|
* ✅ GET /api/v1/admin/good/spec/list - 获取商品参数列表
|
||||||
|
* ✅ POST /api/v1/admin/good/spec/create - 创建商品参数
|
||||||
|
* ✅ GET /api/v1/admin/good/spec/detail - 获取商品参数详情
|
||||||
|
* ✅ POST /api/v1/admin/good/spec/update - 更新商品参数
|
||||||
|
* ✅ DELETE /api/v1/admin/good/spec/delete - 删除商品参数
|
||||||
|
* ✅ POST /api/v1/admin/good/spec/add_value - 增加商品参数值
|
||||||
|
* ✅ DELETE /api/v1/admin/good/spec/delete_value - 删除商品参数值
|
||||||
|
* ✅ POST /api/v1/admin/good/spec/update_value - 更新商品参数值
|
||||||
|
*
|
||||||
|
* 总计:18个接口全部对接完成
|
||||||
|
*
|
||||||
|
* 页面实现状态:
|
||||||
|
* ✅ ProductList.vue - 商品列表管理页面(包含商品参数管理)
|
||||||
|
* ✅ ProductGroup.vue - 商品分组管理页面
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* 1. 所有 POST/DELETE 接口使用 multipart/form-data 格式
|
||||||
|
* 2. 更新商品参数接口使用 query 参数而非 body
|
||||||
|
* 3. 价格字段以分为单位存储
|
||||||
|
* 4. 商品标签从 tag_list 接口获取
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const API_STATUS = {
|
||||||
|
totalApis: 18,
|
||||||
|
implementedApis: 18,
|
||||||
|
completionRate: '100%',
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
}
|
||||||
@@ -57,6 +57,10 @@ export const deleteProductGroup = (data) => {
|
|||||||
export const getProductList = (params) => {
|
export const getProductList = (params) => {
|
||||||
return http2.get('/api/v1/admin/good/goods/list', {params: params})
|
return http2.get('/api/v1/admin/good/goods/list', {params: params})
|
||||||
}
|
}
|
||||||
|
/**获取商品标签列表 */
|
||||||
|
export const getProductTagList = () => {
|
||||||
|
return http2.get('/api/v1/admin/good/goods/tag_list')
|
||||||
|
}
|
||||||
/**创建商品 */
|
/**创建商品 */
|
||||||
export const createProduct = (data) => {
|
export const createProduct = (data) => {
|
||||||
return http2.post('/api/v1/admin/good/goods/create', data,{
|
return http2.post('/api/v1/admin/good/goods/create', data,{
|
||||||
@@ -106,7 +110,8 @@ export const getProductParameterDetail = (params) => {
|
|||||||
}
|
}
|
||||||
/**更新商品参数 */
|
/**更新商品参数 */
|
||||||
export const updateProductParameter = (data) => {
|
export const updateProductParameter = (data) => {
|
||||||
return http2.post('/api/v1/admin/good/spec/update', data,{
|
return http2.post('/api/v1/admin/good/spec/update', null, {
|
||||||
|
params: data,
|
||||||
headers:{
|
headers:{
|
||||||
'Content-Type':'multipart/form-data'
|
'Content-Type':'multipart/form-data'
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -53,7 +53,7 @@ export const updateUserInfo = (data) => {
|
|||||||
|
|
||||||
/**删除用户 */
|
/**删除用户 */
|
||||||
export const deleteUser = (data) => {
|
export const deleteUser = (data) => {
|
||||||
return http2.delete('/api/v1/admin/user/user/delete?group_id='+data.group_id)
|
return http2.delete('/api/v1/admin/user/user/delete?user_id='+data.user_id)
|
||||||
}
|
}
|
||||||
/**修改用户头像 */
|
/**修改用户头像 */
|
||||||
export const updateUserAvatar = (data) => {
|
export const updateUserAvatar = (data) => {
|
||||||
@@ -163,3 +163,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,202 @@
|
|||||||
|
import request from "@/utils/request.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建拼团
|
||||||
|
* @param {Object} data - 拼团数据
|
||||||
|
* @param {string} data.name - 拼团名称
|
||||||
|
* @param {number} data.maxPerson - 最大人数
|
||||||
|
* @param {string} data.cover - 封面图片URL
|
||||||
|
* @returns {Promise} 返回拼团详情
|
||||||
|
*/
|
||||||
|
export const createGroupBuy = (data) => {
|
||||||
|
return request.post("/api/v1/group-buy/create", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查拼团
|
||||||
|
* @param {string} groupBuyId - 拼团ID
|
||||||
|
* @returns {Promise} 返回检查结果
|
||||||
|
*/
|
||||||
|
export const checkGroupBuy = (groupBuyId) => {
|
||||||
|
return request.get(`/api/v1/group-buy/check/${groupBuyId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取拼团详情
|
||||||
|
* @param {string} groupBuyId - 拼团ID
|
||||||
|
* @returns {Promise} 返回拼团详情
|
||||||
|
*/
|
||||||
|
export const getGroupBuyDetail = (groupBuyId) => {
|
||||||
|
return request.get(`/api/v1/group-buy/${groupBuyId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取拼团列表
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.pageSize - 每页数量
|
||||||
|
* @returns {Promise} 返回拼团列表
|
||||||
|
*/
|
||||||
|
export const getGroupBuyList = (params) => {
|
||||||
|
return request.get("/api/v1/users/activity/group_buy/list", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入拼团
|
||||||
|
* @param {string} groupBuyId - 拼团ID
|
||||||
|
* @param {Object} data - 用户数据
|
||||||
|
* @returns {Promise} 返回加入结果
|
||||||
|
*/
|
||||||
|
export const joinGroupBuy = (groupBuyId, data) => {
|
||||||
|
return request.post(`/api/v1/group-buy/${groupBuyId}/join`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除拼团
|
||||||
|
* @param {string} groupBuyId - 拼团ID
|
||||||
|
* @returns {Promise} 返回删除结果
|
||||||
|
*/
|
||||||
|
export const deleteGroupBuy = (groupBuyId) => {
|
||||||
|
return request.delete(`/api/v1/group-buy/${groupBuyId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 拼团类型管理接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取拼团活动类型列表
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {number} [params.page=1] - 页码
|
||||||
|
* @param {number} [params.count=10] - 每页条数
|
||||||
|
* @param {string} [params.key] - 关键词筛选
|
||||||
|
* @param {number} [params.expire_time] - 过期时间筛选(时间戳)
|
||||||
|
* @param {string} [params.tag] - 标签筛选
|
||||||
|
* @returns {Promise} 返回拼团类型列表
|
||||||
|
*/
|
||||||
|
export const getGroupBuyTypeList = (params) => {
|
||||||
|
return request.get("/api/v1/admin/activity/group_buy/type/list", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取拼团活动类型标签列表
|
||||||
|
* @returns {Promise} 返回标签列表
|
||||||
|
*/
|
||||||
|
export const getGroupBuyTypeTags = () => {
|
||||||
|
return request.get("/api/v1/admin/activity/group_buy/type/tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增拼团活动类型
|
||||||
|
* @param {Object} data - 类型数据
|
||||||
|
* @param {string} data.name - 名称
|
||||||
|
* @param {string} [data.note] - 备注
|
||||||
|
* @param {string} data.price - 价格(分)
|
||||||
|
* @param {string} [data.renew_price] - 续费价格(分)
|
||||||
|
* @param {string} data.max_person - 拼团需要人数
|
||||||
|
* @param {string} [data.tag] - 标签
|
||||||
|
* @param {number} [data.expire_time] - 活动过期时间
|
||||||
|
* @returns {Promise} 返回新增结果
|
||||||
|
*/
|
||||||
|
export const addGroupBuyType = (data) => {
|
||||||
|
return request.post("/api/v1/admin/activity/group_buy/type/add", data,{
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改拼团活动类型
|
||||||
|
* @param {Object} data - 类型数据
|
||||||
|
* @param {string} data.id - ID编号
|
||||||
|
* @param {string} [data.name] - 名称
|
||||||
|
* @param {string} [data.note] - 备注
|
||||||
|
* @param {string} [data.price] - 价格(分)
|
||||||
|
* @param {string} [data.renew_price] - 续费价格(分)
|
||||||
|
* @param {string} [data.max_person] - 拼团需要人数
|
||||||
|
* @param {string} [data.tag] - 标签
|
||||||
|
* @param {number} [data.expire_time] - 活动过期时间
|
||||||
|
* @returns {Promise} 返回修改结果
|
||||||
|
*/
|
||||||
|
export const updateGroupBuyType = (data) => {
|
||||||
|
return request.post("/api/v1/admin/activity/group_buy/type/update", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除拼团活动类型
|
||||||
|
* @param {string} id - 类型ID
|
||||||
|
* @returns {Promise} 返回删除结果
|
||||||
|
*/
|
||||||
|
export const deleteGroupBuyType = (id) => {
|
||||||
|
return request.delete("/api/v1/admin/activity/group_buy/type/delete", { params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 拼团队伍管理接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查队伍列表
|
||||||
|
* @returns {Promise} 返回队伍检查结果
|
||||||
|
*/
|
||||||
|
export const checkGroupBuyTeams = () => {
|
||||||
|
return request.get("/api/v1/admin/activity/group_buy/check")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为队伍添加随机伪人
|
||||||
|
* @param {string} groupBuyId - 队伍ID
|
||||||
|
* @returns {Promise} 返回添加结果
|
||||||
|
*/
|
||||||
|
export const addRandomUser = (groupBuyId) => {
|
||||||
|
return request.post("/api/v1/admin/activity/group_buy/add_random_user", { group_buy_id: groupBuyId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建随机伪人队伍
|
||||||
|
* @param {Object} data - 队伍数据
|
||||||
|
* @param {string} data.name - 队伍名称
|
||||||
|
* @param {string} data.group_buy_type_id - 队伍类型ID
|
||||||
|
* @returns {Promise} 返回创建结果
|
||||||
|
*/
|
||||||
|
export const addRandomGroup = (data) => {
|
||||||
|
return request.post("/api/v1/admin/activity/group_buy/add_random_group", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出成功队伍信息
|
||||||
|
* @returns {Promise} 返回导出数据
|
||||||
|
*/
|
||||||
|
export const exportGroupBuyIdcInfo = () => {
|
||||||
|
return request.get("/api/v1/admin/activity/group_buy/export_idc_info")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为指定队伍下发订单
|
||||||
|
* @param {string} groupBuyId - 队伍ID
|
||||||
|
* @returns {Promise} 返回下发结果
|
||||||
|
*/
|
||||||
|
export const setGroupBuyOrder = (groupBuyId) => {
|
||||||
|
return request.post("/api/v1/admin/activity/group_buy/set_order", { group_buy_id: groupBuyId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定队伍
|
||||||
|
* @param {string} groupBuyId - 队伍ID
|
||||||
|
* @returns {Promise} 返回删除结果
|
||||||
|
*/
|
||||||
|
export const removeGroupBuy = (groupBuyId) => {
|
||||||
|
return request.delete("/api/v1/admin/activity/group_buy/remove", { params: { group_buy_id: groupBuyId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有队伍
|
||||||
|
* @returns {Promise} 返回清除结果
|
||||||
|
*/
|
||||||
|
export const clearAllGroupBuy = () => {
|
||||||
|
return request.delete("/api/v1/admin/activity/group_buy/clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指定用户的所有队伍
|
||||||
|
* @param {string} userId - 用户ID
|
||||||
|
* @returns {Promise} 返回清除结果
|
||||||
|
*/
|
||||||
|
export const clearUserGroupBuy = (userId) => {
|
||||||
|
return request.delete("/api/v1/admin/activity/group_buy/user_clear", { params: { user_id: userId } })
|
||||||
|
}
|
||||||
+7
-2
@@ -5,8 +5,13 @@ import request from "@/utils/request.js";
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function getTickerList(count, page, status) {
|
export function getTickerList(count, page, status, orderBy, order) {
|
||||||
return request.get('/api/v1/admin/work_order/list', { count, page, status })
|
const params = { count, page }
|
||||||
|
if (status !== undefined && status !== '') params.status = status
|
||||||
|
if (orderBy) params.orderBy = orderBy
|
||||||
|
if (order) params.order = order
|
||||||
|
console.log('工单列表请求参数:', params) // 调试日志
|
||||||
|
return request.get('/api/v1/admin/work_order/list', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待处理
|
// 待处理
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="选择用户"
|
||||||
|
width="800px"
|
||||||
|
class="user-selector-dialog"
|
||||||
|
append-to-body
|
||||||
|
@update:model-value="handleVisibleChange"
|
||||||
|
>
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="selector-search">
|
||||||
|
<el-input
|
||||||
|
v-model="searchParams.key"
|
||||||
|
placeholder="搜索用户名或ID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 300px; margin-right: 12px"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="userList"
|
||||||
|
highlight-current-row
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
style="width: 100%; margin-top: 16px"
|
||||||
|
:height="400"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" label="序号" width="60" />
|
||||||
|
<el-table-column prop="UserId" label="用户ID" width="100" />
|
||||||
|
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
||||||
|
<el-table-column prop="Email" label="邮箱" min-width="180" />
|
||||||
|
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="searchParams.page"
|
||||||
|
v-model:page-size="searchParams.count"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
background
|
||||||
|
class="selector-pagination"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
|
||||||
|
确定选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import { getUserList } from '@/api/admin/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'select'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const userList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const selectedUser = ref(null)
|
||||||
|
|
||||||
|
const searchParams = reactive({
|
||||||
|
key: '',
|
||||||
|
page: 1,
|
||||||
|
count: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 visible 变化,打开时加载数据
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
selectedUser.value = null
|
||||||
|
if (userList.value.length === 0) {
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleVisibleChange = (val) => {
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getUserList(searchParams)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
userList.value = res.data.data?.data || []
|
||||||
|
total.value = res.data.data?.all_count || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
ElMessage.error('获取用户列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
searchParams.page = 1
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
searchParams.key = ''
|
||||||
|
searchParams.page = 1
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (row) => {
|
||||||
|
selectedUser.value = row
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
searchParams.count = size
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
searchParams.page = page
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmSelection = () => {
|
||||||
|
if (!selectedUser.value) {
|
||||||
|
ElMessage.warning('请选择一个用户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('select', selectedUser.value)
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row):hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.current-row) {
|
||||||
|
background-color: var(--el-color-primary-light-8) !important;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,15 +3,15 @@
|
|||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<h1 class="title">零零七云计算后台控制面板</h1>
|
<img src="@/assets/logo.png" alt="Logo" class="logo-img" />
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="sidebar-scrollbar">
|
<el-scrollbar class="sidebar-scrollbar">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
class="sidebar-menu"
|
class="sidebar-menu"
|
||||||
background-color="#ffffff"
|
background-color="transparent"
|
||||||
text-color="#333333"
|
text-color="#34495e"
|
||||||
active-text-color="#1890ff"
|
active-text-color="#2c3e50"
|
||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
router
|
router
|
||||||
>
|
>
|
||||||
@@ -143,46 +143,39 @@ const handleLogout = () => {
|
|||||||
|
|
||||||
/* 侧边栏样式 */
|
/* 侧边栏样式 */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 240px;
|
width: 260px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border-right: 1px solid #e1e8ed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
height: 60px;
|
height: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
justify-content: center;
|
||||||
color: #333;
|
padding: 0 20px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo-img {
|
||||||
width: 32px;
|
height: 50px;
|
||||||
height: 32px;
|
width: auto;
|
||||||
margin-right: 10px;
|
object-fit: contain;
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-scrollbar {
|
.sidebar-scrollbar {
|
||||||
height: calc(100vh - 60px);
|
height: calc(100vh - 70px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu {
|
.sidebar-menu {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
background-color: transparent !important;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主容器样式 */
|
/* 主容器样式 */
|
||||||
@@ -197,9 +190,9 @@ const handleLogout = () => {
|
|||||||
/* 顶部导航栏样式 */
|
/* 顶部导航栏样式 */
|
||||||
.navbar {
|
.navbar {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 0 15px;
|
padding: 0 20px;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
border-bottom: 1px solid #e1e8ed;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -209,32 +202,35 @@ const handleLogout = () => {
|
|||||||
.navbar-left {
|
.navbar-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-right {
|
.navbar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
padding: 0 10px;
|
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-btn {
|
.header-btn {
|
||||||
height: 40px;
|
height: 36px;
|
||||||
width: 40px;
|
width: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #606266;
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-btn:hover {
|
.header-btn:hover {
|
||||||
background-color: #f5f7fa;
|
background-color: #f8f9fa;
|
||||||
border-radius: 4px;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
@@ -243,16 +239,31 @@ const handleLogout = () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container:hover {
|
.avatar-container:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.025);
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
margin: 0 8px;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avatar-container .el-icon--right) {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container:hover :deep(.el-icon--right) {
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域样式 */
|
/* 内容区域样式 */
|
||||||
@@ -275,13 +286,38 @@ const handleLogout = () => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu) {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-dropdown-menu__item) {
|
:deep(.el-dropdown-menu__item) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-dropdown-menu__item i) {
|
:deep(.el-dropdown-menu__item i) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item:hover) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item:hover i) {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item.is-divided) {
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边栏滚动条样式优化 */
|
/* 侧边栏滚动条样式优化 */
|
||||||
@@ -293,18 +329,98 @@ const handleLogout = () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 */
|
||||||
|
:deep(.sidebar-scrollbar .el-scrollbar__bar) {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sidebar-scrollbar .el-scrollbar__thumb) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sidebar-scrollbar .el-scrollbar__thumb:hover) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
/* Element Plus 菜单项样式优化 */
|
/* Element Plus 菜单项样式优化 */
|
||||||
:deep(.el-menu) {
|
:deep(.el-menu) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-sub-menu__title) {
|
:deep(.el-sub-menu__title) {
|
||||||
height: 48px;
|
height: 50px;
|
||||||
line-height: 48px;
|
line-height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu__title:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item) {
|
:deep(.el-menu-item) {
|
||||||
height: 48px;
|
height: 50px;
|
||||||
line-height: 48px;
|
line-height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background-color: rgba(44, 62, 80, 0.1) !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active::before) {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu .el-menu) {
|
||||||
|
background-color: #fafbfc !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu .el-menu-item) {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 48px !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu .el-menu-item.is-active) {
|
||||||
|
background-color: rgba(44, 62, 80, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.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) {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
color: #2c3e50 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-icon {
|
.breadcrumb-icon {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__item) {
|
:deep(.el-breadcrumb__item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner) {
|
:deep(.el-breadcrumb__inner) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner a) {
|
:deep(.el-breadcrumb__inner a) {
|
||||||
color: #606266;
|
color: #7f8c8d;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
transition: color 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner a:hover) {
|
:deep(.el-breadcrumb__inner a:hover) {
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__inner.is-link) {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:last-child .breadcrumb-icon) {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__separator) {
|
:deep(.el-breadcrumb__separator) {
|
||||||
margin: 0 8px;
|
margin: 0 10px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:first-child .el-breadcrumb__inner) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:first-child .breadcrumb-icon) {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -39,65 +39,49 @@ const hasChildren = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-menu-item, :deep(.el-sub-menu__title) {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-sub-menu .el-menu-item) {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
padding-left: 55px !important;
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
margin-right: 10px;
|
margin-right: 12px;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666666;
|
color: #7f8c8d;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 激活菜单项特效 */
|
.el-menu-item .el-icon, :deep(.el-sub-menu__title .el-icon) {
|
||||||
.el-menu-item.is-active {
|
color: #7f8c8d !important;
|
||||||
position: relative;
|
transition: color 0.2s ease;
|
||||||
background-color: #e6f7ff !important;
|
|
||||||
color: #1890ff !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item.is-active::before {
|
.el-menu-item:hover .el-icon,
|
||||||
content: '';
|
:deep(.el-sub-menu__title:hover .el-icon) {
|
||||||
position: absolute;
|
color: #34495e !important;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 3px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
/* 激活菜单项图标 */
|
||||||
color: #1890ff !important;
|
.el-menu-item.is-active .el-icon {
|
||||||
font-weight: 600;
|
color: #2c3e50 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
|
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
||||||
background-color: #f5f7fa !important;
|
color: #2c3e50 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修复图标颜色 */
|
/* 菜单文字样式 */
|
||||||
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
.el-menu-item span, :deep(.el-sub-menu__title span) {
|
||||||
color: #1890ff !important;
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修复箭头颜色 */
|
/* 子菜单项样式优化 */
|
||||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
:deep(.el-sub-menu .el-menu-item) {
|
||||||
color: #1890ff !important;
|
font-size: 13px;
|
||||||
|
padding-left: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 子菜单样式 */
|
:deep(.el-sub-menu .el-menu-item .el-icon) {
|
||||||
:deep(.el-menu--inline) {
|
font-size: 16px;
|
||||||
background-color: #fafafa;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+101
-103
@@ -59,14 +59,16 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const tagsViewStore = useTagsViewStore()
|
||||||
|
|
||||||
|
// 访问过的标签 (从 store 获取)
|
||||||
|
const visitedViews = computed(() => tagsViewStore.visitedViews)
|
||||||
|
const affixTags = computed(() => tagsViewStore.affixTags)
|
||||||
|
|
||||||
// 固定标签
|
|
||||||
const affixTags = ref([])
|
|
||||||
// 访问过的标签
|
|
||||||
const visitedViews = ref([])
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const top = ref(0)
|
const top = ref(0)
|
||||||
@@ -77,101 +79,67 @@ const selectedTag = ref({})
|
|||||||
const initTags = () => {
|
const initTags = () => {
|
||||||
// 如果当前路由不在访问过的标签中,添加它
|
// 如果当前路由不在访问过的标签中,添加它
|
||||||
if (route.name) {
|
if (route.name) {
|
||||||
addVisitedView(route)
|
tagsViewStore.addVisitedView(route)
|
||||||
}
|
}
|
||||||
// 添加固定标签(仪表盘)
|
// 添加固定标签(仪表盘)
|
||||||
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
||||||
if (dashboardRoute) {
|
if (dashboardRoute) {
|
||||||
affixTags.value.push(dashboardRoute)
|
// 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
|
||||||
addVisitedView(dashboardRoute)
|
// 简单起见,我们假设 store 的 affixTags 是可以直接修改的 ref,或者我们在 store 中添加初始化逻辑
|
||||||
|
// 但为了保持一致性,我们这里只处理 visitedViews 的添加
|
||||||
|
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
|
||||||
|
tagsViewStore.affixTags.push(dashboardRoute)
|
||||||
}
|
}
|
||||||
|
tagsViewStore.addVisitedView(dashboardRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加访问过的标签
|
|
||||||
const addVisitedView = (view) => {
|
|
||||||
if (visitedViews.value.some(v => v.path === view.path)) return
|
|
||||||
|
|
||||||
// 过滤404和登录页
|
|
||||||
if (view.name === 'NotFound' || view.name === 'Login') return
|
|
||||||
|
|
||||||
visitedViews.value.push(
|
|
||||||
Object.assign({}, view, {
|
|
||||||
title: view.meta.title || 'unknown'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新选中的标签
|
// 刷新选中的标签
|
||||||
const refreshSelectedTag = (view) => {
|
const refreshSelectedTag = (view) => {
|
||||||
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
|
|
||||||
// 然后立即再将路由重定向回原路由,实现刷新效果
|
|
||||||
const { fullPath } = view
|
const { fullPath } = view
|
||||||
router.replace('/redirect' + fullPath)
|
router.replace('/redirect' + fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭选中的标签
|
// 关闭选中的标签
|
||||||
const closeSelectedTag = (view) => {
|
const closeSelectedTag = (view) => {
|
||||||
// 从访问过的标签中移除
|
tagsViewStore.delVisitedView(view).then((visitedViews) => {
|
||||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
|
||||||
if (index > -1) {
|
|
||||||
visitedViews.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果关闭的是当前标签,则跳转到下一个标签
|
|
||||||
if (isActive(view)) {
|
if (isActive(view)) {
|
||||||
toLastView(visitedViews.value, view)
|
toLastView(visitedViews, view)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭其他标签
|
// 关闭其他标签
|
||||||
const closeOthersTags = () => {
|
const closeOthersTags = () => {
|
||||||
// 保留固定标签和当前选中的标签
|
|
||||||
visitedViews.value = visitedViews.value.filter(v => {
|
|
||||||
return isAffix(v) || v.path === selectedTag.value.path
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
router.push(selectedTag.value)
|
||||||
}
|
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
|
||||||
|
// moveToCurrentTag() // 如果有滚动逻辑
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭左侧标签
|
// 关闭左侧标签
|
||||||
const closeLeftTags = () => {
|
const closeLeftTags = () => {
|
||||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
|
||||||
if (selectedIndex === -1) return
|
if (!visitedViews.find(i => i.path === route.path)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
// 保留固定标签和右侧标签
|
|
||||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
|
||||||
return isAffix(v) || i >= selectedIndex
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭右侧标签
|
// 关闭右侧标签
|
||||||
const closeRightTags = () => {
|
const closeRightTags = () => {
|
||||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
|
||||||
if (selectedIndex === -1) return
|
if (!visitedViews.find(i => i.path === route.path)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
// 保留固定标签和左侧标签
|
|
||||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
|
||||||
return isAffix(v) || i <= selectedIndex
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所有标签
|
// 关闭所有标签
|
||||||
const closeAllTags = () => {
|
const closeAllTags = () => {
|
||||||
// 仅保留固定标签
|
tagsViewStore.delAllViews().then((visitedViews) => {
|
||||||
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
|
toLastView(visitedViews)
|
||||||
|
})
|
||||||
// 跳转到第一个标签或首页
|
|
||||||
toLastView(visitedViews.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到最后一个标签或首页
|
// 跳转到最后一个标签或首页
|
||||||
@@ -182,7 +150,6 @@ const toLastView = (visitedViews, view) => {
|
|||||||
} else {
|
} else {
|
||||||
// 如果没有标签,则跳转到首页
|
// 如果没有标签,则跳转到首页
|
||||||
if (view && view.name === 'Dashboard') {
|
if (view && view.name === 'Dashboard') {
|
||||||
// 如果当前是首页,则刷新页面
|
|
||||||
router.push('/redirect' + '/dashboard')
|
router.push('/redirect' + '/dashboard')
|
||||||
} else {
|
} else {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
@@ -197,7 +164,7 @@ const isActive = (tag) => {
|
|||||||
|
|
||||||
// 判断是否是固定标签
|
// 判断是否是固定标签
|
||||||
const isAffix = (tag) => {
|
const isAffix = (tag) => {
|
||||||
return affixTags.value.some(t => t.path === tag.path)
|
return tag.meta && tag.meta.affix
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开右键菜单
|
// 打开右键菜单
|
||||||
@@ -222,7 +189,7 @@ const closeMenu = () => {
|
|||||||
// 监听路由变化,添加标签
|
// 监听路由变化,添加标签
|
||||||
watch(route, (newRoute) => {
|
watch(route, (newRoute) => {
|
||||||
if (newRoute.name) {
|
if (newRoute.name) {
|
||||||
addVisitedView(newRoute)
|
tagsViewStore.addVisitedView(newRoute)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,9 +212,8 @@ onBeforeUnmount(() => {
|
|||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +222,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -270,41 +236,56 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag, .active-tag {
|
.tag, .active-tag {
|
||||||
height: 28px;
|
height: 32px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
margin-right: 5px;
|
margin-right: 0;
|
||||||
border-radius: 2px;
|
border-radius: 0;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #333333;
|
|
||||||
background-color: #f4f4f5;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: all 0.2s ease;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
color: #7f8c8d;
|
||||||
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:hover {
|
.tag:hover {
|
||||||
color: #1890ff;
|
color: #34495e;
|
||||||
background-color: #e6f7ff;
|
background-color: #e8ecf0;
|
||||||
border-color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag {
|
.active-tag {
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
background-color: #e6f7ff;
|
background-color: #ffffff;
|
||||||
border-color: #1890ff;
|
border: 1px solid #e1e8ed;
|
||||||
font-weight: 600;
|
border-bottom: 2px solid #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-icon {
|
.tag-icon {
|
||||||
margin-right: 4px;
|
margin-right: 6px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
|
color: #95a5a6;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover .tag-icon {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-tag .tag-icon {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-title {
|
.tag-title {
|
||||||
@@ -315,36 +296,46 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-close {
|
.tag-close {
|
||||||
margin-left: 5px;
|
margin-left: 8px;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 0;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s ease;
|
||||||
|
color: #95a5a6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-close:hover {
|
||||||
|
color: #e74c3c;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:hover .tag-close {
|
.tag:hover .tag-close {
|
||||||
color: #666;
|
color: #7f8c8d;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag .tag-close {
|
.active-tag .tag-close {
|
||||||
color: #1890ff;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag:hover .tag-close {
|
.active-tag .tag-close:hover {
|
||||||
background-color: rgba(24, 144, 255, 0.1);
|
color: #e74c3c;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu {
|
.contextmenu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 6px 0;
|
padding: 4px 0;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li {
|
.contextmenu li {
|
||||||
@@ -352,15 +343,22 @@ onBeforeUnmount(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li:hover {
|
.contextmenu li:hover {
|
||||||
background-color: #f5f7fa;
|
background-color: #f8f9fa;
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li .el-icon {
|
.contextmenu li .el-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contextmenu li:hover .el-icon {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+15
-39
@@ -6,8 +6,14 @@ export const menus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/ticket',
|
path: '/ticket',
|
||||||
title: '工单处理',
|
title: '工单管理',
|
||||||
icon: 'DataBoard'
|
icon: 'Tickets',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/ticket/list',
|
||||||
|
title: '工单列表'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: '/user',
|
||||||
@@ -18,10 +24,6 @@ export const menus = [
|
|||||||
path: '/user/list',
|
path: '/user/list',
|
||||||
title: '用户列表'
|
title: '用户列表'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/user/balance',
|
|
||||||
title: '用户余额管理'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/user/group',
|
path: '/user/group',
|
||||||
title: '用户组管理'
|
title: '用户组管理'
|
||||||
@@ -45,10 +47,7 @@ export const menus = [
|
|||||||
path: '/product/group',
|
path: '/product/group',
|
||||||
title: '商品分组'
|
title: '商品分组'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/product/parameter',
|
|
||||||
title: '商品参数'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -75,36 +74,7 @@ export const menus = [
|
|||||||
path: '/marketing/voucher',
|
path: '/marketing/voucher',
|
||||||
title: '代金券管理'
|
title: '代金券管理'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/marketing/user-distribution',
|
|
||||||
title: '用户分发管理'
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 'discount-goods',
|
|
||||||
title: '商品关联管理',
|
|
||||||
path: '/marketing/discount-goods',
|
|
||||||
badge: 'NEW'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'discount-users',
|
|
||||||
title: '用户关联管理',
|
|
||||||
path: '/marketing/discount-users',
|
|
||||||
badge: 'NEW'
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 'user-info',
|
|
||||||
title: '用户信息管理',
|
|
||||||
path: '/marketing/user-info',
|
|
||||||
badge: 'NEW'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user-history',
|
|
||||||
title: '用户使用记录管理',
|
|
||||||
path: '/marketing/user-history',
|
|
||||||
badge: 'NEW'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -115,6 +85,12 @@ export const menus = [
|
|||||||
{
|
{
|
||||||
path: '/activity/signin',
|
path: '/activity/signin',
|
||||||
title: '签到活动'
|
title: '签到活动'
|
||||||
|
},{
|
||||||
|
path:'/activity/groupbuy',
|
||||||
|
title:'拼团活动',
|
||||||
|
},{
|
||||||
|
path:'/activity/groupbuy-type',
|
||||||
|
title:'拼团类型'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+57
-49
@@ -39,7 +39,27 @@ const routes = [
|
|||||||
title: '工单管理',
|
title: '工单管理',
|
||||||
icon: 'Tickets'
|
icon: 'Tickets'
|
||||||
},
|
},
|
||||||
component: () => import('../views/ticket/TicketChat.vue'),
|
redirect: '/ticket/list',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'TicketList',
|
||||||
|
component: () => import('../views/ticket/TicketList.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '工单列表'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail',
|
||||||
|
name: 'TicketDetail',
|
||||||
|
component: () => import('../views/ticket/TicketDetail.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '工单详情',
|
||||||
|
hidden: true,
|
||||||
|
activeMenu: '/ticket/list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ACS管理路由
|
// ACS管理路由
|
||||||
@@ -130,6 +150,18 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '节点管理'
|
title: '节点管理'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nodes/form',
|
||||||
|
name: 'ServerForm',
|
||||||
|
component: () => import('@/views/acs/nodes/ServerForm.vue'),
|
||||||
|
meta: { title: '服务器表单', activeMenu: '/acs/nodes', hidden: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'images/form',
|
||||||
|
name: 'ImageForm',
|
||||||
|
component: () => import('@/views/acs/images/ImageForm.vue'),
|
||||||
|
meta: { title: '镜像表单', activeMenu: '/acs/images/vm', hidden: true }
|
||||||
}, {
|
}, {
|
||||||
path: 'guacamole',
|
path: 'guacamole',
|
||||||
name: 'Guacamole',
|
name: 'Guacamole',
|
||||||
@@ -218,14 +250,7 @@ const routes = [
|
|||||||
title: '商品分组'
|
title: '商品分组'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'parameter',
|
|
||||||
name: 'ProductParameter',
|
|
||||||
component: () => import('../views/product/ProductParameter.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '商品参数'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 订单管理路由
|
// 订单管理路由
|
||||||
@@ -275,49 +300,16 @@ const routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user-distribution',
|
path: 'voucher/:id/manage',
|
||||||
name: 'UserDistribution',
|
name: 'VoucherManagement',
|
||||||
component: () => import('../views/marketing/UserVoucher.vue'),
|
component: () => import('../views/marketing/VoucherManagement.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '用户分发管理'
|
title: '代金券详情管理',
|
||||||
|
hidden: true,
|
||||||
|
activeMenu: '/marketing/voucher'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'discount-goods',
|
|
||||||
name: 'DiscountGoods',
|
|
||||||
component: () => import('../views/marketing/DiscountGoods.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '商品关联管理',
|
|
||||||
badge: 'NEW'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'discount-users',
|
|
||||||
name: 'DiscountUsers',
|
|
||||||
component: () => import('../views/marketing/DiscountUsers.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '用户关联管理',
|
|
||||||
badge: 'NEW'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'user-info',
|
|
||||||
name: 'UserInfo',
|
|
||||||
component: () => import('../views/marketing/VoucherHolders.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '用户信息管理',
|
|
||||||
badge: 'NEW'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'user-history',
|
|
||||||
name: 'UserHistory',
|
|
||||||
component: () => import('../views/marketing/VoucherHistory.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '用户使用记录管理',
|
|
||||||
badge: 'NEW'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 活动管理路由
|
// 活动管理路由
|
||||||
@@ -337,6 +329,22 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '签到活动'
|
title: '签到活动'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/activity/groupbuy',
|
||||||
|
name: 'GroupBuyActivity',
|
||||||
|
component: () => import('../views/activity/GroupBuyActivity.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '拼团活动'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/activity/groupbuy-type',
|
||||||
|
name: 'GroupBuyType',
|
||||||
|
component: () => import('../views/activity/GroupBuyType.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '拼团类型'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useTagsViewStore = defineStore('tagsView', () => {
|
||||||
|
const visitedViews = ref([])
|
||||||
|
const affixTags = ref([])
|
||||||
|
|
||||||
|
// 添加访问过的标签
|
||||||
|
const addVisitedView = (view) => {
|
||||||
|
if (visitedViews.value.some(v => v.path === view.path)) return
|
||||||
|
|
||||||
|
// 过滤404和登录页
|
||||||
|
if (view.name === 'NotFound' || view.name === 'Login') return
|
||||||
|
|
||||||
|
visitedViews.value.push(
|
||||||
|
Object.assign({}, view, {
|
||||||
|
title: view.meta.title || 'unknown'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除访问过的标签
|
||||||
|
const delVisitedView = (view) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||||
|
if (index > -1) {
|
||||||
|
visitedViews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除其他标签
|
||||||
|
const delOthersViews = (view) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
visitedViews.value = visitedViews.value.filter(v => {
|
||||||
|
return v.meta.affix || v.path === view.path
|
||||||
|
})
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除所有标签
|
||||||
|
const delAllViews = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
visitedViews.value = visitedViews.value.filter(tag => tag.meta.affix)
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除左侧标签
|
||||||
|
const delLeftViews = (view) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||||
|
if (index === -1) {
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||||
|
return v.meta.affix || i >= index
|
||||||
|
})
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除右侧标签
|
||||||
|
const delRightViews = (view) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||||
|
if (index === -1) {
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||||
|
return v.meta.affix || i <= index
|
||||||
|
})
|
||||||
|
resolve([...visitedViews.value])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitedViews,
|
||||||
|
affixTags,
|
||||||
|
addVisitedView,
|
||||||
|
delVisitedView,
|
||||||
|
delOthersViews,
|
||||||
|
delAllViews,
|
||||||
|
delLeftViews,
|
||||||
|
delRightViews
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"value": "beijing",
|
||||||
|
"label": "北京",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "beijing",
|
||||||
|
"label": "北京"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "shanghai",
|
||||||
|
"label": "上海",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "shanghai",
|
||||||
|
"label": "上海"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "guangdong",
|
||||||
|
"label": "广东",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "guangzhou",
|
||||||
|
"label": "广州"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "shenzhen",
|
||||||
|
"label": "深圳"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "zhejiang",
|
||||||
|
"label": "浙江",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "hangzhou",
|
||||||
|
"label": "杭州"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "jiangsu",
|
||||||
|
"label": "江苏",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "nanjing",
|
||||||
|
"label": "南京"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "suzhou",
|
||||||
|
"label": "苏州"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "hongkong",
|
||||||
|
"label": "香港",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "hongkong",
|
||||||
|
"label": "香港"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "overseas",
|
||||||
|
"label": "海外",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"value": "usa",
|
||||||
|
"label": "美国"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "japan",
|
||||||
|
"label": "日本"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "singapore",
|
||||||
|
"label": "新加坡"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -3,8 +3,9 @@ import { ElMessage } from 'element-plus'
|
|||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
// 基础URL
|
// 基础URL
|
||||||
const baseUrl = 'https://apiservertest.s1f.ren'
|
const baseUrl = 'https://apiservertest.s1f.ren' // SSL证书有问题
|
||||||
// const baseUrl = 'https://cloudapi.007yjs.com'
|
// const baseUrl = 'http://apiservertest.s1f.ren' // HTTP版本
|
||||||
|
// const baseUrl = 'https://cloudapi.007yjs.com' // 尝试备用地址
|
||||||
|
|
||||||
// 检查URL是否需要认证
|
// 检查URL是否需要认证
|
||||||
const urlNeedAuth = (url) => {
|
const urlNeedAuth = (url) => {
|
||||||
@@ -93,8 +94,8 @@ class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE 请求
|
// DELETE 请求
|
||||||
delete(url,data={}, config = {}) {
|
delete(url, config = {}) {
|
||||||
return this.instance.delete(url,data, config)
|
return this.instance.delete(url, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH 请求
|
// PATCH 请求
|
||||||
|
|||||||
@@ -52,3 +52,8 @@ export function timeToTimestamp(time) {
|
|||||||
|
|
||||||
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000)
|
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function reducenum(num){
|
||||||
|
return num / 100
|
||||||
|
}
|
||||||
@@ -1,17 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="guacamole-container">
|
<div class="guacamole-container">
|
||||||
<!-- 页面标题和操作按钮 -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="left">
|
|
||||||
<h2 class="title">远程桌面网关管理</h2>
|
|
||||||
<el-tag type="info" effect="plain" class="count-tag">共 {{ guacamoleStats.total }} 个配置</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">添加配置</el-button>
|
|
||||||
<el-button @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<div class="stats-panel">
|
<div class="stats-panel">
|
||||||
<div class="stat-card total-card">
|
<div class="stat-card total-card">
|
||||||
@@ -37,32 +25,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索和筛选 -->
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
|
<el-form :inline="true" :model="filterForm" class="search-form">
|
||||||
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="filterForm.url"
|
v-model="filterForm.url"
|
||||||
placeholder="搜索 Guacamole URL"
|
placeholder="搜索 Guacamole URL"
|
||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
class="search-input"
|
style="width: 300px"
|
||||||
/>
|
/>
|
||||||
<div class="filter-actions">
|
</el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
<el-form-item>
|
||||||
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetFilter">
|
||||||
|
<el-icon><refresh /></el-icon>重置
|
||||||
|
</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 @click="handleRefresh">
|
||||||
|
<el-icon><refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Guacamole 配置列表 -->
|
<!-- Guacamole 配置列表 -->
|
||||||
<div class="table-container">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="guacamoleData"
|
:data="guacamoleData"
|
||||||
border
|
|
||||||
stripe
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
table-layout="auto"
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
class="guacamole-table"
|
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
|
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
|
||||||
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
|
||||||
@@ -70,11 +74,12 @@
|
|||||||
<el-table-column label="密码" width="120" align="center">
|
<el-table-column label="密码" width="120" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
type="text"
|
type="primary"
|
||||||
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="togglePasswordVisibility(scope.row)"
|
@click="togglePasswordVisibility(scope.row)"
|
||||||
:icon="scope.row.showPassword ? View : Hide"
|
|
||||||
>
|
>
|
||||||
|
<el-icon style="margin-right: 4px"><component :is="scope.row.showPassword ? View : Hide" /></el-icon>
|
||||||
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
|
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,30 +91,25 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="action-buttons">
|
|
||||||
<el-tooltip content="编辑配置" placement="top" :hide-after="1500">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="primary"
|
||||||
:icon="Edit"
|
link
|
||||||
circle
|
|
||||||
@click="handleEdit(scope.row)"
|
@click="handleEdit(scope.row)"
|
||||||
/>
|
>
|
||||||
</el-tooltip>
|
<el-icon><edit /></el-icon>编辑
|
||||||
<el-tooltip content="删除配置" placement="top" :hide-after="1500">
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
:icon="Delete"
|
link
|
||||||
circle
|
|
||||||
@click="handleDelete(scope.row)"
|
@click="handleDelete(scope.row)"
|
||||||
/>
|
>
|
||||||
</el-tooltip>
|
<el-icon><delete /></el-icon>删除
|
||||||
</div>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -119,9 +119,10 @@
|
|||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
background
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑配置对话框 -->
|
<!-- 添加/编辑配置对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -510,42 +511,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.guacamole-container {
|
.guacamole-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面标题样式 */
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-tag {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片 */
|
/* 统计卡片 */
|
||||||
@@ -558,18 +524,18 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
@@ -608,60 +574,64 @@ onMounted(async () => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索和筛选部分 */
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
justify-content: space-between;
|
||||||
margin-bottom: 24px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
padding: 16px 20px;
|
||||||
padding: 16px;
|
gap: 20px;
|
||||||
border-radius: 8px;
|
flex-wrap: wrap;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格容器 */
|
.search-form :deep(.el-form-item) {
|
||||||
.table-container {
|
margin-bottom: 0;
|
||||||
background: white;
|
margin-right: 12px;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guacamole-table {
|
.action-bar {
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 操作按钮 */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 8px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页 */
|
.table-section {
|
||||||
.pagination-container {
|
padding: 0;
|
||||||
display: flex;
|
}
|
||||||
justify-content: flex-end;
|
|
||||||
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单样式 */
|
/* 表单样式 */
|
||||||
@@ -676,14 +646,6 @@ onMounted(async () => {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px dashed #ebeef5;
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框底部 */
|
/* 对话框底部 */
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -696,6 +658,37 @@ onMounted(async () => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
@@ -708,17 +701,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -727,13 +709,18 @@ onMounted(async () => {
|
|||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.filter-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-form {
|
||||||
max-width: none;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-images-container" v-loading="loading">
|
<div class="container-images-container" v-loading="loading">
|
||||||
<div class="page-header">
|
<el-card class="main-container" shadow="never">
|
||||||
<h2>容器镜像</h2>
|
<!-- 搜索和操作栏 -->
|
||||||
<div class="header-actions">
|
<div class="filter-section">
|
||||||
<el-button type="primary" @click="handleRefresh">
|
<div class="filter-content">
|
||||||
<el-icon><refresh /></el-icon>刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 服务器选择和搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="服务器">
|
<el-form-item label="服务器">
|
||||||
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
||||||
@@ -23,7 +16,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="镜像名称">
|
<el-form-item label="镜像名称">
|
||||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch">
|
<el-button type="primary" @click="handleSearch">
|
||||||
@@ -34,10 +27,16 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleRefresh">
|
||||||
|
<el-icon><refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 当前服务器镜像列表 -->
|
<!-- 当前服务器镜像列表 -->
|
||||||
<div v-if="currentServer" class="server-section">
|
<div v-if="currentServer" class="table-section">
|
||||||
<div class="server-header">
|
<div class="server-header">
|
||||||
<h3>{{ currentServer.name }}</h3>
|
<h3>{{ currentServer.name }}</h3>
|
||||||
<div class="server-actions">
|
<div class="server-actions">
|
||||||
@@ -50,12 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card class="table-card">
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="currentMirrorList"
|
:data="currentMirrorList"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="image_id"
|
row-key="image_id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column label="镜像信息" min-width="250">
|
<el-table-column label="镜像信息" min-width="250">
|
||||||
@@ -65,7 +63,7 @@
|
|||||||
<div class="image-info-content">
|
<div class="image-info-content">
|
||||||
<div class="image-name-row">
|
<div class="image-name-row">
|
||||||
<span class="table-image-name">{{ scope.row.name }}</span>
|
<span class="table-image-name">{{ scope.row.name }}</span>
|
||||||
<el-tag>{{ scope.row.tag || '无标签' }}</el-tag>
|
<el-tag size="small">{{ scope.row.tag || '无标签' }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
|
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,17 +102,17 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="currentPage"
|
v-model:current-page="currentPage"
|
||||||
:page-size="10"
|
:page-size="10"
|
||||||
:total="total"
|
:total="total"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
@current-change="handleCurrentPageChange"
|
@current-change="handleCurrentPageChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 镜像详情对话框 -->
|
<!-- 镜像详情对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -201,12 +199,6 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- <el-form-item label="分类ID" prop="class_id">
|
|
||||||
<el-input v-model="form.class_id" placeholder="请输入分类ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分类名称" prop="class_name">
|
|
||||||
<el-input v-model="form.class_name" placeholder="请输入分类名称" />
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item label="图标">
|
<el-form-item label="图标">
|
||||||
<div class="image-icon-upload">
|
<div class="image-icon-upload">
|
||||||
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
|
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
|
||||||
@@ -986,65 +978,65 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container-images-container {
|
.container-images-container {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
padding: 16px 20px;
|
||||||
background: #fff;
|
gap: 20px;
|
||||||
padding: 16px 24px;
|
flex-wrap: wrap;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-section {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-header {
|
.server-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 16px 24px;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-header h3 {
|
.server-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1055,7 +1047,7 @@ onMounted(() => {
|
|||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
background-color: #409EFF;
|
background-color: #409EFF;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@@ -1066,12 +1058,6 @@ onMounted(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-cell {
|
.image-info-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1118,11 +1104,12 @@ onMounted(() => {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情对话框样式 */
|
/* 详情对话框样式 */
|
||||||
@@ -1263,75 +1250,34 @@ onMounted(() => {
|
|||||||
background-color: #ecf5ff;
|
background-color: #ecf5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话框样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-dialog__header) {
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__footer) {
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f5f7fa;
|
background: #f8f9fa !important;
|
||||||
color: #606266;
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row:hover) {
|
:deep(.el-table td) {
|
||||||
background-color: #ecf5ff !important;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link) {
|
:deep(.el-table tr:hover > td) {
|
||||||
padding: 4px 8px;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link):hover {
|
:deep(.el-card__body) {
|
||||||
background-color: #f0f2f5;
|
padding: 0;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.server-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-cell {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-content {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-image-logo {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-categories-container" v-loading="loading">
|
<div class="image-categories-container" v-loading="loading">
|
||||||
<!-- 页面标题 -->
|
<el-card class="main-container" shadow="never">
|
||||||
<div class="page-header">
|
<!-- 搜索和操作栏 -->
|
||||||
<h2>镜像分类管理</h2>
|
<div class="filter-section">
|
||||||
<p class="page-description">管理不同服务器下的镜像分类</p>
|
<div class="filter-content">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 操作栏 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" class="search-form">
|
<el-form :inline="true" class="search-form">
|
||||||
<el-form-item label="服务器">
|
<el-form-item label="服务器">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -31,9 +27,16 @@
|
|||||||
placeholder="搜索分类名称"
|
placeholder="搜索分类名称"
|
||||||
clearable
|
clearable
|
||||||
@input="handleSearch"
|
@input="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="action-bar">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAddCategory"
|
@click="handleAddCategory"
|
||||||
@@ -41,19 +44,17 @@
|
|||||||
>
|
>
|
||||||
<el-icon><plus /></el-icon>添加分类
|
<el-icon><plus /></el-icon>添加分类
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
</el-form>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<el-card class="table-card">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="filteredCategoryList"
|
:data="filteredCategoryList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
border
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
stripe
|
|
||||||
highlight-current-row
|
|
||||||
>
|
>
|
||||||
<el-table-column type="index" width="60" align="center" label="序号" />
|
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||||
<el-table-column prop="name" label="分类名称" min-width="150" />
|
<el-table-column prop="name" label="分类名称" min-width="150" />
|
||||||
@@ -64,8 +65,9 @@
|
|||||||
:size="40"
|
:size="40"
|
||||||
:src="scope.row.class_ico"
|
:src="scope.row.class_ico"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
|
style="background-color: #f5f7fa; border: 1px solid #ebeef5;"
|
||||||
/>
|
/>
|
||||||
<el-icon v-else :size="20"><picture /></el-icon>
|
<el-icon v-else :size="20" color="#909399"><picture /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="所属服务器" min-width="150">
|
<el-table-column label="所属服务器" min-width="150">
|
||||||
@@ -73,15 +75,11 @@
|
|||||||
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
|
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="created_at" label="创建时间" min-width="180">
|
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||||||
<template #default="scope">
|
|
||||||
{{ scope.row.created_at }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
type="success"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="handleEditCategory(scope.row)"
|
@click="handleEditCategory(scope.row)"
|
||||||
>
|
>
|
||||||
@@ -92,16 +90,16 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页器 -->
|
<!-- 分页器 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
background
|
background
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
layout="total, sizes, prev, pager, next"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.image-categories-container {
|
.image-categories-container {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
margin-bottom: 24px;
|
align-items: center;
|
||||||
background: #fff;
|
padding: 16px 20px;
|
||||||
padding: 16px 24px;
|
gap: 20px;
|
||||||
border-radius: 8px;
|
flex-wrap: wrap;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-description {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 0;
|
||||||
border-radius: 8px;
|
margin-right: 12px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.action-bar {
|
||||||
display: flex;
|
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;
|
justify-content: flex-end;
|
||||||
margin-top: 16px;
|
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图片上传区域样式 */
|
/* 图片上传区域样式 */
|
||||||
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
|
|||||||
background-color: #ecf5ff;
|
background-color: #ecf5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话框样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-dialog__header) {
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__footer) {
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f5f7fa;
|
background: #f8f9fa !important;
|
||||||
color: #606266;
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row:hover) {
|
:deep(.el-table td) {
|
||||||
background-color: #ecf5ff !important;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link) {
|
:deep(.el-table tr:hover > td) {
|
||||||
padding: 4px 8px;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link):hover {
|
:deep(.el-card__body) {
|
||||||
background-color: #f0f2f5;
|
padding: 0;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.image-icon-upload {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-buttons {
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,825 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-form-container">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button @click="goBack" class="back-btn" circle>
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<div class="header-title-area">
|
||||||
|
<h1 class="page-title">{{ isEdit ? '编辑镜像' : '添加镜像' }}</h1>
|
||||||
|
<span class="page-subtitle">{{ isEdit ? '修改现有镜像的配置信息' : '上传并配置新的虚拟机镜像' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button @click="goBack" size="large">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
|
||||||
|
{{ isEdit ? '保存修改' : '立即创建' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主表单区域 -->
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
|
||||||
|
|
||||||
|
<!-- 左侧:主要配置 -->
|
||||||
|
<div class="form-main-col">
|
||||||
|
<el-card class="premium-card" shadow="never">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>基础信息</h3>
|
||||||
|
<p>配置镜像的基本标识与文件信息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="镜像名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入镜像名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="展示名称" prop="show_name">
|
||||||
|
<el-input v-model="form.show_name" placeholder="请输入展示名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="文件路径" prop="path">
|
||||||
|
<el-input v-model="form.path" placeholder="请输入镜像文件在服务器上的绝对路径">
|
||||||
|
<template #prefix><el-icon><Folder /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="镜像描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入关于此镜像的详细描述"
|
||||||
|
resize="none"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon"><el-icon><SetUp /></el-icon></div>
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>分类与版本</h3>
|
||||||
|
<p>管理镜像的分类归属与版本信息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="所属分类" prop="class_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.class_id"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleCategoryChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in categoryList" :key="item.class_id" :label="item.name" :value="item.class_id" />
|
||||||
|
<el-option label="+ 创建新分类" value="" class="create-new-option" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<div class="new-category-input" v-if="showNewCategoryInput">
|
||||||
|
<el-input
|
||||||
|
v-model="form.class_name"
|
||||||
|
placeholder="输入新分类名称"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="createNewCategory">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="版本号" prop="vm_gen">
|
||||||
|
<el-input v-model="form.vm_gen" placeholder="例如:v1.0.0" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="关联套餐" prop="plan_id">
|
||||||
|
<el-select v-model="form.plan_id" placeholder="请选择适用的套餐" style="width: 100%">
|
||||||
|
<el-option v-for="item in planList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:图标与高级 -->
|
||||||
|
<div class="form-side-col">
|
||||||
|
<el-card class="premium-card" shadow="never">
|
||||||
|
<div class="section-header small">
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>镜像图标</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-uploader">
|
||||||
|
<div class="icon-preview" v-if="form.image_ico">
|
||||||
|
<img :src="mainUrl + form.image_ico" />
|
||||||
|
<div class="icon-actions">
|
||||||
|
<el-button size="small" circle @click="form.image_ico = ''"><el-icon><Delete /></el-icon></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-area" v-else>
|
||||||
|
<div class="upload-placeholder">
|
||||||
|
<el-icon class="upload-icon"><Picture /></el-icon>
|
||||||
|
<div class="upload-text">点击上传或选择图标</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-buttons">
|
||||||
|
<el-button type="primary" size="small" @click="$refs.fileInput.click()">本地上传</el-button>
|
||||||
|
<el-button size="small" @click="openPicLibrary">素材库</el-button>
|
||||||
|
</div>
|
||||||
|
<input ref="fileInput" type="file" style="display: none" @change="onFileSelected" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 素材库对话框 -->
|
||||||
|
<el-dialog v-model="picSwitch" title="选择图标" width="800px" append-to-body>
|
||||||
|
<div class="pic-search">
|
||||||
|
<el-input
|
||||||
|
v-model="picPagin.key"
|
||||||
|
placeholder="搜索图标..."
|
||||||
|
prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
@change="getpicList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pic-grid" v-loading="picLoading">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in picList"
|
||||||
|
:key="index"
|
||||||
|
class="pic-item"
|
||||||
|
:class="{ active: currentIndex === index }"
|
||||||
|
@click="selectImage(index)"
|
||||||
|
>
|
||||||
|
<img :src="`${mainUrl}/v1/attachment/get_attachment?aid=${item.attachment_id}`" />
|
||||||
|
<div class="pic-name">{{ item.title || '未命名' }}</div>
|
||||||
|
<div class="pic-check" v-if="currentIndex === index"><el-icon><Check /></el-icon></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
:current-page="picPagin.page"
|
||||||
|
:page-size="picPagin.count"
|
||||||
|
@current-change="CurrentPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="picSwitch = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmPicSelection" :disabled="currentIndex === null">确定选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElNotification } from 'element-plus'
|
||||||
|
import {
|
||||||
|
ArrowLeft, Monitor, Folder, SetUp, Picture, Delete,
|
||||||
|
Search, Check
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { getServerPlan } from '@/utils/acs/server'
|
||||||
|
import {
|
||||||
|
editMirror, addVirtualMirror, getImageTypeList, createImageType, getUserMirrorList
|
||||||
|
} from '@/utils/acs/mirror'
|
||||||
|
import { uploadFile, getFileList } from '@/utils/acs/message'
|
||||||
|
import { mainUrl } from '@/utils/request'
|
||||||
|
|
||||||
|
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const tagsViewStore = useTagsViewStore()
|
||||||
|
const formRef = ref(null)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
tagsViewStore.delVisitedView(route)
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
const isEdit = computed(() => !!route.query.id)
|
||||||
|
const serverId = computed(() => route.query.server_id)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
show_name: '',
|
||||||
|
description: '',
|
||||||
|
server_type: 'hyperV',
|
||||||
|
plan_id: '',
|
||||||
|
image_ico: '',
|
||||||
|
server_id: '',
|
||||||
|
path: '',
|
||||||
|
class_id: '',
|
||||||
|
class_name: '',
|
||||||
|
vm_gen: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
|
||||||
|
path: [{ required: true, message: '请输入文件路径', trigger: 'blur' }],
|
||||||
|
show_name: [{ required: true, message: '请输入展示名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryList = ref([])
|
||||||
|
const planList = ref([])
|
||||||
|
const showNewCategoryInput = ref(false)
|
||||||
|
|
||||||
|
// 素材库相关
|
||||||
|
const picSwitch = ref(false)
|
||||||
|
const picLoading = ref(false)
|
||||||
|
const picPagin = reactive({
|
||||||
|
count: 20,
|
||||||
|
page: 1,
|
||||||
|
key: '',
|
||||||
|
user_type: 1
|
||||||
|
})
|
||||||
|
const picList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const currentIndex = ref(null)
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(form, {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
show_name: '',
|
||||||
|
description: '',
|
||||||
|
server_type: 'hyperV',
|
||||||
|
plan_id: '',
|
||||||
|
image_ico: '',
|
||||||
|
server_id: '',
|
||||||
|
path: '',
|
||||||
|
class_id: '',
|
||||||
|
class_name: '',
|
||||||
|
vm_gen: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = async () => {
|
||||||
|
resetForm()
|
||||||
|
|
||||||
|
if (!serverId.value) {
|
||||||
|
ElMessage.error('缺少服务器ID参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.server_id = serverId.value
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取套餐列表
|
||||||
|
const planRes = await getServerPlan({ server_id: serverId.value })
|
||||||
|
if (planRes.data.code === 200) {
|
||||||
|
planList.value = planRes.data.data.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
id: item.plan_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类列表
|
||||||
|
await fetchCategoryList()
|
||||||
|
|
||||||
|
// 如果是编辑模式,填充数据
|
||||||
|
if (isEdit.value) {
|
||||||
|
const id = route.query.id
|
||||||
|
// 尝试从 history.state 获取数据
|
||||||
|
const stateData = history.state.params ? JSON.parse(JSON.stringify(history.state.params)) : null
|
||||||
|
|
||||||
|
if (stateData && stateData.id == id) {
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
if (key in stateData) {
|
||||||
|
form[key] = stateData[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 确保 ID 类型一致性 (有些时候 API 返回的是数字,有些时候是字符串)
|
||||||
|
if (form.plan_id) form.plan_id = Number(form.plan_id) || form.plan_id
|
||||||
|
if (form.class_id) form.class_id = Number(form.class_id) || form.class_id
|
||||||
|
} else {
|
||||||
|
// Fallback: fetch list and find item
|
||||||
|
const listRes = await getUserMirrorList({
|
||||||
|
server_id: serverId.value,
|
||||||
|
count: 100,
|
||||||
|
page: 1
|
||||||
|
})
|
||||||
|
if (listRes.data.code === 200) {
|
||||||
|
const found = listRes.data.data.find(item => item.id == id)
|
||||||
|
if (found) {
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
if (key in found) form[key] = found[key]
|
||||||
|
})
|
||||||
|
if (form.plan_id) form.plan_id = Number(form.plan_id) || form.plan_id
|
||||||
|
if (form.class_id) form.class_id = Number(form.class_id) || form.class_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化数据失败:', error)
|
||||||
|
ElMessage.error('数据加载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCategoryList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getImageTypeList(serverId.value)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
categoryList.value = res.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCategoryChange = (val) => {
|
||||||
|
if (val === '') {
|
||||||
|
// 选择了创建新分类
|
||||||
|
form.class_id = ''
|
||||||
|
showNewCategoryInput.value = true
|
||||||
|
} else {
|
||||||
|
showNewCategoryInput.value = false
|
||||||
|
form.class_name = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewCategory = async () => {
|
||||||
|
if (!form.class_name.trim()) {
|
||||||
|
ElMessage.warning('请输入分类名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await createImageType(serverId.value, form.class_name.trim(), '')
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('分类创建成功')
|
||||||
|
await fetchCategoryList()
|
||||||
|
// 选中新创建的分类
|
||||||
|
const newCat = categoryList.value.find(c => c.name === form.class_name.trim())
|
||||||
|
if (newCat) {
|
||||||
|
form.class_id = newCat.class_id
|
||||||
|
showNewCategoryInput.value = false
|
||||||
|
form.class_name = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.msg || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('创建分类失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传与选择
|
||||||
|
const onFileSelected = async (event) => {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadFile({ file })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
form.image_ico = '/v1/attachment/get_attachment?aid=' + res.data.data.attachment_id
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('上传出错')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPicLibrary = () => {
|
||||||
|
picSwitch.value = true
|
||||||
|
getpicList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getpicList = async () => {
|
||||||
|
picLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getFileList(picPagin)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
picList.value = res.data.data
|
||||||
|
total.value = res.data.count
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
picLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectImage = (index) => {
|
||||||
|
currentIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmPicSelection = () => {
|
||||||
|
if (currentIndex.value !== null) {
|
||||||
|
const item = picList.value[currentIndex.value]
|
||||||
|
form.image_ico = `/v1/attachment/get_attachment?aid=${item.attachment_id}`
|
||||||
|
picSwitch.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CurrentPageChange = (page) => {
|
||||||
|
picPagin.page = page
|
||||||
|
getpicList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const submitData = { ...form }
|
||||||
|
|
||||||
|
// 处理分类逻辑
|
||||||
|
if (submitData.class_id) {
|
||||||
|
submitData.class_name = ''
|
||||||
|
} else if (submitData.class_name) {
|
||||||
|
submitData.class_id = ''
|
||||||
|
} else {
|
||||||
|
submitData.class_id = ''
|
||||||
|
submitData.class_name = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (isEdit.value) {
|
||||||
|
submitData.image_id = submitData.id
|
||||||
|
delete submitData.id
|
||||||
|
res = await editMirror(submitData)
|
||||||
|
} else {
|
||||||
|
res = await addVirtualMirror(submitData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElNotification({
|
||||||
|
title: '操作成功',
|
||||||
|
message: isEdit.value ? '镜像更新成功' : '镜像创建成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
goBack()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.msg || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-form-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航 */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 20px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
border: none;
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #606266;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: #e6e8eb;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单布局 */
|
||||||
|
.form-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-form {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-main-col {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-side-col {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.premium-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-card:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-card :deep(.el-card__body) {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 章节标题 */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.small {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
||||||
|
color: #409EFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info p {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单网格 */
|
||||||
|
.form-grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新分类输入 */
|
||||||
|
.new-category-input {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标上传器 */
|
||||||
|
.icon-uploader {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: #409EFF;
|
||||||
|
background: #f2f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 素材库网格 */
|
||||||
|
.pic-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-item {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-item:hover {
|
||||||
|
border-color: #409EFF;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-item.active {
|
||||||
|
border-color: #409EFF;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-item img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.main-form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-side-col {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.image-form-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .el-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,42 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-requests-container">
|
<div class="image-requests-container">
|
||||||
<div class="page-header">
|
<el-card class="main-container" shadow="never">
|
||||||
<h2>申请镜像</h2>
|
<!-- 搜索和操作栏 -->
|
||||||
<div class="header-actions">
|
<div class="filter-section">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<div class="filter-content">
|
||||||
<el-icon><plus /></el-icon>申请镜像
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleRefresh">
|
|
||||||
<el-icon><refresh /></el-icon>刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="info-alert"
|
|
||||||
>
|
|
||||||
<el-icon><info-filled /></el-icon>
|
|
||||||
申请的镜像需要经过安全审核,审核通过后可在创建云电脑或容器时使用,审核结果将通过站内信通知。
|
|
||||||
</el-alert>
|
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="镜像名称">
|
<el-form-item label="镜像名称">
|
||||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="镜像类型">
|
<el-form-item label="镜像类型">
|
||||||
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable>
|
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable style="width: 150px">
|
||||||
<el-option label="Docker镜像" value="docker" />
|
<el-option label="Docker镜像" value="docker" />
|
||||||
<el-option label="Windows镜像" value="windows" />
|
<el-option label="Windows镜像" value="windows" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="申请状态">
|
<el-form-item label="申请状态">
|
||||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||||
<el-option label="已通过" value="approved" />
|
<el-option label="已通过" value="approved" />
|
||||||
<el-option label="审核中" value="pending" />
|
<el-option label="审核中" value="pending" />
|
||||||
<el-option label="已拒绝" value="rejected" />
|
<el-option label="已拒绝" value="rejected" />
|
||||||
@@ -51,16 +30,38 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><plus /></el-icon>申请镜像
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleRefresh">
|
||||||
|
<el-icon><refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<el-alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="info-alert"
|
||||||
|
style="margin: 20px 20px 0; width: auto;"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
申请的镜像需要经过安全审核,审核通过后可在创建云电脑或容器时使用,审核结果将通过站内信通知。
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-card class="table-card">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="申请ID" width="150" align="center" />
|
<el-table-column prop="id" label="申请ID" width="150" align="center" />
|
||||||
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
|
||||||
@@ -97,7 +98,6 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -106,6 +106,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -589,41 +591,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.image-requests-container {
|
.image-requests-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-alert {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
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;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,12 +656,16 @@ onMounted(() => {
|
|||||||
/* 环境变量配置样式 */
|
/* 环境变量配置样式 */
|
||||||
.env-vars-container {
|
.env-vars-container {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-vars-header {
|
.env-vars-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-vars-item {
|
.env-vars-item {
|
||||||
@@ -665,17 +688,23 @@ onMounted(() => {
|
|||||||
|
|
||||||
.add-env-btn {
|
.add-env-btn {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 端口配置样式 */
|
/* 端口配置样式 */
|
||||||
.ports-container {
|
.ports-container {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ports-header {
|
.ports-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ports-item {
|
.ports-item {
|
||||||
@@ -702,6 +731,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
.add-port-btn {
|
.add-port-btn {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情样式 */
|
/* 详情样式 */
|
||||||
@@ -710,11 +741,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.request-reason {
|
.request-reason {
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f9fa;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-comment {
|
.review-comment {
|
||||||
@@ -724,5 +757,37 @@ onMounted(() => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
border-left: 4px solid #67c23a;
|
border-left: 4px solid #67c23a;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="vm-images-container" v-loading="loading">
|
<div class="vm-images-container" v-loading="loading">
|
||||||
<div class="page-header">
|
<el-card class="main-container" shadow="never">
|
||||||
<h2>虚拟机镜像</h2>
|
<!-- 搜索和操作栏 -->
|
||||||
<div class="header-actions">
|
<div class="filter-section">
|
||||||
<el-button type="primary" @click="toLoad(selectedServer)" :disabled="!selectedServer">
|
<div class="filter-content">
|
||||||
<el-icon><plus /></el-icon>添加镜像
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="服务器">
|
<el-form-item label="服务器">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -29,10 +22,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="镜像名称">
|
<el-form-item label="镜像名称">
|
||||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="操作系统">
|
<el-form-item label="操作系统">
|
||||||
<el-select v-model="searchForm.os" placeholder="请选择操作系统" clearable>
|
<el-select v-model="searchForm.os" placeholder="请选择操作系统" clearable style="width: 150px">
|
||||||
<el-option label="Windows" value="windows" />
|
<el-option label="Windows" value="windows" />
|
||||||
<el-option label="Ubuntu" value="ubuntu" />
|
<el-option label="Ubuntu" value="ubuntu" />
|
||||||
<el-option label="CentOS" value="centos" />
|
<el-option label="CentOS" value="centos" />
|
||||||
@@ -41,7 +34,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||||
<el-option label="可用" value="available" />
|
<el-option label="可用" value="available" />
|
||||||
<el-option label="创建中" value="creating" />
|
<el-option label="创建中" value="creating" />
|
||||||
<el-option label="已禁用" value="disabled" />
|
<el-option label="已禁用" value="disabled" />
|
||||||
@@ -56,16 +49,22 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="toLoad(selectedServer)" :disabled="!selectedServer">
|
||||||
|
<el-icon><plus /></el-icon>添加镜像
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 镜像列表 -->
|
<!-- 镜像列表 -->
|
||||||
<el-card class="table-card" v-if="selectedServer">
|
<div class="table-section" v-if="selectedServer">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="tableLoading"
|
v-loading="tableLoading"
|
||||||
:data="currentImages"
|
:data="currentImages"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column label="镜像信息" min-width="250">
|
<el-table-column label="镜像信息" min-width="250">
|
||||||
@@ -111,19 +110,18 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
background
|
background
|
||||||
layout="total, sizes, prev, pager, next"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
@update:current-page="handleCurrentPageChange"
|
@update:current-page="handleCurrentPageChange"
|
||||||
@update:page-size="handleSizeChange"
|
@update:page-size="handleSizeChange"
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 未选择服务器时的提示 -->
|
<!-- 未选择服务器时的提示 -->
|
||||||
<el-empty
|
<el-empty
|
||||||
@@ -131,155 +129,26 @@
|
|||||||
description="请选择一个服务器查看镜像列表"
|
description="请选择一个服务器查看镜像列表"
|
||||||
class="empty-tip"
|
class="empty-tip"
|
||||||
/>
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 镜像表单对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="editOr ? '编辑镜像' : '添加镜像'"
|
|
||||||
width="600px"
|
|
||||||
:before-close="handleDialogClose"
|
|
||||||
>
|
|
||||||
<el-form :model="imageForm" label-width="120px" :rules="rules" ref="imageFormRef">
|
|
||||||
<el-form-item label="镜像名称" prop="name">
|
|
||||||
<el-input v-model="imageForm.name" placeholder="请输入镜像名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="文件路径" prop="path" >
|
|
||||||
<el-input v-model="imageForm.path" placeholder="请输入镜像文件路径" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="展示名称" prop="show_name">
|
|
||||||
<el-input v-model="imageForm.show_name" placeholder="请输入展示名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分类" prop="class_id" >
|
|
||||||
<div class="category-select-container">
|
|
||||||
<el-select
|
|
||||||
v-model="imageForm.class_id"
|
|
||||||
placeholder="请选择分类"
|
|
||||||
clearable
|
|
||||||
style="width: 100%"
|
|
||||||
@change="handleCategoryChange"
|
|
||||||
>
|
|
||||||
<el-option v-for="item in categoryList" :key="item.class_id" :label="item.name" :value="item.class_id" />
|
|
||||||
</el-select>
|
|
||||||
<div class="category-input-container" v-if="showNewCategoryInput">
|
|
||||||
<el-input
|
|
||||||
v-model="imageForm.class_name"
|
|
||||||
placeholder="请输入新分类名称"
|
|
||||||
style="width: 100%; margin-top: 8px"
|
|
||||||
/>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="createNewCategory"
|
|
||||||
style="margin-top: 8px"
|
|
||||||
>
|
|
||||||
创建新分类
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="图标" prop="image_ico">
|
|
||||||
<div class="image-icon-upload">
|
|
||||||
<img v-if="imageForm.image_ico" :src="mainUrl + imageForm.image_ico" class="preview-icon" />
|
|
||||||
<div class="upload-buttons">
|
|
||||||
<el-button type="primary" @click="$refs.fileInput.click()">选择文件</el-button>
|
|
||||||
<input ref="fileInput" type="file" style="display: none" @change="onFileSelected" />
|
|
||||||
<div class="el-upload__tip" v-if="selectedFileName">
|
|
||||||
已选择: {{ selectedFileName }}
|
|
||||||
</div>
|
|
||||||
<el-button @click="picSwitch = true; getpicList()">从素材库选择</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="套餐" prop="plan_id">
|
|
||||||
<el-select v-model="imageForm.plan_id" placeholder="请选择套餐" style="width: 100%">
|
|
||||||
<el-option v-for="item in planlist" :key="item.id" :label="item.name" :value="item.id" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="版本" prop="vm_gen">
|
|
||||||
<el-input v-model="imageForm.vm_gen" placeholder="请输入版本"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="描述" prop="description">
|
|
||||||
<el-input
|
|
||||||
v-model="imageForm.description"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="请输入镜像描述"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="getit">一键粘贴内容</el-button>
|
|
||||||
<el-button @click="copyit">一键复制内容</el-button>
|
|
||||||
<el-button @click="handleDialogClose">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 素材库对话框 -->
|
|
||||||
<el-dialog v-model="picSwitch" title="素材库" width="70%" :before-close="() => picSwitch = false">
|
|
||||||
<div class="pic-search">
|
|
||||||
<el-input
|
|
||||||
v-model="picPagin.key"
|
|
||||||
placeholder="请输入搜索内容"
|
|
||||||
style="width: 300px; margin-bottom: 20px"
|
|
||||||
@blur="getpicList()"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="getpicList()">
|
|
||||||
<el-icon><search /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
<div class="piclist">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in picList"
|
|
||||||
:key="index"
|
|
||||||
class="icon"
|
|
||||||
:class="{ choose: currentIndex === index }"
|
|
||||||
@click="selectImage(index, item)"
|
|
||||||
>
|
|
||||||
<img :src="`${mainUrl}/v1/attachment/get_attachment?aid=${item.attachment_id}`" />
|
|
||||||
<div class="tit">{{ item.title ? item.title.slice(0, 8) : '未命名' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pagination-container" style="margin-top: 20px">
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
:total="total"
|
|
||||||
:current-page="picPagin.page"
|
|
||||||
:page-size="picPagin.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
@current-change="CurrentPageChange"
|
|
||||||
@size-change="PageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="picSwitch = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="tochoose">确认选择</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
Plus, Search, Refresh, Edit, Delete, TurnOff, Open, Monitor
|
Plus, Search, Refresh, Edit, Delete, TurnOff, Open, Monitor
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getServer, getServerPlan } from '@/utils/acs/server'
|
import { getServer } from '@/utils/acs/server'
|
||||||
import {
|
import {
|
||||||
editMirror, delMirror, getUserMirrorList, addVirtualMirror, getImageTypeList, createImageType
|
delMirror, getUserMirrorList, getImageTypeList
|
||||||
} from '@/utils/acs/mirror'
|
} from '@/utils/acs/mirror'
|
||||||
import { uploadFile, getFileList } from '@/utils/acs/message'
|
|
||||||
import { mainUrl } from '@/utils/request'
|
import { mainUrl } from '@/utils/request'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -305,52 +174,6 @@ const currentPage = ref(1)
|
|||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
|
|
||||||
// 对话框相关
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const editOr = ref(false)
|
|
||||||
const selectedFileName = ref(null)
|
|
||||||
const planlist = ref([])
|
|
||||||
const nowserver_id = ref('')
|
|
||||||
|
|
||||||
// 表单对象和规则
|
|
||||||
const imageFormRef = ref(null)
|
|
||||||
const imageForm = reactive({
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
show_name: '',
|
|
||||||
description: '',
|
|
||||||
server_type: 'hyperV',
|
|
||||||
plan_id: '',
|
|
||||||
image_ico: '',
|
|
||||||
server_id: '',
|
|
||||||
path: '',
|
|
||||||
class_id: '',
|
|
||||||
class_name: '',
|
|
||||||
vm_gen: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
// 根据接口文档,所有字段都是可选的
|
|
||||||
// name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
|
|
||||||
// path: [{ required: true, message: '请输入镜像文件路径', trigger: 'blur' }],
|
|
||||||
// show_name: [{ required: true, message: '请输入展示名称', trigger: 'blur' }],
|
|
||||||
// plan_id: [{ required: true, message: '请选择套餐', trigger: 'change' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 素材库相关
|
|
||||||
const picSwitch = ref(false)
|
|
||||||
const picPagin = reactive({
|
|
||||||
count: 50,
|
|
||||||
page: 1,
|
|
||||||
key: '',
|
|
||||||
user_type: 1
|
|
||||||
})
|
|
||||||
const picList = ref([])
|
|
||||||
const total = ref(10)
|
|
||||||
const currentIndex = ref(null)
|
|
||||||
const categoryList = ref([])
|
|
||||||
const showNewCategoryInput = ref(false)
|
|
||||||
|
|
||||||
// 获取操作系统图标
|
// 获取操作系统图标
|
||||||
const getOsIcon = (os) => {
|
const getOsIcon = (os) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
@@ -462,141 +285,21 @@ const handleSizeChange = (newSize) => {
|
|||||||
fetchImageList()
|
fetchImageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取镜像分类列表
|
|
||||||
const fetchCategoryList = async (serverId) => {
|
|
||||||
try {
|
|
||||||
const response = await getImageTypeList(serverId)
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
categoryList.value = response.data.data || []
|
|
||||||
} else {
|
|
||||||
ElMessage.error('获取镜像分类失败:' + response.data.message)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取镜像分类出错:', error)
|
|
||||||
ElMessage.error('获取镜像分类列表失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理分类选择变化
|
|
||||||
const handleCategoryChange = (value) => {
|
|
||||||
if (value) {
|
|
||||||
// 选择了现有分类,清空新分类名称
|
|
||||||
imageForm.class_name = ''
|
|
||||||
showNewCategoryInput.value = false
|
|
||||||
} else {
|
|
||||||
// 清空选择,显示新分类输入框
|
|
||||||
showNewCategoryInput.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新分类
|
|
||||||
const createNewCategory = async () => {
|
|
||||||
if (!imageForm.class_name.trim()) {
|
|
||||||
ElMessage.warning('请输入分类名称')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await createImageType(nowserver_id.value, imageForm.class_name.trim(), '')
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
ElMessage.success('分类创建成功')
|
|
||||||
// 重新获取分类列表
|
|
||||||
await fetchCategoryList(nowserver_id.value)
|
|
||||||
// 设置新创建的分类为选中状态
|
|
||||||
const newCategory = categoryList.value.find(item => item.name === imageForm.class_name.trim())
|
|
||||||
if (newCategory) {
|
|
||||||
imageForm.class_id = newCategory.class_id
|
|
||||||
imageForm.class_name = ''
|
|
||||||
showNewCategoryInput.value = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error('创建分类失败:' + response.data.message)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建分类出错:', error)
|
|
||||||
ElMessage.error('创建分类失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增镜像
|
// 新增镜像
|
||||||
const toLoad = async (data) => {
|
const toLoad = (serverId) => {
|
||||||
dialogVisible.value = true
|
router.push({
|
||||||
editOr.value = false
|
path: '/acs/images/form',
|
||||||
Object.keys(imageForm).forEach(key => {
|
query: { server_id: serverId }
|
||||||
imageForm[key] = ''
|
|
||||||
})
|
})
|
||||||
imageForm.server_id = data
|
|
||||||
nowserver_id.value = data
|
|
||||||
showNewCategoryInput.value = true // 添加镜像时默认显示新分类输入框
|
|
||||||
try {
|
|
||||||
// 获取套餐列表
|
|
||||||
let res = await getServerPlan({ server_id: data })
|
|
||||||
planlist.value = res.data.data.map(item => {
|
|
||||||
return {
|
|
||||||
name: item.name,
|
|
||||||
id: item.plan_id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 获取分类列表
|
|
||||||
await fetchCategoryList(data)
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('获取数据失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理文件变更
|
|
||||||
const onFileSelected = (event) => {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
selectedFileName.value = file.name
|
|
||||||
uploadFile({ file: file }).then((res) => {
|
|
||||||
if (res.data.code == 200) {
|
|
||||||
ElMessage.success('上传成功')
|
|
||||||
selectedFileName.value = null
|
|
||||||
imageForm.image_ico = '/v1/attachment/get_attachment?aid=' + res.data.data.attachment_id
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
ElMessage.error('上传失败')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
selectedFileName.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加镜像
|
|
||||||
const handleAdd = () => {
|
|
||||||
if (!selectedServer.value) {
|
|
||||||
ElMessage.warning('请先选择一个服务器')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toLoad(selectedServer.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑镜像
|
// 编辑镜像
|
||||||
const handleEdit = async (row) => {
|
const handleEdit = (row) => {
|
||||||
console.log("编辑镜像信息:",row)
|
router.push({
|
||||||
try {
|
path: '/acs/images/form',
|
||||||
let res = await getServerPlan({ server_id: row.server_id })
|
query: { id: row.id, server_id: row.server_id },
|
||||||
planlist.value = res.data.data.map(item => {
|
state: { params: JSON.parse(JSON.stringify(row)) }
|
||||||
return {
|
|
||||||
name: item.name,
|
|
||||||
id: item.plan_id,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
// 获取分类列表
|
|
||||||
await fetchCategoryList(row.server_id)
|
|
||||||
console.log("编辑镜像信息:",row)
|
|
||||||
editOr.value = true
|
|
||||||
dialogVisible.value = true
|
|
||||||
showNewCategoryInput.value = false // 编辑时默认不显示新分类输入框
|
|
||||||
for (const key in row) {
|
|
||||||
if (row.hasOwnProperty(key)) {
|
|
||||||
imageForm[key] = row[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('获取数据失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除镜像
|
// 删除镜像
|
||||||
@@ -624,122 +327,6 @@ const handleCreateVM = (row) => {
|
|||||||
ElMessage.success(`正在使用镜像"${row.name}"创建虚拟机,请前往虚拟机管理页面查看`)
|
ElMessage.success(`正在使用镜像"${row.name}"创建虚拟机,请前往虚拟机管理页面查看`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭对话框
|
|
||||||
const handleDialogClose = () => {
|
|
||||||
dialogVisible.value = false
|
|
||||||
showNewCategoryInput.value = false
|
|
||||||
if (imageFormRef.value) {
|
|
||||||
imageFormRef.value.resetFields()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (!imageFormRef.value) return
|
|
||||||
|
|
||||||
await imageFormRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
// 准备提交数据,根据接口要求处理分类字段
|
|
||||||
const submitData = { ...imageForm }
|
|
||||||
|
|
||||||
// 如果选择了现有分类,清空 class_name
|
|
||||||
if (submitData.class_id) {
|
|
||||||
submitData.class_name = ''
|
|
||||||
}
|
|
||||||
// 如果没有选择现有分类但有新分类名称,清空 class_id
|
|
||||||
else if (submitData.class_name) {
|
|
||||||
submitData.class_id = ''
|
|
||||||
}
|
|
||||||
// 如果都没有,清空两个字段
|
|
||||||
else {
|
|
||||||
submitData.class_id = ''
|
|
||||||
submitData.class_name = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editOr.value) {
|
|
||||||
submitData.image_id = submitData.id
|
|
||||||
delete submitData.id
|
|
||||||
editMirror(submitData).then(res => {
|
|
||||||
if (res.data.code == 200) {
|
|
||||||
dialogVisible.value = false
|
|
||||||
ElMessage.success('编辑成功')
|
|
||||||
fetchImageList()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.data.msg || '编辑失败')
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
ElMessage.error('编辑失败')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
addVirtualMirror(submitData).then(res => {
|
|
||||||
if (res.data.code == 200) {
|
|
||||||
dialogVisible.value = false
|
|
||||||
ElMessage.success('添加成功')
|
|
||||||
fetchImageList()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.data.msg || '添加失败')
|
|
||||||
}
|
|
||||||
}).catch((e) => {
|
|
||||||
ElMessage.error('添加失败')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 素材库相关
|
|
||||||
const getpicList = () => {
|
|
||||||
getFileList(picPagin).then(res => {
|
|
||||||
picList.value = res.data.data
|
|
||||||
total.value = res.data.count
|
|
||||||
}).catch(() => {
|
|
||||||
ElMessage.error('获取图片列表失败')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectImage = (index, data) => {
|
|
||||||
currentIndex.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const CurrentPageChange = async newPage => {
|
|
||||||
picPagin.page = newPage
|
|
||||||
getpicList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageSizeChange = async newSize => {
|
|
||||||
picPagin.count = newSize
|
|
||||||
getpicList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const tochoose = () => {
|
|
||||||
if (currentIndex.value != null) {
|
|
||||||
imageForm.image_ico = `/v1/attachment/get_attachment?aid=${picList.value[currentIndex.value].attachment_id}`
|
|
||||||
picSwitch.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制粘贴相关
|
|
||||||
const copytext = ref({})
|
|
||||||
const copyit = async () => {
|
|
||||||
copytext.value = JSON.parse(JSON.stringify(imageForm))
|
|
||||||
ElMessage.success('复制成功')
|
|
||||||
await navigator.clipboard.writeText(JSON.stringify(copytext.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getit = async () => {
|
|
||||||
try {
|
|
||||||
const text = await navigator.clipboard.readText()
|
|
||||||
Object.keys(JSON.parse(text)).forEach(key => {
|
|
||||||
imageForm[key] = JSON.parse(text)[key]
|
|
||||||
})
|
|
||||||
imageForm.server_id = nowserver_id.value
|
|
||||||
ElMessage.success('粘贴成功')
|
|
||||||
return text
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error('无法读取剪贴板内容,请检查获取到的格式')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchServerList()
|
fetchServerList()
|
||||||
@@ -748,51 +335,59 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.vm-images-container {
|
.vm-images-container {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
padding: 16px 20px;
|
||||||
background: #fff;
|
gap: 20px;
|
||||||
padding: 16px 24px;
|
flex-wrap: wrap;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 0;
|
||||||
border-radius: 8px;
|
margin-right: 12px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-info-cell {
|
.image-info-cell {
|
||||||
@@ -837,176 +432,40 @@ onMounted(() => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-tip {
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-icon-upload {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 6px;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-upload__tip {
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 素材库样式 */
|
|
||||||
.piclist {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.piclist .icon {
|
|
||||||
width: 100px;
|
|
||||||
height: 110px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
transition: all 0.3s;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.piclist .icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.piclist .icon img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.piclist .icon .tit {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.piclist .choose {
|
|
||||||
border-color: #409EFF;
|
|
||||||
box-shadow: 0 0 12px rgba(64, 158, 255, 0.6);
|
|
||||||
background-color: #ecf5ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框样式优化 */
|
|
||||||
:deep(.el-dialog__header) {
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__footer) {
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f5f7fa;
|
background: #f8f9fa !important;
|
||||||
color: #606266;
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row:hover) {
|
:deep(.el-table td) {
|
||||||
background-color: #ecf5ff !important;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link) {
|
:deep(.el-table tr:hover > td) {
|
||||||
padding: 4px 8px;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link):hover {
|
:deep(.el-card__body) {
|
||||||
background-color: #f0f2f5;
|
padding: 0;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 分类选择容器样式 */
|
|
||||||
.category-select-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-input-container {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-input-container .el-input {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.image-info-cell {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-content {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-image-logo {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="announcements-container">
|
<div class="announcements-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>官方公告</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布公告
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="公告标题">
|
<el-form-item label="公告标题">
|
||||||
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable />
|
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="发布时间">
|
<el-form-item label="发布时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@@ -23,10 +19,11 @@
|
|||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
clearable
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||||
<el-option label="已发布" value="published" />
|
<el-option label="已发布" value="published" />
|
||||||
<el-option label="草稿" value="draft" />
|
<el-option label="草稿" value="draft" />
|
||||||
<el-option label="已下线" value="offline" />
|
<el-option label="已下线" value="offline" />
|
||||||
@@ -41,21 +38,27 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><plus /></el-icon>发布公告
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-card class="table-card">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
|
<el-table-column prop="publisher" label="发布人" width="120" />
|
||||||
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
|
<el-table-column prop="publishTime" label="发布时间" width="180" />
|
||||||
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
|
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
|
||||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -64,36 +67,27 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click="handleView(scope.row)">
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
<el-icon><view /></el-icon>查看
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
|
||||||
<el-icon><edit /></el-icon>编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="warning"
|
||||||
link
|
link
|
||||||
@click="handleChangeStatus(scope.row)"
|
@click="handleChangeStatus(scope.row)"
|
||||||
v-if="scope.row.status !== 'offline'"
|
v-if="scope.row.status !== 'offline'"
|
||||||
>
|
>下线</el-button>
|
||||||
<el-icon><turn-off /></el-icon>下线
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="danger"
|
||||||
link
|
link
|
||||||
@click="handleDelete(scope.row)"
|
@click="handleDelete(scope.row)"
|
||||||
v-if="scope.row.status === 'offline'"
|
v-if="scope.row.status === 'offline'"
|
||||||
>
|
>删除</el-button>
|
||||||
<el-icon><delete /></el-icon>删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -102,6 +96,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -365,32 +361,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.announcements-container {
|
.announcements-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
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;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,4 +442,35 @@ onMounted(() => {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="news-container">
|
<div class="news-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>新闻咨询</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布新闻
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="新闻标题">
|
<el-form-item label="新闻标题">
|
||||||
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable />
|
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="新闻分类">
|
<el-form-item label="新闻分类">
|
||||||
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable>
|
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable style="width: 150px">
|
||||||
<el-option label="产品动态" value="product" />
|
<el-option label="产品动态" value="product" />
|
||||||
<el-option label="技术干货" value="technology" />
|
<el-option label="技术干货" value="technology" />
|
||||||
<el-option label="行业资讯" value="industry" />
|
<el-option label="行业资讯" value="industry" />
|
||||||
@@ -32,6 +28,7 @@
|
|||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
clearable
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -43,16 +40,22 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><plus /></el-icon>发布新闻
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新闻列表卡片 -->
|
<!-- 数据表格 -->
|
||||||
<div v-loading="loading" class="news-list">
|
<div class="table-section">
|
||||||
<el-card class="table-card">
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
:data="newsData"
|
:data="newsData"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
|
||||||
@@ -68,21 +71,14 @@
|
|||||||
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
|
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
|
||||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click="handleView(scope.row)">
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
<el-icon><view /></el-icon>查看
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
</el-button>
|
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
|
||||||
<el-icon><edit /></el-icon>编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" link @click="handleDelete(scope.row)">
|
|
||||||
<el-icon><delete /></el-icon>删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -91,10 +87,11 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 新闻详情对话框 -->
|
<!-- 新闻详情对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -400,36 +397,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.news-container {
|
.news-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-list {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
.table-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
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;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,4 +506,35 @@ onMounted(() => {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: #E6A23C;
|
color: #E6A23C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="policies-container">
|
<div class="policies-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>官方政策</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布政策
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
|
||||||
<el-card class="search-card">
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="政策标题">
|
<el-form-item label="政策标题">
|
||||||
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable />
|
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="政策类型">
|
<el-form-item label="政策类型">
|
||||||
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable>
|
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable style="width: 150px">
|
||||||
<el-option label="服务条款" value="terms" />
|
<el-option label="服务条款" value="terms" />
|
||||||
<el-option label="定价政策" value="pricing" />
|
<el-option label="定价政策" value="pricing" />
|
||||||
<el-option label="隐私政策" value="privacy" />
|
<el-option label="隐私政策" value="privacy" />
|
||||||
@@ -32,6 +28,7 @@
|
|||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
clearable
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -43,16 +40,22 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><plus /></el-icon>发布政策
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-card class="table-card">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
|
||||||
@@ -75,34 +78,25 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click="handleView(scope.row)">
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
<el-icon><view /></el-icon>查看
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
|
||||||
<el-icon><edit /></el-icon>编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="warning"
|
||||||
link
|
link
|
||||||
@click="handleChangeStatus(scope.row)"
|
@click="handleChangeStatus(scope.row)"
|
||||||
v-if="scope.row.status === 'active'"
|
v-if="scope.row.status === 'active'"
|
||||||
>
|
>下线</el-button>
|
||||||
<el-icon><turn-off /></el-icon>下线
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="danger"
|
||||||
link
|
link
|
||||||
@click="handleDelete(scope.row)"
|
@click="handleDelete(scope.row)"
|
||||||
v-if="scope.row.status === 'inactive'"
|
v-if="scope.row.status === 'inactive'"
|
||||||
>
|
>删除</el-button>
|
||||||
<el-icon><delete /></el-icon>删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -111,6 +105,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -435,32 +431,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.policies-container {
|
.policies-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
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;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,4 +517,35 @@ onMounted(() => {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
+150
-728
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,847 @@
|
|||||||
|
<template>
|
||||||
|
<div class="server-form-container">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button @click="goBack" class="back-btn" circle>
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<div class="header-title-area">
|
||||||
|
<h1 class="page-title">{{ isEdit ? '编辑服务器' : '新建服务器' }}</h1>
|
||||||
|
<span class="page-subtitle">{{ isEdit ? '修改服务器配置信息' : '配置并部署新的服务器节点' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button @click="goBack" size="large">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
|
||||||
|
{{ isEdit ? '保存修改' : '立即创建' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主表单区域 -->
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
|
||||||
|
|
||||||
|
<!-- 左侧:主要配置 -->
|
||||||
|
<div class="form-main-col">
|
||||||
|
<el-card class="premium-card" shadow="never">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>基础信息</h3>
|
||||||
|
<p>配置服务器的基本标识与网络信息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="服务器名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="例如:生产环境-Web节点-01" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="IP地址" prop="server_ip">
|
||||||
|
<el-input v-model="form.server_ip" placeholder="例如:192.168.1.100" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="所在地区" prop="location">
|
||||||
|
<el-cascader
|
||||||
|
v-model="locationArray"
|
||||||
|
:options="regionsBuff"
|
||||||
|
:props="optionProps"
|
||||||
|
placeholder="选择服务器所在的物理位置"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon"><el-icon><Cpu /></el-icon></div>
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>硬件规格</h3>
|
||||||
|
<p>定义服务器的计算资源配额</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resource-cards">
|
||||||
|
<div class="resource-item">
|
||||||
|
<div class="resource-label">CPU核心</div>
|
||||||
|
<el-input v-model="form.cpu" placeholder="0">
|
||||||
|
<template #suffix>核</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="resource-item">
|
||||||
|
<div class="resource-label">内存容量</div>
|
||||||
|
<el-input v-model="form.memory" placeholder="0">
|
||||||
|
<template #suffix>MB</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="resource-item">
|
||||||
|
<div class="resource-label">硬盘空间</div>
|
||||||
|
<el-input v-model="form.disk" placeholder="0">
|
||||||
|
<template #suffix>GB</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="resource-item">
|
||||||
|
<div class="resource-label">网络带宽</div>
|
||||||
|
<el-input v-model="form.bandwidth" placeholder="0">
|
||||||
|
<template #suffix>Mbps</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="premium-card" shadow="never" style="margin-top: 24px;">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon"><el-icon><Connection /></el-icon></div>
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>连接与认证</h3>
|
||||||
|
<p>配置服务器的访问方式与凭证</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="服务器类型" prop="server_type" style="margin-bottom: 24px;">
|
||||||
|
<div class="type-selector">
|
||||||
|
<div
|
||||||
|
class="type-card"
|
||||||
|
:class="{ active: form.server_type === 'dockerContainer' }"
|
||||||
|
@click="form.server_type = 'dockerContainer'"
|
||||||
|
>
|
||||||
|
<div class="type-icon"><el-icon><Box /></el-icon></div>
|
||||||
|
<div class="type-info">
|
||||||
|
<div class="type-name">容器云服务器</div>
|
||||||
|
<div class="type-desc">基于Docker容器技术的轻量级实例</div>
|
||||||
|
</div>
|
||||||
|
<div class="type-check" v-if="form.server_type === 'dockerContainer'">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="type-card"
|
||||||
|
:class="{ active: form.server_type === 'hyperV' }"
|
||||||
|
@click="form.server_type = 'hyperV'"
|
||||||
|
>
|
||||||
|
<div class="type-icon"><el-icon><Platform /></el-icon></div>
|
||||||
|
<div class="type-info">
|
||||||
|
<div class="type-name">虚拟机云服务器</div>
|
||||||
|
<div class="type-desc">基于Hyper-V技术的完整虚拟化实例</div>
|
||||||
|
</div>
|
||||||
|
<div class="type-check" v-if="form.server_type === 'hyperV'">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 容器云特有 -->
|
||||||
|
<template v-if="form.server_type === 'dockerContainer'">
|
||||||
|
<el-form-item label="Auth-ID" prop="auth_id">
|
||||||
|
<el-input v-model="form.auth_id" placeholder="输入服务器管理ID" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 虚拟机特有 -->
|
||||||
|
<template v-if="form.server_type === 'hyperV'">
|
||||||
|
<el-form-item label="Guacamole网关" prop="guacamole_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.guacamole_id"
|
||||||
|
placeholder="选择Guacamole连接配置"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
:loading="guacamoleLoading"
|
||||||
|
@change="handleGuacamoleChange"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in guacamoleList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.url"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
<div class="guacamole-option">
|
||||||
|
<span class="url">{{ item.url }}</span>
|
||||||
|
<span class="user">{{ item.username }}</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="form-grid-2">
|
||||||
|
<el-form-item label="登录用户名" prop="username">
|
||||||
|
<el-input v-model="form.username" placeholder="例如:Administrator" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="登录密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
placeholder="输入登录密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="feature-switch">
|
||||||
|
<div class="switch-info">
|
||||||
|
<span class="switch-title">端口映射</span>
|
||||||
|
<span class="switch-desc">允许外部网络访问该服务器的特定端口</span>
|
||||||
|
</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="form.allow_port_forward"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item label="管理Token" prop="server_token">
|
||||||
|
<el-input
|
||||||
|
v-model="form.server_token"
|
||||||
|
placeholder="节点服务器管理员Token"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:高级设置 -->
|
||||||
|
<div class="form-side-col">
|
||||||
|
<el-card class="premium-card" shadow="never">
|
||||||
|
<div class="section-header small">
|
||||||
|
<div class="section-info">
|
||||||
|
<h3>高级设置</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="控制台连接">
|
||||||
|
<el-input v-model="form.console_url" placeholder="可选,https需反代">
|
||||||
|
<template #prefix><el-icon><Link /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="展示卡片HTML">
|
||||||
|
<el-input
|
||||||
|
v-model="form.html"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="自定义购买页面的展示样式代码"
|
||||||
|
resize="none"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="feature-switch">
|
||||||
|
<div class="switch-info">
|
||||||
|
<span class="switch-title">购物车显示</span>
|
||||||
|
<span class="switch-desc">在前端购买页面展示此节点</span>
|
||||||
|
</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="form.hide"
|
||||||
|
:active-value="0"
|
||||||
|
:inactive-value="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElNotification } from 'element-plus'
|
||||||
|
import {
|
||||||
|
ArrowLeft, Monitor, Cpu, Link, Connection,
|
||||||
|
Box, Platform, Check
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { addServer, editServer, getServer } from '@/utils/acs/server'
|
||||||
|
import { getGuacamoleList } from '@/utils/acs/guacamole'
|
||||||
|
import regions from '@/utils/regions.json'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const isEdit = computed(() => !!route.query.id)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
server_id: '',
|
||||||
|
name: '',
|
||||||
|
server_ip: '',
|
||||||
|
location: '',
|
||||||
|
bandwidth: '',
|
||||||
|
disk: '',
|
||||||
|
memory: '',
|
||||||
|
cpu: '',
|
||||||
|
state: '',
|
||||||
|
auth_id: '',
|
||||||
|
server_token: '',
|
||||||
|
server_type: 'dockerContainer',
|
||||||
|
html: '',
|
||||||
|
hide: 0,
|
||||||
|
console_url: '',
|
||||||
|
guacamole_id: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
allow_port_forward: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
|
||||||
|
server_ip: [
|
||||||
|
{ required: true, message: '请输入IP地址', trigger: 'blur' },
|
||||||
|
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
guacamole_id: [
|
||||||
|
{ required: false, message: '请输入Guacamole服务ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{ required: false, message: '请输入登录用户名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: false, message: '请输入登录密码', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guacamole 相关
|
||||||
|
const guacamoleList = ref([])
|
||||||
|
const guacamoleLoading = ref(false)
|
||||||
|
|
||||||
|
const fetchGuacamoleList = async () => {
|
||||||
|
if (guacamoleLoading.value) return
|
||||||
|
|
||||||
|
guacamoleLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getGuacamoleList()
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
guacamoleList.value = res.data.data || []
|
||||||
|
} else {
|
||||||
|
guacamoleList.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取Guacamole列表失败:', error)
|
||||||
|
guacamoleList.value = []
|
||||||
|
} finally {
|
||||||
|
guacamoleLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGuacamoleChange = (selectedId) => {
|
||||||
|
if (!selectedId) {
|
||||||
|
form.username = ''
|
||||||
|
form.password = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地区数据处理
|
||||||
|
const regionsBuff = ref(regions)
|
||||||
|
const optionProps = {
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
children: 'children',
|
||||||
|
checkStrictly: false,
|
||||||
|
emitPath: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const findValueByLabel = (label, options) => {
|
||||||
|
for (const option of options) {
|
||||||
|
if (option.label === label) return option.value
|
||||||
|
if (option.children) {
|
||||||
|
const result = findValueByLabel(label, option.children)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const findLabelByValue = (value, options) => {
|
||||||
|
for (const option of options) {
|
||||||
|
if (option.value === value) return option.label
|
||||||
|
if (option.children) {
|
||||||
|
const result = findLabelByValue(value, option.children)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationArray = computed({
|
||||||
|
get: () => {
|
||||||
|
if (form.location) {
|
||||||
|
try {
|
||||||
|
const labels = form.location.split(' ')
|
||||||
|
const values = labels.map(label => findValueByLabel(label, regionsBuff.value))
|
||||||
|
return values.filter(value => value !== undefined)
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
set: (newArray) => {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(newArray) && newArray.length > 0) {
|
||||||
|
const labels = newArray.map(value => {
|
||||||
|
const label = findLabelByValue(value, regionsBuff.value)
|
||||||
|
return label || value
|
||||||
|
})
|
||||||
|
form.location = labels.join(' ')
|
||||||
|
} else {
|
||||||
|
form.location = ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
form.location = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = async () => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
const id = route.query.id
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
const stateData = history.state.params
|
||||||
|
if (stateData) {
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
if (key in stateData) {
|
||||||
|
form[key] = stateData[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const res = await getServer(1, 100, '', route.query.type || 'dockerContainer')
|
||||||
|
if (res && res.data && res.data.data) {
|
||||||
|
const found = res.data.data.find(item => item.server_id == id)
|
||||||
|
if (found) {
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
if (key in found) {
|
||||||
|
form[key] = found[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.server_type = route.query.type || 'dockerContainer'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.server_type === 'hyperV') {
|
||||||
|
fetchGuacamoleList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const formData = { ...form }
|
||||||
|
|
||||||
|
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward']
|
||||||
|
numericFields.forEach(field => {
|
||||||
|
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
|
||||||
|
formData[field] = Number(formData[field])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (!isEdit.value) {
|
||||||
|
res = await addServer(formData)
|
||||||
|
} else {
|
||||||
|
res = await editServer(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
ElNotification({
|
||||||
|
title: !isEdit.value ? '添加成功' : '更新成功',
|
||||||
|
message: `服务器"${formData.name}"已${!isEdit.value ? '添加' : '更新'}成功`,
|
||||||
|
type: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
goBack()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.data?.msg || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交表单失败:', error)
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => form.server_type, (val) => {
|
||||||
|
if (val === 'hyperV') {
|
||||||
|
fetchGuacamoleList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.server-form-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航 */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 20px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
border: none;
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #606266;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: #e6e8eb;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单布局 */
|
||||||
|
.form-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-form {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-main-col {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-side-col {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.premium-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-card:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-card :deep(.el-card__body) {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 章节标题 */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.small {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
||||||
|
color: #409EFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info p {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单网格 */
|
||||||
|
.form-grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 资源卡片 */
|
||||||
|
.resource-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
background: #f2f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 类型选择器 */
|
||||||
|
.type-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card:hover {
|
||||||
|
border-color: #409EFF;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card.active {
|
||||||
|
border-color: #409EFF;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f2f6fc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card.active .type-icon {
|
||||||
|
background: #fff;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
color: #409EFF;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 开关样式 */
|
||||||
|
.feature-switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guacamole-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guacamole-option .url {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guacamole-option .user {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.main-form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-side-col {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.server-form-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .el-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-selector {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+129
-196
@@ -446,8 +446,8 @@
|
|||||||
<!-- <serverChart v-if="TypeData" :Type="TypeData" class="chart-section" /> -->
|
<!-- <serverChart v-if="TypeData" :Type="TypeData" class="chart-section" /> -->
|
||||||
|
|
||||||
<!-- 主要内容区域 -->
|
<!-- 主要内容区域 -->
|
||||||
<div class="content-wrapper">
|
<el-card class="main-container" shadow="never">
|
||||||
<el-tabs type="border-card" class="main-tabs">
|
<el-tabs class="main-tabs">
|
||||||
<!-- 实例规格列表 -->
|
<!-- 实例规格列表 -->
|
||||||
<el-tab-pane label="实例规格列表">
|
<el-tab-pane label="实例规格列表">
|
||||||
<div class="tab-header">
|
<div class="tab-header">
|
||||||
@@ -464,11 +464,11 @@
|
|||||||
<el-table
|
<el-table
|
||||||
v-loading="specLoading"
|
v-loading="specLoading"
|
||||||
:data="spec_list"
|
:data="spec_list"
|
||||||
border
|
|
||||||
stripe
|
stripe
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
table-layout="auto"
|
table-layout="auto"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="plan_id" label="规格ID" width="80" />
|
<el-table-column prop="plan_id" label="规格ID" width="80" />
|
||||||
<el-table-column prop="name" label="规格名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="name" label="规格名称" min-width="120" show-overflow-tooltip />
|
||||||
@@ -510,24 +510,24 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="160" fixed="right">
|
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<el-tooltip content="编辑" placement="top" :hide-after="1500">
|
<el-tooltip content="编辑" placement="top" :hide-after="1500">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
circle
|
link
|
||||||
:icon="Edit"
|
:icon="Edit"
|
||||||
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
|
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
|
||||||
/>
|
>编辑</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="删除" placement="top" :hide-after="1500">
|
<el-tooltip content="删除" placement="top" :hide-after="1500">
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
circle
|
link
|
||||||
:icon="Delete"
|
:icon="Delete"
|
||||||
@click="deleteSpec(scope.row.plan_id)"
|
@click="deleteSpec(scope.row.plan_id)"
|
||||||
/>
|
>删除</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -562,9 +562,9 @@
|
|||||||
<el-table
|
<el-table
|
||||||
:data="user_servers"
|
:data="user_servers"
|
||||||
stripe
|
stripe
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column label="ID" prop="container_id" width="80" />
|
<el-table-column label="ID" prop="container_id" width="80" />
|
||||||
<el-table-column label="价格" prop="pay" width="100">
|
<el-table-column label="价格" prop="pay" width="100">
|
||||||
@@ -598,7 +598,7 @@
|
|||||||
: scope.row.container_state == 4
|
: scope.row.container_state == 4
|
||||||
? 'danger'
|
? 'danger'
|
||||||
: 'info'"
|
: 'info'"
|
||||||
effect="light"
|
effect="plain"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
scope.row.container_state == 0
|
scope.row.container_state == 0
|
||||||
@@ -616,11 +616,11 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" fixed="right">
|
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
link
|
||||||
:icon="Setting"
|
:icon="Setting"
|
||||||
@click="$router.push('/servers/container?container_id=' + scope.row.container_id)"
|
@click="$router.push('/servers/container?container_id=' + scope.row.container_id)"
|
||||||
>
|
>
|
||||||
@@ -667,9 +667,9 @@
|
|||||||
<el-table
|
<el-table
|
||||||
:data="floatList"
|
:data="floatList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
border
|
|
||||||
stripe
|
stripe
|
||||||
class="data-table"
|
class="data-table"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column label="ID" prop="id" width="80" />
|
<el-table-column label="ID" prop="id" width="80" />
|
||||||
<el-table-column label="创建时间" min-width="160">
|
<el-table-column label="创建时间" min-width="160">
|
||||||
@@ -680,7 +680,7 @@
|
|||||||
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
|
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
|
||||||
<el-table-column label="状态" width="100" align="center">
|
<el-table-column label="状态" width="100" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.is_used ? 'success' : 'info'" effect="light">
|
<el-tag :type="scope.row.is_used ? 'success' : 'info'" effect="plain">
|
||||||
{{ scope.row.is_used ? "已绑定" : "未绑定" }}
|
{{ scope.row.is_used ? "已绑定" : "未绑定" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
@@ -690,10 +690,10 @@
|
|||||||
<el-tooltip content="删除IP" placement="top" :hide-after="1500">
|
<el-tooltip content="删除IP" placement="top" :hide-after="1500">
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
circle
|
link
|
||||||
:icon="Delete"
|
:icon="Delete"
|
||||||
@click="delFloating(scope.row.id)"
|
@click="delFloating(scope.row.id)"
|
||||||
/>
|
>删除</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
|
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑实例规格对话框 -->
|
<!-- 添加/编辑实例规格对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -2617,9 +2617,7 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.server-container {
|
.server-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面标题区域 */
|
/* 页面标题区域 */
|
||||||
@@ -2628,6 +2626,9 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header .left {
|
.page-header .left {
|
||||||
@@ -2638,7 +2639,7 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
|
|
||||||
.page-header .title {
|
.page-header .title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
@@ -2647,7 +2648,8 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 12px;
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
@@ -2669,13 +2671,13 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
/* 返回按钮样式 */
|
/* 返回按钮样式 */
|
||||||
.back-btn {
|
.back-btn {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #409EFF;
|
color: #606266;
|
||||||
padding: 8px 0;
|
padding: 0;
|
||||||
margin-right: 16px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn:hover {
|
.back-btn:hover {
|
||||||
color: #66b1ff;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 服务器信息卡片 */
|
/* 服务器信息卡片 */
|
||||||
@@ -2684,6 +2686,7 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 服务器详细信息卡片 */
|
/* 服务器详细信息卡片 */
|
||||||
@@ -2692,16 +2695,17 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card:hover {
|
.info-card:hover {
|
||||||
@@ -2710,10 +2714,10 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
background-color: #f5f7fa;
|
background-color: #fafbfc;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2722,7 +2726,7 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-title .el-icon {
|
.card-title .el-icon {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2745,32 +2749,31 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.info-value {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value.highlight {
|
.info-value.highlight {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 硬件信息样式 - 符合整体设计风格 */
|
/* 硬件信息样式 */
|
||||||
.device-count {
|
.device-count {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hardware-summary {
|
.hardware-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item {
|
.summary-item {
|
||||||
@@ -2780,21 +2783,21 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
.hardware-devices {
|
.hardware-devices {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-item {
|
.device-item {
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
background-color: #fafbfc;
|
background-color: #fafbfc;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header {
|
.device-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-title {
|
.device-title {
|
||||||
@@ -2807,57 +2810,27 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
|
|
||||||
.device-title .el-icon {
|
.device-title .el-icon {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-details {
|
.device-details {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-progress {
|
.usage-progress {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 使用率状态颜色类 */
|
|
||||||
.info-value.usage-normal {
|
|
||||||
color: #67C23A;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.usage-medium {
|
|
||||||
color: #409EFF;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.usage-warning {
|
|
||||||
color: #E6A23C;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.usage-critical {
|
|
||||||
color: #F56C6C;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 实际硬件划分样式 */
|
|
||||||
.highlight-item {
|
|
||||||
padding: 8px;
|
|
||||||
background-color: #f0f9ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 3px solid #409EFF;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 流量信息样式 */
|
/* 流量信息样式 */
|
||||||
.traffic-section {
|
.traffic-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-section:last-child {
|
.traffic-section:last-child {
|
||||||
@@ -2865,62 +2838,38 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-speed-grid,
|
.traffic-speed-grid,
|
||||||
.traffic-total-grid {
|
.traffic-total-grid,
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.traffic-total-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.traffic-distribution {
|
.traffic-distribution {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.raw-data {
|
.raw-data {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: #fafbfc;
|
background-color: #fafbfc;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e1e8ed;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.raw-data .info-value {
|
.raw-data .info-value {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-note {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 旧样式保持兼容 */
|
|
||||||
.usage-high {
|
|
||||||
color: #F56C6C;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-medium {
|
|
||||||
color: #E6A23C;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误状态样式 */
|
/* 错误状态样式 */
|
||||||
.error-status {
|
.error-status {
|
||||||
color: #F56C6C;
|
color: #F56C6C;
|
||||||
@@ -2929,15 +2878,10 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 主要内容区域 */
|
/* 主要内容区域 */
|
||||||
.chart-section {
|
.main-container {
|
||||||
margin-bottom: 24px;
|
margin: 0 20px 20px 20px;
|
||||||
}
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
.content-wrapper {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-header {
|
.tab-header {
|
||||||
@@ -2945,11 +2889,13 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-title {
|
.tab-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
@@ -2969,40 +2915,67 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式 */
|
/* 表格样式优化 */
|
||||||
.data-table {
|
:deep(.el-table) {
|
||||||
margin-bottom: 16px;
|
border: none;
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-actions {
|
:deep(.el-table__header) {
|
||||||
display: flex;
|
background: #f8f9fa;
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-value {
|
:deep(.el-table th) {
|
||||||
white-space: nowrap;
|
background: #f8f9fa !important;
|
||||||
}
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
.unit {
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-tag {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #F56C6C;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-info {
|
:deep(.el-table td) {
|
||||||
font-size: 13px;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table tr:hover > td) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs 样式优化 */
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
font-weight: 500;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item.is-active) {
|
||||||
|
color: #409EFF;
|
||||||
|
background: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 分页样式 */
|
/* 分页样式 */
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-top: 20px;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
@@ -3013,15 +2986,13 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px dashed #ebeef5;
|
border-bottom: 1px dashed #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -3124,27 +3095,24 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
margin-right: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-info {
|
.server-info {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-detail-info {
|
.server-detail-info {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card.location-info {
|
.info-card.location-info {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
margin: 0 16px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-header {
|
.tab-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -3159,40 +3127,5 @@ import { ElMessageBox } from 'element-plus';
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 硬件信息响应式 */
|
|
||||||
.hardware-summary {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-item {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 流量信息响应式 */
|
|
||||||
.traffic-speed-grid,
|
|
||||||
.traffic-total-grid,
|
|
||||||
.traffic-distribution {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raw-data {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group-buy-container">
|
||||||
|
<el-card class="header-card">
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" icon="Plus" @click="openCreateDialog">
|
||||||
|
创建随机队伍
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" icon="Download" @click="handleExport" :loading="exportLoading">
|
||||||
|
导出成功队伍
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" icon="Refresh" @click="fetchGroupList" :loading="loading">
|
||||||
|
刷新列表
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" @click="handleClearAll">
|
||||||
|
清除所有队伍
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" @click="showClearUserDialog = true">
|
||||||
|
清除用户队伍
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-table :data="groupList" v-loading="loading" stripe border>
|
||||||
|
<el-table-column prop="id" label="队伍ID" />
|
||||||
|
<el-table-column prop="name" label="队伍名称" min-width="150" />
|
||||||
|
<el-table-column prop="currentMembers" label="当前人数" width="100" align="center" />
|
||||||
|
<el-table-column prop="maxMembers" label="需要人数" width="100" align="center" />
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'pending'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleAddRandomUser(row)"
|
||||||
|
:loading="row.addingUser"
|
||||||
|
>
|
||||||
|
添加伪人
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'success'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="handleSetOrder(row)"
|
||||||
|
:loading="row.settingOrder"
|
||||||
|
>
|
||||||
|
下发订单
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
@click="handleViewMembers(row)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleRemoveGroup(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建随机队伍对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showCreateDialog"
|
||||||
|
title="创建随机伪人队伍"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="createForm" :rules="createRules" ref="createFormRef" label-width="100px">
|
||||||
|
<el-form-item label="队伍名称" prop="name">
|
||||||
|
<el-input v-model="createForm.name" placeholder="请输入队伍名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签" prop="tag">
|
||||||
|
<el-select v-model="createForm.tag" placeholder="请选择标签" style="width: 100%" @change="handleTagChange">
|
||||||
|
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="拼团类型" prop="groupBuyTypeId">
|
||||||
|
<el-select v-model="createForm.groupBuyTypeId" placeholder="请先选择标签" :disabled="!createForm.tag" style="width: 100%">
|
||||||
|
<el-option v-for="item in typeList" :key="item.id" :label="`${item.name} (${item.maxPerson}人)`" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleCreate" :loading="createLoading">
|
||||||
|
创建
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 查看成员对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showMembersDialog"
|
||||||
|
title="队伍成员列表"
|
||||||
|
width="700px"
|
||||||
|
>
|
||||||
|
<el-table :data="currentMembers" border stripe>
|
||||||
|
<el-table-column label="头像" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-avatar :size="50" :src="row.cover" v-if="row.cover">
|
||||||
|
<img src="https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png" />
|
||||||
|
</el-avatar>
|
||||||
|
<el-avatar :size="50" v-else>
|
||||||
|
{{ row.username?.charAt(0) || '?' }}
|
||||||
|
</el-avatar>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||||
|
<el-table-column label="队长" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.teamLeader" type="warning" size="small">队长</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" size="small" @click="handleClearUserGroups(row.userId)">清除队伍</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 清除用户队伍对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showClearUserDialog"
|
||||||
|
title="清除指定用户的所有队伍"
|
||||||
|
width="400px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="clearUserForm" label-width="80px">
|
||||||
|
<el-form-item label="用户ID">
|
||||||
|
<el-input v-model="clearUserForm.userId" placeholder="请输入用户ID" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showClearUserDialog = false">取消</el-button>
|
||||||
|
<el-button type="danger" @click="handleClearUserSubmit" :loading="clearUserLoading">确认清除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getGroupBuyList,
|
||||||
|
getGroupBuyDetail,
|
||||||
|
addRandomUser,
|
||||||
|
addRandomGroup,
|
||||||
|
exportIdcInfo,
|
||||||
|
setOrder
|
||||||
|
} from '@/api/admin/activity'
|
||||||
|
import { getGroupBuyTypeList, getGroupBuyTypeTags, removeGroupBuy, clearAllGroupBuy, clearUserGroupBuy } from '@/api/groupBuy'
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const exportLoading = ref(false)
|
||||||
|
const createLoading = ref(false)
|
||||||
|
const groupList = ref([])
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const showMembersDialog = ref(false)
|
||||||
|
const showClearUserDialog = ref(false)
|
||||||
|
const currentMembers = ref([])
|
||||||
|
const clearUserLoading = ref(false)
|
||||||
|
const clearUserForm = reactive({ userId: '' })
|
||||||
|
|
||||||
|
// 创建表单
|
||||||
|
const createFormRef = ref(null)
|
||||||
|
const createForm = reactive({
|
||||||
|
name: '',
|
||||||
|
tag: '',
|
||||||
|
groupBuyTypeId: ''
|
||||||
|
})
|
||||||
|
const typeList = ref([])
|
||||||
|
const tagList = ref([])
|
||||||
|
|
||||||
|
const createRules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入队伍名称', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
tag: [
|
||||||
|
{ required: true, message: '请选择标签', trigger: 'change' }
|
||||||
|
],
|
||||||
|
groupBuyTypeId: [
|
||||||
|
{ required: true, message: '请选择拼团类型', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标签列表
|
||||||
|
const fetchTags = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getGroupBuyTypeTags()
|
||||||
|
if (res.code === 200) {
|
||||||
|
tagList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 tag 获取拼团类型列表
|
||||||
|
const fetchTypeListByTag = async (tag) => {
|
||||||
|
try {
|
||||||
|
const res = await getGroupBuyTypeList({ page: 1, count: 100, tag })
|
||||||
|
if (res.code === 200) {
|
||||||
|
typeList.value = res.data?.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取拼团类型失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag 变化时获取对应的拼团类型
|
||||||
|
const handleTagChange = (tag) => {
|
||||||
|
createForm.groupBuyTypeId = ''
|
||||||
|
typeList.value = []
|
||||||
|
if (tag) {
|
||||||
|
fetchTypeListByTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开创建对话框
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
createForm.name = ''
|
||||||
|
createForm.tag = ''
|
||||||
|
createForm.groupBuyTypeId = ''
|
||||||
|
typeList.value = []
|
||||||
|
fetchTags()
|
||||||
|
showCreateDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取队伍列表
|
||||||
|
const fetchGroupList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getGroupBuyList()
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const allGroups = res.data.data.group_buy_list || []
|
||||||
|
const lackGroups = res.data.data.lack_group_buy_list || []
|
||||||
|
const successGroups = res.data.data.success_group_buy_list || []
|
||||||
|
|
||||||
|
// 获取成功和缺人队伍的ID列表用于状态判断
|
||||||
|
const successIds = successGroups.map(g => g.group_buy_id)
|
||||||
|
const lackIds = lackGroups.map(g => g.group_buy_id)
|
||||||
|
|
||||||
|
// 将队伍数据转换为显示数据
|
||||||
|
groupList.value = allGroups.map(group => {
|
||||||
|
let status = 'empty'
|
||||||
|
if (successIds.includes(group.group_buy_id)) {
|
||||||
|
status = 'success'
|
||||||
|
} else if (lackIds.includes(group.group_buy_id)) {
|
||||||
|
status = 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: group.group_buy_id,
|
||||||
|
name: group.name,
|
||||||
|
type: group.maxPerson === 5 ? 0 : 1,
|
||||||
|
currentMembers: group.users?.length || 0,
|
||||||
|
maxMembers: group.maxPerson,
|
||||||
|
status: status,
|
||||||
|
createTime: group.createTime || '-',
|
||||||
|
members: group.users || [],
|
||||||
|
addingUser: false,
|
||||||
|
settingOrder: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success(`加载成功,共 ${allGroups.length} 个队伍`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取队伍列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取队伍列表出错:', error)
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'empty': '空队伍',
|
||||||
|
'pending': '进行中',
|
||||||
|
'success': '已满员'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'empty': 'info',
|
||||||
|
'pending': 'warning',
|
||||||
|
'success': 'success'
|
||||||
|
}
|
||||||
|
return typeMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建随机队伍
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!createFormRef.value) return
|
||||||
|
|
||||||
|
await createFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
createLoading.value = true
|
||||||
|
console.log("队伍名称:",createForm.name)
|
||||||
|
try {
|
||||||
|
const res = await addRandomGroup({ name: createForm.name, group_buy_type_id: String(createForm.groupBuyTypeId) })
|
||||||
|
console.log('创建队伍响应:', res)
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(`创建成功!队伍ID: ${res.data.group_buy_id}`)
|
||||||
|
showCreateDialog.value = false
|
||||||
|
createForm.name = ''
|
||||||
|
createForm.groupBuyTypeId = ''
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建队伍出错:', error)
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
createLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加随机伪人
|
||||||
|
const handleAddRandomUser = async (row) => {
|
||||||
|
row.addingUser = true
|
||||||
|
try {
|
||||||
|
const res = await addRandomUser(row.id)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('添加伪人成功')
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '添加伪人失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加伪人出错:', error)
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
row.addingUser = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下发订单
|
||||||
|
const handleSetOrder = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要为该队伍下发订单吗?',
|
||||||
|
'确认操作',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
row.settingOrder = true
|
||||||
|
try {
|
||||||
|
const res = await setOrder(row.id)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('订单下发成功')
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '订单下发失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下发订单出错:', error)
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
row.settingOrder = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 用户取消操作
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewMembers = async (row) => {
|
||||||
|
try {
|
||||||
|
// 获取详细信息
|
||||||
|
const res = await getGroupBuyDetail(row.id)
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
const detail = res.data.data
|
||||||
|
// 使用详情接口返回的数据
|
||||||
|
currentMembers.value = (detail.users || []).map(member => ({
|
||||||
|
userId: member.user_id,
|
||||||
|
username: member.user_name || `用户${member.user_id}`,
|
||||||
|
cover: member.cover || '',
|
||||||
|
teamLeader: member.team_leader || false,
|
||||||
|
idcUid: member.idc_uid || '-',
|
||||||
|
idcPhone: member.idc_phone || '-'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 更新列表中的数据
|
||||||
|
row.name = detail.name
|
||||||
|
row.currentMembers = detail.users?.length || 0
|
||||||
|
row.maxMembers = detail.maxPerson
|
||||||
|
row.members = detail.users || []
|
||||||
|
} else {
|
||||||
|
// 如果获取失败,使用列表中的数据
|
||||||
|
currentMembers.value = row.members.map(member => ({
|
||||||
|
userId: member.user_id,
|
||||||
|
username: member.user_name || `用户${member.user_id}`,
|
||||||
|
cover: member.cover || '',
|
||||||
|
teamLeader: member.team_leader || false,
|
||||||
|
idcUid: member.idc_uid || '-',
|
||||||
|
idcPhone: member.idc_phone || '-'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
showMembersDialog.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取成员信息失败:', error)
|
||||||
|
// 使用列表中的数据
|
||||||
|
currentMembers.value = row.members.map(member => ({
|
||||||
|
userId: member.user_id,
|
||||||
|
username: member.user_name || `用户${member.user_id}`,
|
||||||
|
cover: member.cover || '',
|
||||||
|
teamLeader: member.team_leader || false,
|
||||||
|
idcUid: member.idc_uid || '-',
|
||||||
|
idcPhone: member.idc_phone || '-'
|
||||||
|
}))
|
||||||
|
showMembersDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出成功队伍信息
|
||||||
|
const handleExport = async () => {
|
||||||
|
exportLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await exportIdcInfo()
|
||||||
|
|
||||||
|
if (res.data && res.data.code === 200) {
|
||||||
|
// 将data对象转为JSON字符串并下载
|
||||||
|
const jsonStr = JSON.stringify(res.data.data, null, 2)
|
||||||
|
const blob = new Blob([jsonStr], { type: 'application/json' })
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `拼团成功队伍_${new Date().getTime()}.json`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('导出成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data?.message || '导出失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出出错:', error)
|
||||||
|
ElMessage.error('导出失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchGroupList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除指定队伍
|
||||||
|
const handleRemoveGroup = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该队伍吗?', '确认删除', { type: 'warning' })
|
||||||
|
const res = await removeGroupBuy(row.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch { /* 取消 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有队伍
|
||||||
|
const handleClearAll = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要清除所有队伍吗?此操作不可恢复!', '危险操作', { type: 'error', confirmButtonText: '确定清除' })
|
||||||
|
const res = await clearAllGroupBuy()
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('已清除所有队伍')
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '清除失败')
|
||||||
|
}
|
||||||
|
} catch { /* 取消 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除指定用户的所有队伍
|
||||||
|
const handleClearUserGroups = async (userId) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要清除用户 ${userId} 的所有队伍吗?`, '确认操作', { type: 'warning' })
|
||||||
|
const res = await clearUserGroupBuy(userId)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('清除成功')
|
||||||
|
showMembersDialog.value = false
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '清除失败')
|
||||||
|
}
|
||||||
|
} catch { /* 取消 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过弹窗清除用户队伍
|
||||||
|
const handleClearUserSubmit = async () => {
|
||||||
|
if (!clearUserForm.userId) {
|
||||||
|
ElMessage.warning('请输入用户ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearUserLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await clearUserGroupBuy(clearUserForm.userId)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('清除成功')
|
||||||
|
showClearUserDialog.value = false
|
||||||
|
clearUserForm.userId = ''
|
||||||
|
fetchGroupList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '清除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除用户队伍失败:', error)
|
||||||
|
ElMessage.error('网络错误')
|
||||||
|
} finally {
|
||||||
|
clearUserLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.group-buy-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button) {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button:hover) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group-buy-type-container">
|
||||||
|
<el-card class="header-card">
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" icon="Plus" @click="handleAdd">新增类型</el-button>
|
||||||
|
<el-select v-model="searchTag" placeholder="请选择标签" style="width: 180px; margin-left: 12px" @change="handleTagChange">
|
||||||
|
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="searchKey" placeholder="关键词搜索" style="width: 200px; margin-left: 12px" clearable :disabled="!searchTag" @keyup.enter="fetchList" />
|
||||||
|
<el-button type="info" icon="Refresh" @click="fetchList" :loading="loading" :disabled="!searchTag" style="margin-left: 12px">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-empty v-if="!searchTag" description="请先选择标签" />
|
||||||
|
<template v-else>
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe border>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="name" label="名称" min-width="120" />
|
||||||
|
<el-table-column label="价格" width="120">
|
||||||
|
<template #default="{ row }">¥{{ (row.price / 100).toFixed(2) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="续费价格" width="120">
|
||||||
|
<template #default="{ row }">¥{{ (row.renewPrice / 100).toFixed(2) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="maxPerson" label="拼团人数" width="100" align="center" />
|
||||||
|
<el-table-column prop="tag" label="标签" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.tag" type="info">{{ row.tag }}</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="过期时间" width="180">
|
||||||
|
<template #default="{ row }">{{ row.expireTime ? formatTime(row.expireTime) : '永久' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="fetchList" @current-change="fetchList" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑拼团类型' : '新增拼团类型'" width="500px" :close-on-click-modal="false">
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价格(分)" prop="price">
|
||||||
|
<el-input-number v-model="form.price" :min="0" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="续费价格(分)" prop="renewPrice">
|
||||||
|
<el-input-number v-model="form.renewPrice" :min="0" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="拼团人数" prop="maxPerson">
|
||||||
|
<el-input-number v-model="form.maxPerson" :min="2" :max="100" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签" prop="tag">
|
||||||
|
<el-select v-model="form.tag" placeholder="选择标签" filterable allow-create style="width: 100%">
|
||||||
|
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="过期时间" prop="expireTime">
|
||||||
|
<el-date-picker v-model="form.expireTime" type="datetime" placeholder="选择过期时间" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注字段">
|
||||||
|
<div class="note-fields-container">
|
||||||
|
<el-button type="primary" size="small" @click="addNoteField" style="margin-bottom: 10px">+ 添加字段</el-button>
|
||||||
|
<el-table :data="form.noteFields" border size="small" v-if="form.noteFields.length">
|
||||||
|
<el-table-column label="名称" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.label" placeholder="如:内存" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="默认值" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.defaultValue" placeholder="如:20GB" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button type="danger" size="small" link @click="removeNoteField($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getGroupBuyTypeList, getGroupBuyTypeTags, addGroupBuyType, updateGroupBuyType, deleteGroupBuyType } from '@/api/groupBuy'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const searchKey = ref('')
|
||||||
|
const searchTag = ref('')
|
||||||
|
const tagList = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const form = reactive({ id: '', name: '', price: 0, renewPrice: 0, maxPerson: 5, tag: '', expireTime: null, noteFields: [] })
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||||
|
renewPrice: [{ required: true, message: '请输入续费价格', trigger: 'blur' }],
|
||||||
|
maxPerson: [{ required: true, message: '请输入拼团人数', trigger: 'blur' }],
|
||||||
|
tag: [{ required: true, message: '请选择标签', trigger: 'change' }],
|
||||||
|
expireTime: [{ required: true, message: '请选择过期时间', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加备注字段
|
||||||
|
const addNoteField = () => {
|
||||||
|
form.noteFields.push({ label: '', defaultValue: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除备注字段
|
||||||
|
const removeNoteField = (index) => {
|
||||||
|
form.noteFields.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (timeStr) => {
|
||||||
|
if (!timeStr) return '-'
|
||||||
|
return new Date(timeStr).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getGroupBuyTypeList({ page: page.value, count: pageSize.value, key: searchKey.value || undefined, tag: searchTag.value || undefined })
|
||||||
|
console.log("获取拼团类型列表数据:",res)
|
||||||
|
if (res.code === 200) {
|
||||||
|
tableData.value = res.data?.data || []
|
||||||
|
total.value = res.data?.all_count || 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '获取列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
ElMessage.error('网络错误')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTags = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getGroupBuyTypeTags()
|
||||||
|
if (res.code === 200) tagList.value = res.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签变化时重新获取列表
|
||||||
|
const handleTagChange = (tag) => {
|
||||||
|
page.value = 1
|
||||||
|
searchKey.value = ''
|
||||||
|
tableData.value = []
|
||||||
|
total.value = 0
|
||||||
|
if (tag) {
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
Object.assign(form, { id: '', name: '', price: 0, renewPrice: 0, maxPerson: 5, tag: '', expireTime: null, noteFields: [] })
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
let noteFields = []
|
||||||
|
try {
|
||||||
|
noteFields = row.note ? JSON.parse(row.note) : []
|
||||||
|
} catch { noteFields = [] }
|
||||||
|
Object.assign(form, { id: row.id, name: row.name, price: row.price, renewPrice: row.renewPrice, maxPerson: row.maxPerson, tag: row.tag || '', expireTime: row.expireTime || null, noteFields })
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const noteJson = JSON.stringify(form.noteFields.filter(f => f.label))
|
||||||
|
const data = {
|
||||||
|
name: form.name,
|
||||||
|
price: String(form.price),
|
||||||
|
renew_price: String(form.renewPrice),
|
||||||
|
max_person: String(form.maxPerson),
|
||||||
|
tag: form.tag,
|
||||||
|
expire_time: form.expireTime ? Math.floor(new Date(form.expireTime).getTime() / 1000) : 0,
|
||||||
|
note: noteJson
|
||||||
|
}
|
||||||
|
if (isEdit.value) data.id = String(form.id)
|
||||||
|
const res = isEdit.value ? await updateGroupBuyType(data) : await addGroupBuyType(data)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
fetchTags()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error)
|
||||||
|
ElMessage.error('网络错误')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该拼团类型吗?', '确认删除', { type: 'warning' })
|
||||||
|
const res = await deleteGroupBuyType(row.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
fetchTags()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch { /* 取消 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { fetchTags() })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.group-buy-type-container { padding: 20px; }
|
||||||
|
.header-card { margin-bottom: 20px; }
|
||||||
|
.header-actions { display: flex; align-items: center; }
|
||||||
|
.table-card { box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); }
|
||||||
|
.pagination-wrapper { margin-top: 20px; display: flex; justify-content: flex-end; }
|
||||||
|
.note-fields-container { width: 100%; }
|
||||||
|
</style>
|
||||||
+126
-119
@@ -1,17 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="all-sites-container">
|
<div class="all-sites-container">
|
||||||
<!-- 页面头部 -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="left">
|
|
||||||
<h2 class="title">所有站点</h2>
|
|
||||||
<el-tag type="info" effect="plain" class="count-tag">共 {{ pagination.total }} 个站点</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
|
|
||||||
<!-- <el-button type="success" @click="handleExport" :icon="Download" class="action-btn">导出数据</el-button> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<div class="stats-panel">
|
<div class="stats-panel">
|
||||||
<div class="stat-card total-card">
|
<div class="stat-card total-card">
|
||||||
@@ -44,28 +32,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索和筛选 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="搜索容器">
|
<el-form-item label="搜索容器">
|
||||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<el-icon><Delete /></el-icon>重置
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleRefresh">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 站点列表 -->
|
<!-- 站点列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="siteList"
|
:data="siteList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
border
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
stripe
|
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||||
@@ -107,20 +106,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" fixed="right" align="center">
|
<el-table-column label="操作" fixed="right" align="center" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
|
||||||
|
|
||||||
<el-tooltip content="重新检查" placement="top">
|
<el-tooltip content="重新检查" placement="top">
|
||||||
<el-button type="warning" :icon="Refresh" circle size="small" @click="handleRecheck(row)" />
|
<el-button type="warning" link @click="handleRecheck(row)">
|
||||||
|
<el-icon><Refresh /></el-icon>检查
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
|
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
|
||||||
<el-button type="danger" :icon="Warning" circle size="small" @click="handleMarkViolation(row)" />
|
<el-button type="danger" link @click="handleMarkViolation(row)">
|
||||||
|
<el-icon><Warning /></el-icon>违规
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
|
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
|
||||||
<el-button type="success" :icon="CircleCheck" circle size="small" @click="handleMarkNormal(row)" />
|
<el-button type="success" link @click="handleMarkNormal(row)">
|
||||||
|
<el-icon><CircleCheck /></el-icon>正常
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -137,9 +139,8 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -650,42 +651,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.all-sites-container {
|
.all-sites-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面标题样式 */
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-tag {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片 */
|
/* 统计卡片 */
|
||||||
@@ -698,18 +664,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
@@ -753,88 +719,129 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 筛选容器 */
|
.main-container {
|
||||||
.filter-container {
|
border: 1px solid #e1e8ed;
|
||||||
margin-bottom: 20px;
|
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 {
|
.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-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格容器 */
|
.action-bar {
|
||||||
.table-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 8px;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页 */
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 15px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 站点详情 */
|
|
||||||
.site-detail {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-section {
|
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
.detail-section h4 {
|
background: #fafbfc;
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框底部 */
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 992px) {
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义样式 */
|
.filter-content {
|
||||||
.text-muted {
|
flex-direction: column;
|
||||||
color: #909399;
|
align-items: stretch;
|
||||||
font-style: italic;
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+138
-221
@@ -1,100 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="violation-sites-container">
|
<div class="violation-sites-container">
|
||||||
<!-- 页面头部 -->
|
<el-card class="main-container" shadow="never">
|
||||||
<div class="page-header">
|
|
||||||
<div class="left">
|
|
||||||
<h2 class="title">违规站点</h2>
|
|
||||||
<el-tag type="danger" effect="plain" class="count-tag">共 {{ pagination.total }} 个违规站点</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
|
|
||||||
<!-- <el-button type="warning" @click="handleBatchProcess" :disabled="!selectedRows.length" :icon="Warning" class="action-btn">
|
|
||||||
批量处理
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" @click="handleExport" :icon="Download" class="action-btn">导出违规报告</el-button> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 违规统计卡片 -->
|
|
||||||
<!-- <div class="stats-panel">
|
|
||||||
<div class="stat-card total-card">
|
|
||||||
<div class="stat-icon"><el-icon><Warning /></el-icon></div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-value">{{ violationStats.total }}</div>
|
|
||||||
<div class="stat-label">总违规站点</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card severe-card">
|
|
||||||
<div class="stat-icon"><el-icon><CircleClose /></el-icon></div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-value">{{ violationStats.severe }}</div>
|
|
||||||
<div class="stat-label">严重违规</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card moderate-card">
|
|
||||||
<div class="stat-icon"><el-icon><WarningFilled /></el-icon></div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-value">{{ violationStats.moderate }}</div>
|
|
||||||
<div class="stat-label">中度违规</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card pending-card">
|
|
||||||
<div class="stat-icon"><el-icon><Clock /></el-icon></div>
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-value">{{ violationStats.pending }}</div>
|
|
||||||
<div class="stat-label">待处理</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索和筛选 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="搜索容器">
|
<el-form-item label="搜索容器">
|
||||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- <el-form-item label="违规类型">
|
|
||||||
<el-select v-model="queryParams.violationType" placeholder="请选择违规类型" clearable>
|
|
||||||
<el-option label="全部" value="" />
|
|
||||||
<el-option label="内容违规" value="content" />
|
|
||||||
<el-option label="版权侵犯" value="copyright" />
|
|
||||||
<el-option label="恶意软件" value="malware" />
|
|
||||||
<el-option label="钓鱼网站" value="phishing" />
|
|
||||||
<el-option label="其他违规" value="other" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="违规等级">
|
|
||||||
<el-select v-model="queryParams.severity" placeholder="请选择违规等级" clearable>
|
|
||||||
<el-option label="全部" value="" />
|
|
||||||
<el-option label="轻微" value="light" />
|
|
||||||
<el-option label="中度" value="moderate" />
|
|
||||||
<el-option label="严重" value="severe" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处理状态">
|
|
||||||
<el-select v-model="queryParams.processStatus" placeholder="请选择处理状态" clearable>
|
|
||||||
<el-option label="全部" value="" />
|
|
||||||
<el-option label="待处理" value="pending" />
|
|
||||||
<el-option label="处理中" value="processing" />
|
|
||||||
<el-option label="已处理" value="processed" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<el-icon><Delete /></el-icon>重置
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleRefresh">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 违规站点列表 -->
|
<!-- 违规站点列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="violationList"
|
:data="violationList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
border
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
stripe
|
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||||
@@ -124,19 +62,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" fixed="right" align="center">
|
<el-table-column label="操作" fixed="right" align="center" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
|
||||||
<el-tooltip content="重新检查" placement="top">
|
<el-tooltip content="重新检查" placement="top">
|
||||||
<el-button type="warning" :icon="Refresh" circle size="small" @click="handleRecheck(row)" />
|
<el-button type="warning" link @click="handleRecheck(row)">
|
||||||
|
<el-icon><Refresh /></el-icon>检查
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
|
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
|
||||||
<el-button type="danger" :icon="Warning" circle size="small" @click="handleMarkViolation(row)" />
|
<el-button type="danger" link @click="handleMarkViolation(row)">
|
||||||
|
<el-icon><Warning /></el-icon>违规
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
|
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
|
||||||
<el-button type="success" :icon="CircleCheck" circle size="small" @click="handleMarkNormal(row)" />
|
<el-button type="success" link @click="handleMarkNormal(row)">
|
||||||
|
<el-icon><CircleCheck /></el-icon>正常
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -153,6 +95,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 违规详情对话框 -->
|
<!-- 违规详情对话框 -->
|
||||||
@@ -799,7 +742,13 @@ const handleBatchProcess = () => {
|
|||||||
|
|
||||||
// 导出数据
|
// 导出数据
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
ElMessage.success('违规报告导出功能开发中...')
|
ElMessage.success('导出功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (row) => {
|
||||||
|
currentSite.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新检查
|
// 重新检查
|
||||||
@@ -893,12 +842,6 @@ const handleMarkNormal = (row) => {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
const handleView = (row) => {
|
|
||||||
currentSite.value = row
|
|
||||||
detailDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理违规
|
// 处理违规
|
||||||
const handleProcess = (row) => {
|
const handleProcess = (row) => {
|
||||||
currentSite.value = row
|
currentSite.value = row
|
||||||
@@ -907,49 +850,14 @@ const handleProcess = (row) => {
|
|||||||
processDialogVisible.value = true
|
processDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封禁站点
|
|
||||||
const handleBlock = (row) => {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`确定要封禁站点 "${row.domain}" 吗?`,
|
|
||||||
'封禁确认',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定封禁',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
// 模拟API调用
|
|
||||||
row.isBlocked = true
|
|
||||||
ElMessage.success('站点已封禁')
|
|
||||||
getList()
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解封站点
|
|
||||||
const handleUnblock = (row) => {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`确定要解封站点 "${row.domain}" 吗?`,
|
|
||||||
'解封确认',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定解封',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'success'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
// 模拟API调用
|
|
||||||
row.isBlocked = false
|
|
||||||
ElMessage.success('站点已解封')
|
|
||||||
getList()
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交处理
|
// 提交处理
|
||||||
const submitProcess = () => {
|
const submitProcess = () => {
|
||||||
if (!processFormRef.value) return
|
if (!processFormRef.value) return
|
||||||
|
|
||||||
processFormRef.value.validate((valid) => {
|
processFormRef.value.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
ElMessage.success('违规处理提交成功')
|
// 模拟提交处理
|
||||||
|
ElMessage.success('处理成功')
|
||||||
processDialogVisible.value = false
|
processDialogVisible.value = false
|
||||||
getList()
|
getList()
|
||||||
}
|
}
|
||||||
@@ -964,42 +872,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.violation-sites-container {
|
.violation-sites-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面标题样式 */
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-tag {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片 */
|
/* 统计卡片 */
|
||||||
@@ -1012,18 +885,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
@@ -1039,8 +912,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-card .stat-icon {
|
.total-card .stat-icon {
|
||||||
background-color: rgba(230, 162, 60, 0.1);
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
color: #E6A23C;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.severe-card .stat-icon {
|
.severe-card .stat-icon {
|
||||||
@@ -1049,8 +922,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.moderate-card .stat-icon {
|
.moderate-card .stat-icon {
|
||||||
background-color: rgba(255, 193, 7, 0.1);
|
background-color: rgba(230, 162, 60, 0.1);
|
||||||
color: #FFC107;
|
color: #E6A23C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pending-card .stat-icon {
|
.pending-card .stat-icon {
|
||||||
@@ -1067,56 +940,67 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 筛选容器 */
|
.main-container {
|
||||||
.filter-container {
|
border: 1px solid #e1e8ed;
|
||||||
margin-bottom: 20px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 0;
|
margin: 0;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
/* 表格容器 */
|
|
||||||
.table-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-cell {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocked-tag {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-left: 8px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 8px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-count {
|
.table-section {
|
||||||
color: #606266;
|
padding: 0;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页 */
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 15px;
|
margin-top: 20px;
|
||||||
display: flex;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 违规详情 */
|
/* 详情样式 */
|
||||||
.violation-detail {
|
.violation-detail {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
@@ -1127,43 +1011,76 @@ onMounted(() => {
|
|||||||
|
|
||||||
.detail-section h4 {
|
.detail-section h4 {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: #303133;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
border-left: 4px solid #409EFF;
|
||||||
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话框底部 */
|
/* 表格样式优化 */
|
||||||
.dialog-footer {
|
:deep(.el-table) {
|
||||||
display: flex;
|
border: none;
|
||||||
justify-content: flex-end;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 992px) {
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义样式 */
|
.filter-content {
|
||||||
.text-muted {
|
flex-direction: column;
|
||||||
color: #909399;
|
align-items: stretch;
|
||||||
font-style: italic;
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="discount-code-container">
|
<div class="discount-code-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增优惠码
|
<el-icon><Plus /></el-icon>新增优惠码
|
||||||
@@ -13,15 +16,34 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 优惠码列表 -->
|
<!-- 优惠码列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-code"></div>
|
||||||
|
<div class="skeleton-cell skeleton-name"></div>
|
||||||
|
<div class="skeleton-cell skeleton-type"></div>
|
||||||
|
<div class="skeleton-cell skeleton-value"></div>
|
||||||
|
<div class="skeleton-cell skeleton-min"></div>
|
||||||
|
<div class="skeleton-cell skeleton-max"></div>
|
||||||
|
<div class="skeleton-cell skeleton-times"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="discountList"
|
:data="discountList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -67,9 +89,11 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -86,6 +110,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 优惠码表单对话框 -->
|
<!-- 优惠码表单对话框 -->
|
||||||
@@ -93,6 +118,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
|
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
|
||||||
width="700px"
|
width="700px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="discountFormRef"
|
ref="discountFormRef"
|
||||||
@@ -161,8 +187,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -489,18 +517,40 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
@@ -516,9 +566,91 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
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-checkbox { width: 55px; }
|
||||||
|
.skeleton-id { width: 80px; }
|
||||||
|
.skeleton-code { width: 150px; }
|
||||||
|
.skeleton-name { width: 180px; }
|
||||||
|
.skeleton-type { width: 120px; }
|
||||||
|
.skeleton-value { width: 120px; }
|
||||||
|
.skeleton-min { width: 120px; }
|
||||||
|
.skeleton-max { width: 120px; }
|
||||||
|
.skeleton-times { width: 120px; }
|
||||||
|
.skeleton-action { width: 200px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="discount-goods-container">
|
<div class="discount-goods-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<div class="filter-content">
|
||||||
<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
|
<el-select
|
||||||
v-model="queryParams.code_id"
|
v-model="queryParams.code_id"
|
||||||
placeholder="请选择代金券"
|
placeholder="请选择代金券"
|
||||||
@@ -37,19 +40,38 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 商品关联列表 -->
|
<!-- 商品关联列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-discount-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-related-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-name"></div>
|
||||||
|
<div class="skeleton-cell skeleton-type"></div>
|
||||||
|
<div class="skeleton-cell skeleton-note"></div>
|
||||||
|
<div class="skeleton-cell skeleton-price"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="goodsList"
|
:data="goodsList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="discountId" label="代金券ID" width="120" />
|
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
|
||||||
<el-table-column label="关联对象ID" width="120">
|
<el-table-column label="关联对象ID" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.goodId || row.goodGroupId || '-' }}
|
{{ row.goodId || row.goodGroupId || '-' }}
|
||||||
@@ -85,8 +107,10 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -103,6 +127,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑商品关联对话框 -->
|
<!-- 添加/编辑商品关联对话框 -->
|
||||||
@@ -110,6 +135,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'"
|
:title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'"
|
||||||
width="600px"
|
width="600px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@@ -123,7 +149,7 @@
|
|||||||
placeholder="请选择代金券"
|
placeholder="请选择代金券"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit' || !!codeId"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -198,15 +224,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
@@ -221,13 +249,27 @@ import {
|
|||||||
getProductGroupList
|
getProductGroupList
|
||||||
} from '@/api/admin/product'
|
} from '@/api/admin/product'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
codeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
code_id: '',
|
code_id: props.codeId || '',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.codeId, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
queryParams.code_id = newVal
|
||||||
|
fetchGoodsList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@@ -687,27 +729,52 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.action-buttons {
|
||||||
margin-top: 24px;
|
display: flex;
|
||||||
justify-content: flex-end;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
@@ -715,5 +782,91 @@ onMounted(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.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-checkbox { width: 55px; }
|
||||||
|
.skeleton-id { width: 80px; }
|
||||||
|
.skeleton-discount-id { width: 120px; }
|
||||||
|
.skeleton-related-id { width: 120px; }
|
||||||
|
.skeleton-name { width: 200px; }
|
||||||
|
.skeleton-type { width: 120px; }
|
||||||
|
.skeleton-note { width: 150px; }
|
||||||
|
.skeleton-price { width: 120px; }
|
||||||
|
.skeleton-time { width: 180px; }
|
||||||
|
.skeleton-action { width: 200px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="discount-users-container">
|
<div class="discount-users-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<div class="filter-content">
|
||||||
<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
|
<el-select
|
||||||
v-model="queryParams.code_id"
|
v-model="queryParams.code_id"
|
||||||
placeholder="请选择代金券"
|
placeholder="请选择代金券"
|
||||||
@@ -37,22 +40,48 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户关联列表 -->
|
<!-- 用户关联列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-discount-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-related-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-type"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="usersList"
|
:data="usersList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="discountId" label="代金券ID" width="120" />
|
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
|
||||||
<el-table-column label="关联对象ID" width="130">
|
<el-table-column label="用户名" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.userId || row.userGroupId || '-' }}
|
{{ row?.user?.user_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="手机号" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row?.user?.phone || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="邮箱" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row?.user?.email || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="类型" width="120">
|
<el-table-column label="类型" width="120">
|
||||||
@@ -62,15 +91,12 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ formatDate(row.CreatedAt) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -87,6 +113,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑用户关联对话框 -->
|
<!-- 添加/编辑用户关联对话框 -->
|
||||||
@@ -94,6 +121,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'"
|
:title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'"
|
||||||
width="600px"
|
width="600px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@@ -173,11 +201,11 @@
|
|||||||
<el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id">
|
<el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id">
|
||||||
<div class="user-selector-wrapper">
|
<div class="user-selector-wrapper">
|
||||||
<div class="selected-user-display" v-if="form.user_id">
|
<div class="selected-user-display" v-if="form.user_id">
|
||||||
<el-tage type="primary" closable @close="clearSelectedUser">
|
<el-tag type="primary" closable @close="clearSelectedUser">
|
||||||
{{ getSelectedUserName() }}
|
{{ getSelectedUserName() }}
|
||||||
</el-tage>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" plan @click="openUserSelector" style="width: 100%;">
|
<el-button type="primary" plain @click="openUserSelector" style="width: 100%;">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
{{ form.user_id ? '重新选择用户' : '选择用户' }}
|
{{ form.user_id ? '重新选择用户' : '选择用户' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -193,85 +221,24 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 用户选择弹窗 -->
|
<!-- 用户选择弹窗 -->
|
||||||
<el-dialog
|
<UserSelector
|
||||||
v-model="userSelectorVisible"
|
v-model:visible="userSelectorVisible"
|
||||||
title="选择用户"
|
@select="confirmUserSelection"
|
||||||
width="800px"
|
|
||||||
class="user-selector-dialog"
|
|
||||||
>
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="selector-search">
|
|
||||||
<el-input
|
|
||||||
v-model="userSearchParams.key"
|
|
||||||
placeholder="搜索用户名或ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="searchUsers"
|
|
||||||
style="width: 300px; margin-right: 12px"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button type="primary" @click="searchUsers">
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="resetUserSearch">重置</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户表格 -->
|
|
||||||
<el-table
|
|
||||||
v-loading="userSelectorLoading"
|
|
||||||
:data="userSelectorList"
|
|
||||||
highlight-current-row
|
|
||||||
@current-change="handleUserSelectChange"
|
|
||||||
style="width: 100%; margin-top: 16px"
|
|
||||||
:height="400"
|
|
||||||
>
|
|
||||||
<el-table-column type="index" label="序号" width="60" />
|
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
|
||||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
|
||||||
<el-table-column prop="Email" label="邮箱" min-width="180" />
|
|
||||||
<el-table-column label="状态" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
|
|
||||||
{{ row.Status === 1 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="userSearchParams.page"
|
|
||||||
v-model:page-size="userSearchParams.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="userSelectorTotal"
|
|
||||||
@size-change="handleUserSelectorSizeChange"
|
|
||||||
@current-change="handleUserSelectorPageChange"
|
|
||||||
background
|
|
||||||
class="selector-pagination"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="userSelectorVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
|
|
||||||
确定选择
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
@@ -285,14 +252,29 @@ import {
|
|||||||
getUserList,
|
getUserList,
|
||||||
getUserGroupList
|
getUserGroupList
|
||||||
} from '@/api/admin/user'
|
} from '@/api/admin/user'
|
||||||
|
import UserSelector from '@/components/UserSelector/index.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
codeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
code_id: '',
|
code_id: props.codeId || '',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.codeId, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
queryParams.code_id = newVal
|
||||||
|
fetchUsersList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@@ -336,15 +318,6 @@ const userGroupOptions = ref([]) // 用户组列表选项
|
|||||||
|
|
||||||
// 用户选择弹窗相关
|
// 用户选择弹窗相关
|
||||||
const userSelectorVisible = ref(false)
|
const userSelectorVisible = ref(false)
|
||||||
const userSelectorLoading = ref(false)
|
|
||||||
const userSelectorList = ref([])
|
|
||||||
const userSelectorTotal = ref(0)
|
|
||||||
const selectedUserTemp = ref(null) // 临时存储选中的用户
|
|
||||||
const userSearchParams = reactive({
|
|
||||||
key: '',
|
|
||||||
page: 1,
|
|
||||||
count: 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
@@ -360,25 +333,24 @@ const formatDate = (dateStr) => {
|
|||||||
|
|
||||||
// 获取用户类型名称(根据行数据)
|
// 获取用户类型名称(根据行数据)
|
||||||
const getUserTypeNameByRow = (row) => {
|
const getUserTypeNameByRow = (row) => {
|
||||||
// userId 不为 0 说明是用户
|
|
||||||
if (row.userId && row.userId !== 0) {
|
//通过看是否有user对象参数判断是否为用户还是用户组类型
|
||||||
|
if(row.user){
|
||||||
return '用户'
|
return '用户'
|
||||||
}
|
}else{
|
||||||
// userGroupId 不为 0 说明是用户组
|
|
||||||
if (row.userGroupId && row.userGroupId !== 0) {
|
|
||||||
return '用户组'
|
return '用户组'
|
||||||
}
|
}
|
||||||
|
|
||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户类型标签(根据行数据)
|
// 获取用户类型标签(根据行数据)
|
||||||
const getUserTypeTagByRow = (row) => {
|
const getUserTypeTagByRow = (row) => {
|
||||||
// 用户用蓝色
|
|
||||||
if (row.userId && row.userId !== 0) {
|
|
||||||
|
if(row.user){
|
||||||
return 'primary'
|
return 'primary'
|
||||||
}
|
}else{
|
||||||
// 用户组用橙色
|
|
||||||
if (row.userGroupId && row.userGroupId !== 0) {
|
|
||||||
return 'warning'
|
return 'warning'
|
||||||
}
|
}
|
||||||
return 'info'
|
return 'info'
|
||||||
@@ -441,70 +413,19 @@ const fetchUserGroupList = async () => {
|
|||||||
// 打开用户选择器
|
// 打开用户选择器
|
||||||
const openUserSelector = () => {
|
const openUserSelector = () => {
|
||||||
userSelectorVisible.value = true
|
userSelectorVisible.value = true
|
||||||
selectedUserTemp.value = null
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户选择器列表
|
|
||||||
const fetchUserSelectorList = async () => {
|
|
||||||
userSelectorLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getUserList(userSearchParams)
|
|
||||||
console.log('用户选择器列表:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
userSelectorList.value = res.data.data?.data || []
|
|
||||||
userSelectorTotal.value = res.data.data?.all_count || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户列表失败:', error)
|
|
||||||
ElMessage.error('获取用户列表失败')
|
|
||||||
} finally {
|
|
||||||
userSelectorLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索用户
|
|
||||||
const searchUsers = () => {
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置用户搜索
|
|
||||||
const resetUserSearch = () => {
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择变化
|
|
||||||
const handleUserSelectChange = (row) => {
|
|
||||||
selectedUserTemp.value = row
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择器分页
|
|
||||||
const handleUserSelectorSizeChange = (size) => {
|
|
||||||
userSearchParams.count = size
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUserSelectorPageChange = (page) => {
|
|
||||||
userSearchParams.page = page
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认用户选择
|
// 确认用户选择
|
||||||
const confirmUserSelection = () => {
|
const confirmUserSelection = (user) => {
|
||||||
if (!selectedUserTemp.value) {
|
if (!user) {
|
||||||
ElMessage.warning('请选择一个用户')
|
ElMessage.warning('请选择一个用户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.selected_user = selectedUserTemp.value.UserId
|
form.selected_user = user.UserId
|
||||||
form.user_id = selectedUserTemp.value.UserId
|
form.user_id = user.UserId
|
||||||
// 将选中的用户添加到 userOptions 中(如果不存在)
|
// 将选中的用户添加到 userOptions 中(如果不存在)
|
||||||
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
|
||||||
userOptions.value.push(selectedUserTemp.value)
|
userOptions.value.push(user)
|
||||||
}
|
}
|
||||||
userSelectorVisible.value = false
|
userSelectorVisible.value = false
|
||||||
ElMessage.success('用户选择成功')
|
ElMessage.success('用户选择成功')
|
||||||
@@ -663,6 +584,7 @@ const handleEdit = (row) => {
|
|||||||
})
|
})
|
||||||
//点击编辑需要初始化加载用户列表
|
//点击编辑需要初始化加载用户列表
|
||||||
fetchUserList()
|
fetchUserList()
|
||||||
|
fetchUserGroupList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除用户关联
|
// 删除用户关联
|
||||||
@@ -818,29 +740,100 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/* 用户选择器样式 */
|
/* 用户选择器样式 */
|
||||||
.user-selector-wrapper {
|
.user-selector-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -879,5 +872,42 @@ onMounted(() => {
|
|||||||
:deep(.current-row) {
|
:deep(.current-row) {
|
||||||
background-color: #ecf5ff !important;
|
background-color: #ecf5ff !important;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 骨架屏样式 */
|
||||||
|
.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-checkbox { width: 55px; }
|
||||||
|
.skeleton-id { width: 80px; }
|
||||||
|
.skeleton-discount-id { width: 120px; }
|
||||||
|
.skeleton-related-id { width: 130px; }
|
||||||
|
.skeleton-type { width: 120px; }
|
||||||
|
.skeleton-time { width: 180px; }
|
||||||
|
.skeleton-action { width: 200px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group-buy-manage">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>拼团管理</span>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">创建拼团</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 拼团列表 -->
|
||||||
|
<el-table :data="groupBuyList" style="width: 100%">
|
||||||
|
<el-table-column prop="group_buy_id" label="拼团ID" width="180" />
|
||||||
|
<el-table-column prop="name" label="拼团名称" width="150" />
|
||||||
|
<el-table-column prop="maxPerson" label="最大人数" width="100" />
|
||||||
|
<el-table-column label="当前人数" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.users?.length || 0 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="checkGroupBuy(row.group_buy_id)">
|
||||||
|
检查
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" @click="viewDetail(row)">
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" @click="deleteGroupBuy(row.group_buy_id)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建拼团对话框 -->
|
||||||
|
<el-dialog v-model="createDialogVisible" title="创建拼团" width="500px">
|
||||||
|
<el-form :model="createForm" label-width="100px">
|
||||||
|
<el-form-item label="拼团名称">
|
||||||
|
<el-input v-model="createForm.name" placeholder="请输入拼团名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最大人数">
|
||||||
|
<el-input-number v-model="createForm.maxPerson" :min="2" :max="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="封面图片">
|
||||||
|
<el-input v-model="createForm.cover" placeholder="请输入封面图片URL" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleCreate" :loading="creating">
|
||||||
|
创建
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 拼团详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="拼团详情" width="600px">
|
||||||
|
<el-descriptions :column="2" border v-if="currentGroupBuy">
|
||||||
|
<el-descriptions-item label="拼团ID">
|
||||||
|
{{ currentGroupBuy.group_buy_id }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="拼团名称">
|
||||||
|
{{ currentGroupBuy.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最大人数">
|
||||||
|
{{ currentGroupBuy.maxPerson }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前人数">
|
||||||
|
{{ currentGroupBuy.users?.length || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间" :span="2">
|
||||||
|
{{ currentGroupBuy.createTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
<h4>参与用户</h4>
|
||||||
|
<el-table :data="currentGroupBuy?.users || []" style="width: 100%">
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||||
|
<el-table-column prop="user_name" label="用户名" />
|
||||||
|
<el-table-column label="团长" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.team_leader" type="success">是</el-tag>
|
||||||
|
<el-tag v-else type="info">否</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
createGroupBuy as createGroupBuyApi,
|
||||||
|
checkGroupBuy as checkGroupBuyApi,
|
||||||
|
getGroupBuyList,
|
||||||
|
getGroupBuyDetail,
|
||||||
|
deleteGroupBuy as deleteGroupBuyApi
|
||||||
|
} from '@/api/groupBuy.js'
|
||||||
|
|
||||||
|
// 拼团列表
|
||||||
|
const groupBuyList = ref([])
|
||||||
|
|
||||||
|
// 创建对话框
|
||||||
|
const createDialogVisible = ref(false)
|
||||||
|
const creating = ref(false)
|
||||||
|
const createForm = ref({
|
||||||
|
name: '',
|
||||||
|
maxPerson: 5,
|
||||||
|
cover: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 详情对话框
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentGroupBuy = ref(null)
|
||||||
|
|
||||||
|
// 加载拼团列表
|
||||||
|
const loadGroupBuyList = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await getGroupBuyList({ page: 1, pageSize: 20 })
|
||||||
|
if (resp && resp.code === 200) {
|
||||||
|
groupBuyList.value = resp.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载拼团列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
createForm.value = {
|
||||||
|
name: '',
|
||||||
|
maxPerson: 5,
|
||||||
|
cover: ''
|
||||||
|
}
|
||||||
|
createDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建拼团
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!createForm.value.name) {
|
||||||
|
ElMessage.warning('请输入拼团名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
const resp = await createGroupBuyApi(createForm.value)
|
||||||
|
console.log('创建拼团响应:', resp)
|
||||||
|
|
||||||
|
if (resp && resp.code === 200) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
createDialogVisible.value = false
|
||||||
|
|
||||||
|
// 将新创建的拼团添加到列表
|
||||||
|
if (resp.data && resp.data.group_buy_id) {
|
||||||
|
groupBuyList.value.unshift(resp.data)
|
||||||
|
} else {
|
||||||
|
// 如果返回的不是完整数据,重新加载列表
|
||||||
|
await loadGroupBuyList()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(resp?.message || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建拼团失败:', error)
|
||||||
|
ElMessage.error('创建失败')
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查拼团
|
||||||
|
const checkGroupBuy = async (groupBuyId) => {
|
||||||
|
try {
|
||||||
|
const resp = await checkGroupBuyApi(groupBuyId)
|
||||||
|
console.log('检查拼团响应:', resp)
|
||||||
|
|
||||||
|
if (resp && resp.code === 200) {
|
||||||
|
ElMessage.success(`检查结果: ${resp.data}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(resp?.message || '检查失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查拼团失败:', error)
|
||||||
|
ElMessage.error('检查失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const viewDetail = async (row) => {
|
||||||
|
try {
|
||||||
|
const resp = await getGroupBuyDetail(row.group_buy_id)
|
||||||
|
if (resp && resp.code === 200) {
|
||||||
|
currentGroupBuy.value = resp.data
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
} else {
|
||||||
|
// 如果获取详情失败,使用列表中的数据
|
||||||
|
currentGroupBuy.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取详情失败:', error)
|
||||||
|
// 使用列表中的数据
|
||||||
|
currentGroupBuy.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除拼团
|
||||||
|
const deleteGroupBuy = async (groupBuyId) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这个拼团吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const resp = await deleteGroupBuyApi(groupBuyId)
|
||||||
|
if (resp && resp.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadGroupBuyList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(resp?.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除拼团失败:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadGroupBuyList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.group-buy-manage {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="user-voucher-container">
|
<div class="user-voucher-container">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<el-card class="filter-container" shadow="never">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||||
<el-form-item label="代金券">
|
<el-form-item label="代金券" v-if="!codeId">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.code_id"
|
v-model="queryParams.code_id"
|
||||||
placeholder="请选择代金券"
|
placeholder="请选择代金券"
|
||||||
@@ -54,37 +54,37 @@
|
|||||||
{{ row.Id || row.id }}
|
{{ row.Id || row.id }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="用户ID" width="100">
|
<el-table-column label="用户ID" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.UserId || row.userId }}
|
{{ row.UserId || row.userId }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="代金券ID" width="100">
|
<el-table-column label="代金券ID" width="100" v-if="!codeId">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.discountId }}
|
{{ row.discountId }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="代金券名称" min-width="150">
|
<el-table-column label="代金券名称" min-width="150" v-if="!codeId">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.discount?.name || '-' }}
|
{{ row.discount?.name || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="面额" width="120">
|
<el-table-column label="面额" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="已使用/最大次数" width="150">
|
<el-table-column label="已使用/最大次数" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
|
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" width="180">
|
<el-table-column label="过期时间" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDate(row.expireAt) }}
|
{{ formatDate(row.expireAt) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" width="180">
|
<el-table-column label="创建时间" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDate(row.CreatedAt) }}
|
{{ formatDate(row.CreatedAt) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<el-select
|
<el-select
|
||||||
v-model="addForm.voucher_id"
|
v-model="addForm.voucher_id"
|
||||||
placeholder="请选择代金券"
|
placeholder="请选择代金券"
|
||||||
:disabled="addForm.discount_type === 'code'"
|
:disabled="addForm.discount_type === 'code' || !!codeId"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -178,25 +178,22 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="用户" prop="user_id">
|
<el-form-item label="用户" prop="user_id">
|
||||||
<el-select
|
<div class="user-selector-wrapper">
|
||||||
v-model="addForm.user_id"
|
<div class="selected-user-display" v-if="addForm.user_id">
|
||||||
placeholder="请选择用户"
|
<el-tag type="primary" closable @close="clearSelectedUser">
|
||||||
:disabled="addForm.target_type === 'group'"
|
{{ getSelectedUserName() }}
|
||||||
filterable
|
</el-tag>
|
||||||
clearable
|
</div>
|
||||||
remote
|
<el-button
|
||||||
:remote-method="searchUsers"
|
type="primary"
|
||||||
:loading="userSearchLoading"
|
plain
|
||||||
|
@click="openUserSelector"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="handleUserChange"
|
|
||||||
>
|
>
|
||||||
<el-option
|
<el-icon><User /></el-icon>
|
||||||
v-for="item in userOptions"
|
{{ addForm.user_id ? '重新选择用户' : '选择用户' }}
|
||||||
:key="item.UserId"
|
</el-button>
|
||||||
:label="`${item.UserName} (ID: ${item.UserId})`"
|
</div>
|
||||||
:value="item.UserId"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="用户组" prop="group_id">
|
<el-form-item label="用户组" prop="group_id">
|
||||||
@@ -268,13 +265,19 @@
|
|||||||
<el-button type="primary" @click="submitEdit">确定</el-button>
|
<el-button type="primary" @click="submitEdit">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<!-- 用户选择弹窗 -->
|
||||||
|
<UserSelector
|
||||||
|
v-model:visible="userSelectorVisible"
|
||||||
|
@select="confirmUserSelection"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getUserVoucherList,
|
getUserVoucherList,
|
||||||
addUserVoucher,
|
addUserVoucher,
|
||||||
@@ -285,14 +288,29 @@ import {
|
|||||||
allocateVoucher
|
allocateVoucher
|
||||||
} from '@/api/admin/discount'
|
} from '@/api/admin/discount'
|
||||||
import { getUserList, getUserGroupList } from '@/api/admin/user'
|
import { getUserList, getUserGroupList } from '@/api/admin/user'
|
||||||
|
import UserSelector from '@/components/UserSelector/index.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
codeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
code_id: undefined,
|
code_id: props.codeId || undefined,
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.codeId, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
queryParams.code_id = newVal
|
||||||
|
fetchUserVoucherList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 添加表单
|
// 添加表单
|
||||||
const addForm = reactive({
|
const addForm = reactive({
|
||||||
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
|
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
|
||||||
@@ -321,6 +339,7 @@ const groupOptions = ref([]) // 用户组选项
|
|||||||
const userSearchLoading = ref(false) // 用户搜索加载状态
|
const userSearchLoading = ref(false) // 用户搜索加载状态
|
||||||
const submitLoading = ref(false) // 提交加载状态
|
const submitLoading = ref(false) // 提交加载状态
|
||||||
const dataList = ref([]) // 优惠列表
|
const dataList = ref([]) // 优惠列表
|
||||||
|
const userSelectorVisible = ref(false)
|
||||||
|
|
||||||
// 编辑表单
|
// 编辑表单
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
@@ -623,6 +642,36 @@ const handleGroupChange = (val) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开用户选择器
|
||||||
|
const openUserSelector = () => {
|
||||||
|
userSelectorVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认用户选择
|
||||||
|
const confirmUserSelection = (user) => {
|
||||||
|
if (!user) {
|
||||||
|
ElMessage.warning('请选择一个用户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addForm.user_id = user.UserId
|
||||||
|
// 将选中的用户添加到 userOptions 中(如果不存在)
|
||||||
|
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
|
||||||
|
userOptions.value.push(user)
|
||||||
|
}
|
||||||
|
userSelectorVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除选中的用户
|
||||||
|
const clearSelectedUser = () => {
|
||||||
|
addForm.user_id = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中用户的显示名称
|
||||||
|
const getSelectedUserName = () => {
|
||||||
|
const user = userOptions.value.find(u => u.UserId === addForm.user_id)
|
||||||
|
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${addForm.user_id}`
|
||||||
|
}
|
||||||
|
|
||||||
// 添加用户代金券
|
// 添加用户代金券
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
addDialogVisible.value = true
|
addDialogVisible.value = true
|
||||||
@@ -630,7 +679,7 @@ const handleAdd = async () => {
|
|||||||
// 重置表单
|
// 重置表单
|
||||||
Object.assign(addForm, {
|
Object.assign(addForm, {
|
||||||
discount_type: 'coupon',
|
discount_type: 'coupon',
|
||||||
voucher_id: undefined,
|
voucher_id: props.codeId || undefined,
|
||||||
code_id: undefined,
|
code_id: undefined,
|
||||||
target_type: 'user',
|
target_type: 'user',
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
@@ -837,6 +886,9 @@ onMounted(() => {
|
|||||||
// 加载代金券列表供选择
|
// 加载代金券列表供选择
|
||||||
fetchVoucherListOptions()
|
fetchVoucherListOptions()
|
||||||
fetchDiscountList()
|
fetchDiscountList()
|
||||||
|
if (queryParams.code_id) {
|
||||||
|
fetchUserVoucherList()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,10 @@
|
|||||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
|
||||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -166,8 +167,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
@@ -180,6 +183,8 @@ import {
|
|||||||
import { timeToTimestamp } from '@/utils/tool'
|
import { timeToTimestamp } from '@/utils/tool'
|
||||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
discount_type: 'coupon', // 固定为coupon表示代金券
|
discount_type: 'coupon', // 固定为coupon表示代金券
|
||||||
@@ -312,6 +317,11 @@ const handleEdit = (row) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 管理代金券
|
||||||
|
const handleManage = (row) => {
|
||||||
|
router.push(`/marketing/voucher/${row.id}/manage`)
|
||||||
|
}
|
||||||
|
|
||||||
// 查看代金券详情
|
// 查看代金券详情
|
||||||
const handleView = async (row) => {
|
const handleView = async (row) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,8 +46,8 @@
|
|||||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||||
<el-table-column prop="username" label="用户名" width="150" />
|
<el-table-column prop="username" label="用户名" width="150" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||||
<el-table-column prop="discount_id" label="代金券ID" width="120" />
|
<el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
|
||||||
<el-table-column prop="discount_name" label="代金券名称" min-width="180" />
|
<el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
|
||||||
<el-table-column label="优惠金额" width="120">
|
<el-table-column label="优惠金额" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
|
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||||
@@ -133,75 +133,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<!-- 用户选择弹窗 -->
|
<!-- 用户选择弹窗 -->
|
||||||
<el-dialog
|
<UserSelector
|
||||||
v-model="userSelectorVisible"
|
v-model:visible="userSelectorVisible"
|
||||||
title="选择用户"
|
@select="confirmUserSelection"
|
||||||
width="800px"
|
|
||||||
class="user-selector-dialog"
|
|
||||||
>
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="selector-search">
|
|
||||||
<el-input
|
|
||||||
v-model="userSearchParams.key"
|
|
||||||
placeholder="搜索用户名或ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="searchUsers"
|
|
||||||
style="width: 300px; margin-right: 12px"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button type="primary" @click="searchUsers">
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="resetUserSearch">重置</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户表格 -->
|
|
||||||
<el-table
|
|
||||||
v-loading="userSelectorLoading"
|
|
||||||
:data="userSelectorList"
|
|
||||||
highlight-current-row
|
|
||||||
@current-change="handleUserSelectChange"
|
|
||||||
style="width: 100%; margin-top: 16px"
|
|
||||||
:height="400"
|
|
||||||
>
|
|
||||||
<el-table-column type="index" label="序号" width="60" />
|
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
|
||||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
|
||||||
<el-table-column prop="Email" label="邮箱" min-width="180" />
|
|
||||||
<el-table-column label="状态" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
|
|
||||||
{{ row.Status === 1 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="userSearchParams.page"
|
|
||||||
v-model:page-size="userSearchParams.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="userSelectorTotal"
|
|
||||||
@size-change="handleUserSelectorSizeChange"
|
|
||||||
@current-change="handleUserSelectorPageChange"
|
|
||||||
background
|
|
||||||
class="selector-pagination"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="userSelectorVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
|
|
||||||
确定选择
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<el-row :gutter="20" style="margin-top: 20px">
|
<el-row :gutter="20" style="margin-top: 20px">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
@@ -237,20 +173,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Refresh, Download } from '@element-plus/icons-vue'
|
import { Search, Refresh, Download } from '@element-plus/icons-vue'
|
||||||
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
|
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
|
||||||
import { getUserList } from '@/api/admin/user'
|
import { getUserList } from '@/api/admin/user'
|
||||||
|
import UserSelector from '@/components/UserSelector/index.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
codeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
|
code_id: props.codeId || undefined,
|
||||||
id: '',
|
id: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.codeId, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
queryParams.code_id = newVal
|
||||||
|
fetchHistoryList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 状态数据
|
// 状态数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const historyList = ref([])
|
const historyList = ref([])
|
||||||
@@ -261,15 +213,6 @@ const currentDetail = ref({})
|
|||||||
const discountOptions = ref([])
|
const discountOptions = ref([])
|
||||||
const selectorType = ref('query')
|
const selectorType = ref('query')
|
||||||
const userSelectorVisible = ref(false)
|
const userSelectorVisible = ref(false)
|
||||||
const userSelectorList = ref([])
|
|
||||||
const userSelectorTotal = ref(0)
|
|
||||||
const userSearchParams = reactive({
|
|
||||||
key: '',
|
|
||||||
page: 1,
|
|
||||||
count: 10
|
|
||||||
})
|
|
||||||
const selectedUserTemp = ref(null)
|
|
||||||
const userSelectorLoading = ref(false)
|
|
||||||
const UserOptions = ref([])
|
const UserOptions = ref([])
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
@@ -371,86 +314,41 @@ const resetUserSearch = () => {
|
|||||||
// fetchUserSelectorList()
|
// fetchUserSelectorList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 打开查询用户选择器
|
||||||
|
const openQueryUserSelector = () => {
|
||||||
|
selectorType.value = 'query'
|
||||||
|
userSelectorVisible.value = true
|
||||||
|
}
|
||||||
|
// 打开编辑用户选择器
|
||||||
|
const openEditUserSelector = () => {
|
||||||
|
selectorType.value = 'edit'
|
||||||
|
userSelectorVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 确认用户选择
|
// 确认用户选择
|
||||||
const confirmUserSelection = () => {
|
const confirmUserSelection = (user) => {
|
||||||
if (!selectedUserTemp.value) {
|
if (!user) {
|
||||||
ElMessage.warning('请选择一个用户')
|
ElMessage.warning('请选择一个用户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectorType.value === 'query') {
|
if (selectorType.value === 'query') {
|
||||||
// 查询表单选择
|
// 查询表单选择
|
||||||
queryParams.user_id = selectedUserTemp.value.UserId
|
queryParams.user_id = user.UserId
|
||||||
} else {
|
} else {
|
||||||
// 编辑表单选择
|
// 编辑表单选择
|
||||||
editForm.user_id = selectedUserTemp.value.UserId
|
editForm.user_id = user.UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||||
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||||
UserOptions.value.push(selectedUserTemp.value)
|
UserOptions.value.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
userSelectorVisible.value = false
|
userSelectorVisible.value = false
|
||||||
ElMessage.success('用户选择成功')
|
ElMessage.success('用户选择成功')
|
||||||
}
|
}
|
||||||
// 打开查询用户选择器
|
|
||||||
const openQueryUserSelector = () => {
|
|
||||||
selectorType.value = 'query'
|
|
||||||
userSelectorVisible.value = true
|
|
||||||
selectedUserTemp.value = null
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
// 打开编辑用户选择器
|
|
||||||
const openEditUserSelector = () => {
|
|
||||||
selectorType.value = 'edit'
|
|
||||||
userSelectorVisible.value = true
|
|
||||||
selectedUserTemp.value = null
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择变化
|
|
||||||
const handleUserSelectChange = (row) => {
|
|
||||||
selectedUserTemp.value = row
|
|
||||||
}
|
|
||||||
// 搜索用户
|
|
||||||
const searchUsers = () => {
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择器分页
|
|
||||||
const handleUserSelectorSizeChange = (size) => {
|
|
||||||
userSearchParams.count = size
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUserSelectorPageChange = (page) => {
|
|
||||||
userSearchParams.page = page
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户选择器列表
|
|
||||||
const fetchUserSelectorList = async () => {
|
|
||||||
userSelectorLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getUserList(userSearchParams)
|
|
||||||
console.log('用户选择器列表:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
userSelectorList.value = res.data.data?.data || []
|
|
||||||
userSelectorTotal.value = res.data.data?.all_count || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户列表失败:', error)
|
|
||||||
ElMessage.error('获取用户列表失败')
|
|
||||||
} finally {
|
|
||||||
userSelectorLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询
|
// 查询
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
@@ -461,7 +359,7 @@ const handleQuery = () => {
|
|||||||
// 重置查询
|
// 重置查询
|
||||||
const resetQuery = () => {
|
const resetQuery = () => {
|
||||||
queryParams.user_id = undefined
|
queryParams.user_id = undefined
|
||||||
queryParams.discount_id = undefined
|
queryParams.code_id = undefined
|
||||||
queryParams.id = ''
|
queryParams.id = ''
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
fetchHistoryList()
|
fetchHistoryList()
|
||||||
|
|||||||
@@ -40,51 +40,51 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="Id" label="ID" width="80" />
|
<el-table-column prop="Id" label="ID" width="80" />
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
<el-table-column prop="UserId" label="用户ID" min-width="100" />
|
||||||
<el-table-column label="代金券ID" width="120">
|
<el-table-column label="代金券ID" min-width="110" v-if="!codeId">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.discountId || '-' }}
|
{{ row.discountId || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="代金券名称" min-width="180">
|
<el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.discount?.name || '-' }}
|
{{ row.discount?.name || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="代金券编码" width="150">
|
<el-table-column label="代金券编码" min-width="150" v-if="!codeId">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.discount?.code || '-' }}
|
{{ row.discount?.code || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="面额" width="120">
|
<el-table-column label="面额" min-width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="useTimes" label="已使用次数" width="120" />
|
<el-table-column prop="useTimes" label="已使用" min-width="100" />
|
||||||
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" />
|
<el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="getStatusType(row)">
|
<el-tag :type="getStatusType(row)" size="small">
|
||||||
{{ getStatusText(row) }}
|
{{ getStatusText(row) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" width="180">
|
<el-table-column label="过期时间" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDate(row.expireAt) }}
|
{{ formatDate(row.expireAt) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" width="180">
|
<el-table-column label="创建时间" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDate(row.CreatedAt) }}
|
{{ formatDate(row.CreatedAt) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="210" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleView(row)">查看详情</el-button>
|
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -203,81 +203,17 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 用户选择弹窗 -->
|
<!-- 用户选择弹窗 -->
|
||||||
<el-dialog
|
<UserSelector
|
||||||
v-model="userSelectorVisible"
|
v-model:visible="userSelectorVisible"
|
||||||
title="选择用户"
|
@select="confirmUserSelection"
|
||||||
width="800px"
|
|
||||||
class="user-selector-dialog"
|
|
||||||
>
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="selector-search">
|
|
||||||
<el-input
|
|
||||||
v-model="userSearchParams.key"
|
|
||||||
placeholder="搜索用户名或ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="searchUsers"
|
|
||||||
style="width: 300px; margin-right: 12px"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button type="primary" @click="searchUsers">
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="resetUserSearch">重置</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户表格 -->
|
|
||||||
<el-table
|
|
||||||
v-loading="userSelectorLoading"
|
|
||||||
:data="userSelectorList"
|
|
||||||
highlight-current-row
|
|
||||||
@current-change="handleUserSelectChange"
|
|
||||||
style="width: 100%; margin-top: 16px"
|
|
||||||
:height="400"
|
|
||||||
>
|
|
||||||
<el-table-column type="index" label="序号" width="60" />
|
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
|
||||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
|
||||||
<el-table-column prop="Email" label="邮箱" min-width="180" />
|
|
||||||
<el-table-column label="状态" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
|
|
||||||
{{ row.Status === 1 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="userSearchParams.page"
|
|
||||||
v-model:page-size="userSearchParams.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="userSelectorTotal"
|
|
||||||
@size-change="handleUserSelectorSizeChange"
|
|
||||||
@current-change="handleUserSelectorPageChange"
|
|
||||||
background
|
|
||||||
class="selector-pagination"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="userSelectorVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
|
|
||||||
确定选择
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue'
|
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getUserVoucherList,
|
getUserVoucherList,
|
||||||
allocateVoucher,
|
allocateVoucher,
|
||||||
@@ -285,14 +221,34 @@ import {
|
|||||||
deleteUserVoucher,
|
deleteUserVoucher,
|
||||||
getDiscountCodeList
|
getDiscountCodeList
|
||||||
} from '@/api/admin/discount'
|
} from '@/api/admin/discount'
|
||||||
import { getUserList } from '@/api/admin/user'
|
import UserSelector from '@/components/UserSelector/index.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
codeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
|
code_id: props.codeId || undefined,
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.codeId, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
queryParams.code_id = newVal
|
||||||
|
// 如果有 code_id,尝试刷新列表(取决于 API 是否支持仅按 code_id 查询)
|
||||||
|
// 如果 API 必须要求 user_id,则这里可能不需要立即刷新,或者提示用户选择用户
|
||||||
|
if (queryParams.user_id) {
|
||||||
|
fetchHoldersList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 状态数据
|
// 状态数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const holdersList = ref([])
|
const holdersList = ref([])
|
||||||
@@ -307,16 +263,7 @@ const discountOptions = ref([])
|
|||||||
|
|
||||||
// 用户选择弹窗相关
|
// 用户选择弹窗相关
|
||||||
const userSelectorVisible = ref(false)
|
const userSelectorVisible = ref(false)
|
||||||
const userSelectorLoading = ref(false)
|
|
||||||
const userSelectorList = ref([])
|
|
||||||
const userSelectorTotal = ref(0)
|
|
||||||
const selectedUserTemp = ref(null) // 临时存储选中的用户
|
|
||||||
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
|
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
|
||||||
const userSearchParams = reactive({
|
|
||||||
key: '',
|
|
||||||
page: 1,
|
|
||||||
count: 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 编辑表单
|
// 编辑表单
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
@@ -448,101 +395,36 @@ const handleExport = () => {
|
|||||||
ElMessage.info('导出功能开发中...')
|
ElMessage.info('导出功能开发中...')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户列表
|
|
||||||
const fetchUserList = async () => {
|
|
||||||
const res = await getUserList({
|
|
||||||
page: 1,
|
|
||||||
count: 10000,
|
|
||||||
key: ''
|
|
||||||
})
|
|
||||||
UserOptions.value = res.data.data?.data || []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开查询用户选择器
|
// 打开查询用户选择器
|
||||||
const openQueryUserSelector = () => {
|
const openQueryUserSelector = () => {
|
||||||
selectorType.value = 'query'
|
selectorType.value = 'query'
|
||||||
userSelectorVisible.value = true
|
userSelectorVisible.value = true
|
||||||
selectedUserTemp.value = null
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑用户选择器
|
// 打开编辑用户选择器
|
||||||
const openEditUserSelector = () => {
|
const openEditUserSelector = () => {
|
||||||
selectorType.value = 'edit'
|
selectorType.value = 'edit'
|
||||||
userSelectorVisible.value = true
|
userSelectorVisible.value = true
|
||||||
selectedUserTemp.value = null
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户选择器列表
|
|
||||||
const fetchUserSelectorList = async () => {
|
|
||||||
userSelectorLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getUserList(userSearchParams)
|
|
||||||
console.log('用户选择器列表:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
userSelectorList.value = res.data.data?.data || []
|
|
||||||
userSelectorTotal.value = res.data.data?.all_count || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户列表失败:', error)
|
|
||||||
ElMessage.error('获取用户列表失败')
|
|
||||||
} finally {
|
|
||||||
userSelectorLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索用户
|
|
||||||
const searchUsers = () => {
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置用户搜索
|
|
||||||
const resetUserSearch = () => {
|
|
||||||
userSearchParams.key = ''
|
|
||||||
userSearchParams.page = 1
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择变化
|
|
||||||
const handleUserSelectChange = (row) => {
|
|
||||||
selectedUserTemp.value = row
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择器分页
|
|
||||||
const handleUserSelectorSizeChange = (size) => {
|
|
||||||
userSearchParams.count = size
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUserSelectorPageChange = (page) => {
|
|
||||||
userSearchParams.page = page
|
|
||||||
fetchUserSelectorList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认用户选择
|
// 确认用户选择
|
||||||
const confirmUserSelection = () => {
|
const confirmUserSelection = (user) => {
|
||||||
if (!selectedUserTemp.value) {
|
if (!user) {
|
||||||
ElMessage.warning('请选择一个用户')
|
ElMessage.warning('请选择一个用户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectorType.value === 'query') {
|
if (selectorType.value === 'query') {
|
||||||
// 查询表单选择
|
// 查询表单选择
|
||||||
queryParams.user_id = selectedUserTemp.value.UserId
|
queryParams.user_id = user.UserId
|
||||||
} else {
|
} else {
|
||||||
// 编辑表单选择
|
// 编辑表单选择
|
||||||
editForm.user_id = selectedUserTemp.value.UserId
|
editForm.user_id = user.UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||||
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||||
UserOptions.value.push(selectedUserTemp.value)
|
UserOptions.value.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
userSelectorVisible.value = false
|
userSelectorVisible.value = false
|
||||||
@@ -692,7 +574,6 @@ const submitEditForm = () => {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUserList()
|
|
||||||
fetchDiscountList()
|
fetchDiscountList()
|
||||||
if (queryParams.user_id) {
|
if (queryParams.user_id) {
|
||||||
fetchHoldersList()
|
fetchHoldersList()
|
||||||
|
|||||||
@@ -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>
|
||||||
+178
-58
@@ -1,39 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="order-list-container">
|
<div class="order-list-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
<!-- <el-form :inline="true" :model="queryParams" class="search-form">
|
<div class="filter-content">
|
||||||
<el-form-item label="订单号">
|
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||||
<el-input v-model="queryParams.order_no" placeholder="请输入订单号" clearable style="width: 200px" />
|
<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>
|
||||||
<el-form-item label="用户ID">
|
<el-form-item label="用户ID">
|
||||||
<el-input v-model="queryParams.user_id" placeholder="请输入用户ID" clearable style="width: 150px" />
|
<el-input v-model="queryParams.user_id" placeholder="用户ID" clearable style="width: 120px" @keyup.enter="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="订单状态">
|
<el-form-item label="用户关键词">
|
||||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px">
|
<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="0" />
|
||||||
<el-option label="已支付" value="1" />
|
<el-option label="已支付" value="1" />
|
||||||
<el-option label="已完成" value="2" />
|
<el-option label="已失效" value="2" />
|
||||||
<el-option label="已取消" value="3" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="创建时间">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="queryParams.dateRange"
|
|
||||||
type="daterange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="开始日期"
|
|
||||||
end-placeholder="结束日期"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery">
|
<el-button type="primary" @click="handleQuery">
|
||||||
<el-icon><Search /></el-icon>查询
|
<el-icon><Search /></el-icon>搜索
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form> -->
|
</el-form>
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增订单
|
<el-icon><Plus /></el-icon>新增订单
|
||||||
@@ -41,22 +36,33 @@
|
|||||||
<el-button type="success" @click="fetchOrderList">
|
<el-button type="success" @click="fetchOrderList">
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- <el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
|
||||||
<el-icon><Delete /></el-icon>批量删除
|
|
||||||
</el-button> -->
|
|
||||||
<!-- <el-button type="success">
|
|
||||||
<el-icon><Download /></el-icon>导出订单
|
|
||||||
</el-button> -->
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 订单列表 -->
|
<!-- 订单列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-name"></div>
|
||||||
|
<div class="skeleton-cell skeleton-user"></div>
|
||||||
|
<div class="skeleton-cell skeleton-price"></div>
|
||||||
|
<div class="skeleton-cell skeleton-status"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="orderList"
|
:data="orderList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="订单ID" width="100" />
|
<el-table-column prop="id" label="订单ID" width="100" />
|
||||||
@@ -107,9 +113,10 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="warning" link @click="handleEdit(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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -126,6 +133,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 订单详情对话框 -->
|
<!-- 订单详情对话框 -->
|
||||||
@@ -133,6 +141,7 @@
|
|||||||
v-model="detailDialogVisible"
|
v-model="detailDialogVisible"
|
||||||
title="订单详情"
|
title="订单详情"
|
||||||
width="800px"
|
width="800px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-descriptions :column="2" border v-if="orderDetail">
|
<el-descriptions :column="2" border v-if="orderDetail">
|
||||||
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
||||||
@@ -162,6 +171,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
|
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
|
||||||
width="700px"
|
width="700px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="orderFormRef"
|
ref="orderFormRef"
|
||||||
@@ -217,8 +227,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,9 +244,12 @@ import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } f
|
|||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
|
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10,
|
||||||
|
key: '',
|
||||||
|
state: '',
|
||||||
|
user_id: '',
|
||||||
|
user_key: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 订单表单
|
// 订单表单
|
||||||
@@ -294,7 +309,14 @@ const orderFormRef = ref(null)
|
|||||||
const fetchOrderList = async () => {
|
const fetchOrderList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getOrderList(queryParams)
|
// 过滤空值参数
|
||||||
|
const params = {}
|
||||||
|
Object.keys(queryParams).forEach(key => {
|
||||||
|
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
|
||||||
|
params[key] = queryParams[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = await getOrderList(params)
|
||||||
console.log('订单列表数据:', res.data)
|
console.log('订单列表数据:', res.data)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
orderList.value = res.data.data.list || []
|
orderList.value = res.data.data.list || []
|
||||||
@@ -349,10 +371,10 @@ const handleQuery = () => {
|
|||||||
|
|
||||||
// 重置查询
|
// 重置查询
|
||||||
const resetQuery = () => {
|
const resetQuery = () => {
|
||||||
queryParams.order_no = ''
|
queryParams.key = ''
|
||||||
|
queryParams.state = ''
|
||||||
queryParams.user_id = ''
|
queryParams.user_id = ''
|
||||||
queryParams.status = ''
|
queryParams.user_key = ''
|
||||||
queryParams.dateRange = []
|
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
fetchOrderList()
|
fetchOrderList()
|
||||||
}
|
}
|
||||||
@@ -531,37 +553,56 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.filter-section {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.action-buttons {
|
||||||
padding: 4px 0;
|
display: flex;
|
||||||
}
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
.username {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-id {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
@@ -577,8 +618,87 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.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-checkbox { width: 55px; }
|
||||||
|
.skeleton-id { width: 100px; }
|
||||||
|
.skeleton-name { width: 180px; }
|
||||||
|
.skeleton-user { width: 100px; }
|
||||||
|
.skeleton-price { width: 120px; }
|
||||||
|
.skeleton-status { width: 100px; }
|
||||||
|
.skeleton-time { width: 170px; }
|
||||||
|
.skeleton-action { width: 200px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-group-container">
|
<div class="product-group-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 操作栏 -->
|
<!-- 操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增商品分组
|
<el-icon><Plus /></el-icon>新增商品分组
|
||||||
@@ -10,18 +13,32 @@
|
|||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 商品分组列表 -->
|
<!-- 商品分组列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-note"></div>
|
||||||
|
<div class="skeleton-cell skeleton-status"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="groupList"
|
:data="groupList"
|
||||||
style="width: 100%"
|
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="id" label="分组ID" width="100" />
|
||||||
<el-table-column prop="name" label="分组名称" min-width="200" />
|
<el-table-column prop="name" label="分组名称" min-width="200" />
|
||||||
<el-table-column prop="note" label="备注" min-width="250" />
|
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -34,8 +51,10 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -52,6 +71,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 商品分组表单对话框 -->
|
<!-- 商品分组表单对话框 -->
|
||||||
@@ -59,6 +79,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增商品分组' : '编辑商品分组'"
|
:title="dialogType === 'add' ? '新增商品分组' : '编辑商品分组'"
|
||||||
width="600px"
|
width="600px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="groupFormRef"
|
ref="groupFormRef"
|
||||||
@@ -80,8 +101,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,23 +276,121 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.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-note { flex: 1; min-width: 250px; }
|
||||||
|
.skeleton-status { width: 100px; }
|
||||||
|
.skeleton-action { width: 180px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-list-container">
|
<div class="product-list-container">
|
||||||
|
<!-- 搜索和商品列表 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="商品分组">
|
<el-form-item label="商品分组">
|
||||||
<el-select v-model="queryParams.good_group_id" placeholder="请选择分组" clearable style="width: 200px">
|
<el-select v-model="queryParams.good_group_id" placeholder="请选择分组" clearable style="width: 200px">
|
||||||
@@ -18,28 +21,46 @@
|
|||||||
<el-icon><Search /></el-icon>查询
|
<el-icon><Search /></el-icon>查询
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
|
<el-button type="success" @click="fetchProductList">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增商品
|
<el-icon><Plus /></el-icon>新增商品
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" @click="fetchProductList">
|
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 商品列表 -->
|
<!-- 商品列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-image"></div>
|
||||||
|
<div class="skeleton-cell skeleton-name"></div>
|
||||||
|
<div class="skeleton-cell skeleton-table"></div>
|
||||||
|
<div class="skeleton-cell skeleton-price"></div>
|
||||||
|
<div class="skeleton-cell skeleton-tag"></div>
|
||||||
|
<div class="skeleton-cell skeleton-inventory"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="productList"
|
:data="productList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="商品ID" width="100" />
|
<el-table-column prop="id" label="商品ID" width="100" />
|
||||||
@@ -48,7 +69,7 @@
|
|||||||
<el-image
|
<el-image
|
||||||
:src="row.image || '/logo.svg'"
|
:src="row.image || '/logo.svg'"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
style="width: 60px; height: 60px; border-radius: 4px"
|
style="width: 60px; height: 60px"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -61,8 +82,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="库存控制" width="100">
|
<el-table-column label="库存控制" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.inventory_control ? 'success' : 'info'">
|
<el-tag :type="row.inventoryControl ? 'success' : 'info'">
|
||||||
{{ row.inventory_control ? '已启用' : '未启用' }}
|
{{ row.inventoryControl ? '已启用' : '未启用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -78,7 +99,7 @@
|
|||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<!-- <el-button type="warning" link @click="handleSpec(row)">规格</el-button> -->
|
<el-button type="warning" link @click="handleParameter(row)">参数</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -96,6 +117,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 商品表单对话框 -->
|
<!-- 商品表单对话框 -->
|
||||||
@@ -103,6 +125,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增商品' : '编辑商品'"
|
:title="dialogType === 'add' ? '新增商品' : '编辑商品'"
|
||||||
width="700px"
|
width="700px"
|
||||||
|
style="margin-top: 300px;"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="productFormRef"
|
ref="productFormRef"
|
||||||
@@ -126,6 +149,16 @@
|
|||||||
<el-form-item label="商品所属表" prop="table">
|
<el-form-item label="商品所属表" prop="table">
|
||||||
<el-input v-model="productForm.table" placeholder="请输入商品所属表" />
|
<el-input v-model="productForm.table" placeholder="请输入商品所属表" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="商品标签" prop="tag">
|
||||||
|
<el-select v-model="productForm.tag" placeholder="请选择商品标签" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in tagOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="内容" prop="content">
|
<el-form-item label="内容" prop="content">
|
||||||
<el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
|
<el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -159,6 +192,152 @@
|
|||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 商品参数列表对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="paramDialogVisible"
|
||||||
|
title="商品参数管理"
|
||||||
|
width="900px"
|
||||||
|
>
|
||||||
|
<div class="filter-section" style="border: none; padding: 0 0 16px 0;">
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAddParameter">
|
||||||
|
<el-icon><Plus /></el-icon>新增参数
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" @click="fetchParameterList">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="paramLoading"
|
||||||
|
:data="parameterList"
|
||||||
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="参数ID" width="100" />
|
||||||
|
<el-table-column prop="name" label="参数名称" min-width="150" />
|
||||||
|
<el-table-column prop="type" label="参数类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getArgTypeTag(row.type)">
|
||||||
|
{{ getArgTypeText(row.type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="250" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" link @click="handleEditParameter(row)">编辑</el-button>
|
||||||
|
<el-button type="success" link @click="handleViewParamValues(row)">查看参数值</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDeleteParameter(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 商品参数表单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="paramFormDialogVisible"
|
||||||
|
:title="paramFormType === 'add' ? '新增商品参数' : '编辑商品参数'"
|
||||||
|
width="600px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="paramFormRef"
|
||||||
|
:model="paramForm"
|
||||||
|
:rules="paramRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="参数名称" prop="arg_name">
|
||||||
|
<el-input v-model="paramForm.arg_name" placeholder="请输入参数名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="参数类型" prop="arg_type">
|
||||||
|
<el-radio-group v-model="paramForm.arg_type">
|
||||||
|
<el-radio label="string">字符串</el-radio>
|
||||||
|
<el-radio label="number">数字</el-radio>
|
||||||
|
<el-radio label="select">选择</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="paramFormDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitParamForm">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 参数值管理对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="paramValuesDialogVisible"
|
||||||
|
title="参数值管理"
|
||||||
|
width="800px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="values-header">
|
||||||
|
<span>参数:{{ currentParam?.name }}</span>
|
||||||
|
<el-button type="primary" @click="handleAddParamValue">
|
||||||
|
<el-icon><Plus /></el-icon>添加参数值
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="paramValuesLoading"
|
||||||
|
:data="paramValueList"
|
||||||
|
style="width: 100%; margin-top: 20px"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="值ID" width="100" />
|
||||||
|
<el-table-column prop="name" label="值名称" min-width="150" />
|
||||||
|
<el-table-column prop="value" label="值" min-width="150" />
|
||||||
|
<el-table-column label="价格" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ (row.price / 100).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" link @click="handleEditParamValue(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDeleteParamValue(row)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 参数值表单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="paramValueFormDialogVisible"
|
||||||
|
:title="paramValueFormType === 'add' ? '添加参数值' : '编辑参数值'"
|
||||||
|
width="500px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="paramValueFormRef"
|
||||||
|
:model="paramValueForm"
|
||||||
|
:rules="paramValueRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="值名称" prop="attr_name">
|
||||||
|
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="值" prop="attr_value">
|
||||||
|
<el-input v-model="paramValueForm.attr_value" placeholder="请输入值" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价格(元)" prop="attr_price">
|
||||||
|
<el-input-number v-model="paramValueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="paramValueFormDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitParamValueForm">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -167,8 +346,17 @@ import { ref, reactive, onMounted } from 'vue'
|
|||||||
import { getFileDetail } from '@/api/admin/file'
|
import { getFileDetail } from '@/api/admin/file'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
|
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
|
||||||
import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product'
|
import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
|
||||||
import { getProductGroupList } from '@/api/admin/product'
|
getProductTagList,
|
||||||
|
getProductParameterList,
|
||||||
|
getProductParameterDetail,
|
||||||
|
createProductParameter,
|
||||||
|
updateProductParameter,
|
||||||
|
deleteProductParameter,
|
||||||
|
addProductParameterValue,
|
||||||
|
updateProductParameterValue,
|
||||||
|
deleteProductParameterValue
|
||||||
|
} from '@/api/admin/product'
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
@@ -182,6 +370,7 @@ const productForm = reactive({
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
table: '',
|
table: '',
|
||||||
|
tag: '',
|
||||||
content: '',
|
content: '',
|
||||||
cover_id: undefined,
|
cover_id: undefined,
|
||||||
good_group_id: undefined, // 添加商品分组字段
|
good_group_id: undefined, // 添加商品分组字段
|
||||||
@@ -217,6 +406,7 @@ const productRules = {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const productList = ref([])
|
const productList = ref([])
|
||||||
const groupOptions = ref([])
|
const groupOptions = ref([])
|
||||||
|
const tagOptions = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const selectedRows = ref([])
|
const selectedRows = ref([])
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@@ -229,9 +419,12 @@ const fetchProductList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getProductList(queryParams)
|
const res = await getProductList(queryParams)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
productList.value = res.data.data.data || []
|
const allData = res.data.data.data || []
|
||||||
productList.value = productList.value.filter(item => item.delete == false)
|
// 过滤掉已删除的数据
|
||||||
total.value = res.data.data.total || 0
|
productList.value = allData.filter(item => item.delete == false)
|
||||||
|
// 计算未删除数据的总数(API返回的all_count包含已删除的,需要减去已删除的数量)
|
||||||
|
const deletedCount = allData.filter(item => item.delete == true).length
|
||||||
|
total.value = (res.data.data.data.length || 0) - deletedCount
|
||||||
productList.value = productList.value.map(item => {
|
productList.value = productList.value.map(item => {
|
||||||
item.image = item.coverId ? getFileDetail({ file_id: item.coverId }).then(res => res.data.data.url) : ''
|
item.image = item.coverId ? getFileDetail({ file_id: item.coverId }).then(res => res.data.data.url) : ''
|
||||||
return item
|
return item
|
||||||
@@ -263,6 +456,20 @@ const fetchGroupList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取商品标签列表
|
||||||
|
const fetchTagList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getProductTagList()
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
tagOptions.value = res.data.data || []
|
||||||
|
console.log('商品标签列表:', tagOptions.value) // 调试日志
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签列表失败:', error)
|
||||||
|
ElMessage.error('获取标签列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 查询
|
// 查询
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
@@ -306,6 +513,7 @@ const handleAdd = () => {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
table: '',
|
table: '',
|
||||||
|
tag: '',
|
||||||
content: '',
|
content: '',
|
||||||
cover_id: undefined,
|
cover_id: undefined,
|
||||||
good_group_id: undefined,
|
good_group_id: undefined,
|
||||||
@@ -322,16 +530,18 @@ const handleAdd = () => {
|
|||||||
|
|
||||||
// 编辑商品
|
// 编辑商品
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
|
console.log("更新:",row)
|
||||||
dialogType.value = 'edit'
|
dialogType.value = 'edit'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
Object.assign(productForm, {
|
Object.assign(productForm, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
table: row.table,
|
table: row.table,
|
||||||
|
tag: row.tag,
|
||||||
content: row.content,
|
content: row.content,
|
||||||
cover_id: row.coverId,
|
cover_id: row.coverId,
|
||||||
good_group_id: row.goodGroupId,
|
good_group_id: row.goodGroupId,
|
||||||
inventory_control: row.inventory_control,
|
inventory_control: row.inventoryControl,
|
||||||
inventory: row.inventory,
|
inventory: row.inventory,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
pay_num: row.payNum,
|
pay_num: row.payNum,
|
||||||
@@ -427,7 +637,7 @@ const submitForm = () => {
|
|||||||
good_group_id: Number(productForm.good_group_id), // 确保是数字类型
|
good_group_id: Number(productForm.good_group_id), // 确保是数字类型
|
||||||
cover_id: productForm.cover_id || 0,
|
cover_id: productForm.cover_id || 0,
|
||||||
inventory: productForm.inventory || 0,
|
inventory: productForm.inventory || 0,
|
||||||
price: productForm.price || 0,
|
price: productForm.price/100 || 0,
|
||||||
pay_num: productForm.pay_num || 1,
|
pay_num: productForm.pay_num || 1,
|
||||||
expire_time: productForm.expire_time || 0,
|
expire_time: productForm.expire_time || 0,
|
||||||
recommend_rebate: productForm.recommend_rebate || 0
|
recommend_rebate: productForm.recommend_rebate || 0
|
||||||
@@ -457,7 +667,273 @@ const submitForm = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchProductList()
|
fetchProductList()
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
|
fetchTagList()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 参数管理相关逻辑
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const paramDialogVisible = ref(false)
|
||||||
|
const paramLoading = ref(false)
|
||||||
|
const parameterList = ref([])
|
||||||
|
const currentProductId = ref(null)
|
||||||
|
|
||||||
|
const paramFormDialogVisible = ref(false)
|
||||||
|
const paramFormType = ref('add')
|
||||||
|
const paramFormRef = ref(null)
|
||||||
|
const paramForm = reactive({
|
||||||
|
arg_id: undefined,
|
||||||
|
arg_name: '',
|
||||||
|
arg_type: 'string'
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramRules = {
|
||||||
|
arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }],
|
||||||
|
arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramValuesDialogVisible = ref(false)
|
||||||
|
const paramValuesLoading = ref(false)
|
||||||
|
const paramValueList = ref([])
|
||||||
|
const currentParam = ref(null)
|
||||||
|
|
||||||
|
const paramValueFormDialogVisible = ref(false)
|
||||||
|
const paramValueFormType = ref('add')
|
||||||
|
const paramValueFormRef = ref(null)
|
||||||
|
const paramValueForm = reactive({
|
||||||
|
attr_id: undefined,
|
||||||
|
attr_name: '',
|
||||||
|
attr_value: '',
|
||||||
|
attr_price: 0,
|
||||||
|
index: 0,
|
||||||
|
attr_range: 0,
|
||||||
|
range_type: 'equal'
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramValueRules = {
|
||||||
|
attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }],
|
||||||
|
attr_value: [{ required: true, message: '请输入值', trigger: 'blur' }],
|
||||||
|
attr_price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开参数管理
|
||||||
|
const handleParameter = (row) => {
|
||||||
|
currentProductId.value = row.id
|
||||||
|
paramDialogVisible.value = true
|
||||||
|
fetchParameterList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数列表
|
||||||
|
const fetchParameterList = async () => {
|
||||||
|
if (!currentProductId.value) return
|
||||||
|
paramLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductParameterList({ good_id: currentProductId.value })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
parameterList.value = res.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取参数列表失败')
|
||||||
|
} finally {
|
||||||
|
paramLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数类型显示
|
||||||
|
const getArgTypeText = (type) => {
|
||||||
|
const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' }
|
||||||
|
return typeMap[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getArgTypeTag = (type) => {
|
||||||
|
const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' }
|
||||||
|
return tagMap[type] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增参数
|
||||||
|
const handleAddParameter = () => {
|
||||||
|
paramFormType.value = 'add'
|
||||||
|
paramFormDialogVisible.value = true
|
||||||
|
Object.assign(paramForm, {
|
||||||
|
arg_id: undefined,
|
||||||
|
arg_name: '',
|
||||||
|
arg_type: 'string'
|
||||||
|
})
|
||||||
|
paramFormRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑参数
|
||||||
|
const handleEditParameter = (row) => {
|
||||||
|
paramFormType.value = 'edit'
|
||||||
|
paramFormDialogVisible.value = true
|
||||||
|
Object.assign(paramForm, {
|
||||||
|
arg_id: row.id,
|
||||||
|
arg_name: row.name,
|
||||||
|
arg_type: row.type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除参数
|
||||||
|
const handleDeleteParameter = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteProductParameter({ good_id: currentProductId.value, arg_id: row.id })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchParameterList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交参数表单
|
||||||
|
const submitParamForm = () => {
|
||||||
|
paramFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
const submitData = {
|
||||||
|
good_id: Number(currentProductId.value),
|
||||||
|
arg_name: paramForm.arg_name,
|
||||||
|
arg_type: paramForm.arg_type
|
||||||
|
}
|
||||||
|
if (paramFormType.value === 'edit') {
|
||||||
|
submitData.arg_id = paramForm.arg_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = paramFormType.value === 'add'
|
||||||
|
? await createProductParameter(submitData)
|
||||||
|
: await updateProductParameter(submitData)
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功')
|
||||||
|
paramFormDialogVisible.value = false
|
||||||
|
fetchParameterList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看参数值
|
||||||
|
const handleViewParamValues = (row) => {
|
||||||
|
currentParam.value = row
|
||||||
|
paramValuesDialogVisible.value = true
|
||||||
|
fetchParamValuesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数值列表
|
||||||
|
const fetchParamValuesList = async () => {
|
||||||
|
if (!currentProductId.value || !currentParam.value) return
|
||||||
|
paramValuesLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductParameterDetail({
|
||||||
|
good_id: currentProductId.value,
|
||||||
|
arg_id: currentParam.value.id
|
||||||
|
})
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
paramValueList.value = res.data.data.attrs || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取参数值列表失败')
|
||||||
|
} finally {
|
||||||
|
paramValuesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加参数值
|
||||||
|
const handleAddParamValue = () => {
|
||||||
|
paramValueFormType.value = 'add'
|
||||||
|
paramValueFormDialogVisible.value = true
|
||||||
|
Object.assign(paramValueForm, {
|
||||||
|
attr_id: undefined,
|
||||||
|
attr_name: '',
|
||||||
|
attr_value: '',
|
||||||
|
attr_price: 0,
|
||||||
|
index: 0,
|
||||||
|
attr_range: 0,
|
||||||
|
range_type: 'equal'
|
||||||
|
})
|
||||||
|
paramValueFormRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑参数值
|
||||||
|
const handleEditParamValue = (row) => {
|
||||||
|
paramValueFormType.value = 'edit'
|
||||||
|
paramValueFormDialogVisible.value = true
|
||||||
|
Object.assign(paramValueForm, {
|
||||||
|
attr_id: row.id,
|
||||||
|
attr_name: row.name,
|
||||||
|
attr_value: row.value,
|
||||||
|
attr_price: row.price / 100,
|
||||||
|
index: row.index || 0,
|
||||||
|
attr_range: row.attr_range || 0,
|
||||||
|
range_type: row.range_type || 'equal'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除参数值
|
||||||
|
const handleDeleteParamValue = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteProductParameterValue({
|
||||||
|
good_id: currentProductId.value,
|
||||||
|
attr_id: row.id
|
||||||
|
})
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchParamValuesList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交参数值表单
|
||||||
|
const submitParamValueForm = () => {
|
||||||
|
paramValueFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
const submitData = {
|
||||||
|
good_id: Number(currentProductId.value),
|
||||||
|
arg_id: Number(currentParam.value.id),
|
||||||
|
attr_name: paramValueForm.attr_name,
|
||||||
|
attr_value: paramValueForm.attr_value,
|
||||||
|
attr_price: paramValueForm.attr_price,
|
||||||
|
index: Number(paramValueForm.index),
|
||||||
|
attr_range: Number(paramValueForm.attr_range),
|
||||||
|
range_type: paramValueForm.range_type
|
||||||
|
}
|
||||||
|
if (paramValueFormType.value === 'edit') {
|
||||||
|
submitData.attr_id = paramValueForm.attr_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = paramValueFormType.value === 'add'
|
||||||
|
? await addProductParameterValue(submitData)
|
||||||
|
: await updateProductParameterValue(submitData)
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功')
|
||||||
|
paramValueFormDialogVisible.value = false
|
||||||
|
fetchParamValuesList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -465,33 +941,144 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item__label) {
|
||||||
|
margin-right: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
@media (max-width: 768px) {
|
||||||
border-radius: 8px;
|
.filter-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
color: #f56c6c;
|
color: #e74c3c;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
: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-checkbox { width: 55px; }
|
||||||
|
.skeleton-id { width: 100px; }
|
||||||
|
.skeleton-image { width: 60px; height: 60px; }
|
||||||
|
.skeleton-name { width: 200px; }
|
||||||
|
.skeleton-table { width: 150px; }
|
||||||
|
.skeleton-price { width: 120px; }
|
||||||
|
.skeleton-tag { width: 100px; }
|
||||||
|
.skeleton-inventory { width: 100px; }
|
||||||
|
.skeleton-action { width: 200px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,637 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="product-parameter-container">
|
|
||||||
<!-- 操作栏 -->
|
|
||||||
<el-card class="filter-container" shadow="never">
|
|
||||||
<el-form ref="queryFormRef" label-width="100px" :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>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 商品参数列表 -->
|
|
||||||
<el-card class="table-container" shadow="never">
|
|
||||||
<el-table
|
|
||||||
v-loading="loading"
|
|
||||||
:data="parameterList"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<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 }">
|
|
||||||
<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>
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 商品参数表单对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogType === 'add' ? '新增商品参数' : '编辑商品参数'"
|
|
||||||
width="600px"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 参数值管理对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="valuesDialogVisible"
|
|
||||||
title="参数值管理"
|
|
||||||
width="800px"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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 }">
|
|
||||||
<el-button type="primary" link @click="handleEditValue(row)">编辑</el-button>
|
|
||||||
<el-button type="danger" link @click="handleDeleteValue(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 参数值表单对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="valueDialogVisible"
|
|
||||||
:title="valueDialogType === 'add' ? '添加参数值' : '编辑参数值'"
|
|
||||||
width="500px"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<el-button @click="valueDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitValueForm">确定</el-button>
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 24px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,49 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="global-setting-container">
|
<div class="global-setting-container">
|
||||||
<!-- 页面头部 -->
|
<el-card class="main-container" shadow="never">
|
||||||
<div class="page-header">
|
|
||||||
<div class="left">
|
|
||||||
<h2 class="title">全局设置</h2>
|
|
||||||
<el-tag type="info" effect="plain" class="info-tag">系统全局配置管理</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">
|
|
||||||
新增设置
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" @click="handleRefresh" :icon="Refresh" class="action-btn">
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索筛选 -->
|
<!-- 搜索筛选 -->
|
||||||
<!-- <el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="设置名称">
|
<el-form-item label="设置名称">
|
||||||
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable />
|
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="权限">
|
<el-form-item label="权限">
|
||||||
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable>
|
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable style="width: 120px">
|
||||||
<el-option label="全部" value="" />
|
<el-option label="全部" value="" />
|
||||||
<el-option label="公有" value="0" />
|
<el-option label="公有" value="0" />
|
||||||
<el-option label="私有" value="1" />
|
<el-option label="私有" value="1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<el-icon><Delete /></el-icon>重置
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card> -->
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>新增设置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleRefresh">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 设置列表 -->
|
<!-- 设置列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="settingsList"
|
:data="settingsList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
border
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
stripe
|
|
||||||
>
|
>
|
||||||
<el-table-column prop="setting_id" label="ID" width="80" />
|
<el-table-column prop="setting_id" label="ID" width="80" />
|
||||||
<el-table-column prop="name" label="Name值" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="name" label="Name值" min-width="200" show-overflow-tooltip />
|
||||||
@@ -57,21 +55,22 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
|
||||||
<el-tooltip content="编辑" placement="top">
|
<el-tooltip content="编辑" placement="top">
|
||||||
<el-button type="primary" :icon="Edit" circle size="small" @click="handleEdit(row)" />
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="删除" placement="top">
|
<el-tooltip content="删除" placement="top">
|
||||||
<el-button type="danger" :icon="Delete" circle size="small" @click="handleDelete(row)" />
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 新增/编辑设置对话框 -->
|
<!-- 新增/编辑设置对话框 -->
|
||||||
@@ -256,13 +255,17 @@ const getAuthorityText = (authority) => {
|
|||||||
return authority === 0 ? '公有' : '私有'
|
return authority === 0 ? '公有' : '私有'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const handleQuery = () => {
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.name = ''
|
||||||
|
queryParams.authority = ''
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新
|
// 刷新
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
@@ -387,65 +390,83 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.global-setting-container {
|
.global-setting-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面标题样式 */
|
.main-container {
|
||||||
.page-header {
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
padding: 16px 20px;
|
||||||
padding-bottom: 16px;
|
gap: 20px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-tag {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 筛选容器 */
|
|
||||||
.filter-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 0;
|
margin: 0;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
/* 表格容器 */
|
|
||||||
.table-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
|
||||||
/* 对话框底部 */
|
/* 对话框底部 */
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
@@ -455,20 +476,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.page-header {
|
.filter-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: stretch;
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header .actions {
|
.search-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-bar {
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
gap: 4px;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="domain-whitelist-container">
|
<div class="domain-whitelist-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="域名">
|
<el-form-item label="域名">
|
||||||
<el-input v-model="queryParams.domain" placeholder="请输入域名" clearable />
|
<el-input v-model="queryParams.domain" placeholder="请输入域名" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><plus /></el-icon>新增域名
|
<el-icon><Plus /></el-icon>新增域名
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button type="success" @click="getList">
|
<el-button type="success" @click="getList">
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||||
<el-icon><delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 域名列表 -->
|
<!-- 域名列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="domainList"
|
:data="domainList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -62,6 +67,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 域名表单对话框 -->
|
<!-- 域名表单对话框 -->
|
||||||
@@ -94,7 +100,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete } from '@element-plus/icons-vue'
|
import { Plus, Delete, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
import { getDomainList, addDomain, deleteDomain, batchDeleteDomain } from '@/api/domain'
|
import { getDomainList, addDomain, deleteDomain, batchDeleteDomain } from '@/api/domain'
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
@@ -284,35 +290,93 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.domain-whitelist-container {
|
.domain-whitelist-container {
|
||||||
padding: 10px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 10px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
gap: 12px;
|
||||||
margin-bottom: 10px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
margin-bottom: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 15px;
|
margin-top: 20px;
|
||||||
display: flex;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="operation-log">
|
<div class="operation-log">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="操作人">
|
<el-form-item label="操作人">
|
||||||
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable />
|
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="操作类型">
|
<el-form-item label="操作类型">
|
||||||
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable>
|
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable style="width: 160px">
|
||||||
<el-option label="登录" value="login" />
|
<el-option label="登录" value="login" />
|
||||||
<el-option label="新增" value="create" />
|
<el-option label="新增" value="create" />
|
||||||
<el-option label="修改" value="update" />
|
<el-option label="修改" value="update" />
|
||||||
@@ -22,29 +25,34 @@
|
|||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="success">
|
<el-button type="success">
|
||||||
<el-icon><upload /></el-icon>导入
|
<el-icon><Upload /></el-icon>导入
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" @click="handleExport">
|
<el-button type="primary" @click="handleExport">
|
||||||
<el-icon><download /></el-icon>导出
|
<el-icon><Download /></el-icon>导出
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 日志列表 -->
|
<!-- 日志列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="logList"
|
:data="logList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column label="操作人" min-width="150">
|
<el-table-column label="操作人" min-width="150">
|
||||||
@@ -84,6 +92,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 详情对话框 -->
|
<!-- 详情对话框 -->
|
||||||
@@ -117,7 +126,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Upload, Download } from '@element-plus/icons-vue'
|
import { Upload, Download, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
@@ -247,57 +256,87 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
}
|
||||||
|
|
||||||
|
.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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
background: transparent;
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f8f9fb !important;
|
background: #f8f9fa !important;
|
||||||
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
font-size: 13px;
|
||||||
height: 50px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table td) {
|
:deep(.el-table td) {
|
||||||
padding: 12px 0;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
:deep(.el-table tr:hover > td) {
|
||||||
background: #f8fafc;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__body tr:hover > td) {
|
:deep(.el-card__body) {
|
||||||
background-color: #f1f5f9 !important;
|
padding: 0;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__body tr) {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.operator-info {
|
.operator-info {
|
||||||
@@ -322,37 +361,6 @@ onMounted(() => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页样式优化 */
|
|
||||||
.pagination {
|
|
||||||
margin-top: 24px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
--el-pagination-hover-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination button:disabled) {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li) {
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 2px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li.active) {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li:hover:not(.active)) {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -367,29 +375,4 @@ onMounted(() => {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式优化 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar .el-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table th) {
|
|
||||||
padding: 6px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table td) {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="permission-admin-container">
|
<div class="permission-admin-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="类型">
|
<el-form-item label="类型">
|
||||||
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 150px" @change="handleOwnerTypeChange">
|
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 150px" @change="handleOwnerTypeChange">
|
||||||
@@ -10,10 +13,6 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
|
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
|
||||||
<!-- <el-select v-model="queryParams.user_id" placeholder="请选择用户" clearable filterable style="width: 200px">
|
|
||||||
<el-option v-for="item in userOptions" :key="item.UserId" :label="`${item.UserName} (ID: ${item.UserId})`" :value="item.UserId" />
|
|
||||||
</el-select> -->
|
|
||||||
|
|
||||||
<div class="user_selector-inline">
|
<div class="user_selector-inline">
|
||||||
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
|
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
|
||||||
{{ getQueryUserName() }}
|
{{ getQueryUserName() }}
|
||||||
@@ -44,14 +43,16 @@
|
|||||||
<el-icon><Refresh/></el-icon>刷新
|
<el-icon><Refresh/></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 管理员权限列表 -->
|
<!-- 管理员权限列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="adminPermissionList"
|
:data="adminPermissionList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column label="拥有者类型" width="120">
|
<el-table-column label="拥有者类型" width="120">
|
||||||
@@ -111,7 +112,9 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 用户选择弹窗 -->
|
<!-- 用户选择弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="userSelectorVisible"
|
v-model="userSelectorVisible"
|
||||||
@@ -745,26 +748,55 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,5 +806,35 @@ onMounted(() => {
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="permission-route-container">
|
<div class="permission-route-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="关键词">
|
<el-form-item label="关键词">
|
||||||
<el-input v-model="queryParams.key" placeholder="请输入关键词搜索" clearable style="width: 250px" />
|
<el-input v-model="queryParams.key" placeholder="请输入关键词搜索" clearable style="width: 250px" />
|
||||||
@@ -17,20 +20,20 @@
|
|||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增路由权限
|
<el-icon><Plus /></el-icon>新增路由权限
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button type="success" @click="fetchPermissionList">
|
<el-button type="success" @click="fetchPermissionList">
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 路由权限列表 -->
|
<!-- 路由权限列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="permissionList"
|
:data="permissionList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="name" label="权限名称" min-width="200" />
|
<el-table-column prop="name" label="权限名称" min-width="200" />
|
||||||
@@ -56,6 +59,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 路由权限表单对话框 -->
|
<!-- 路由权限表单对话框 -->
|
||||||
@@ -91,7 +95,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Search } from '@element-plus/icons-vue'
|
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getPermissionList,
|
getPermissionList,
|
||||||
addPermissionInfo,
|
addPermissionInfo,
|
||||||
@@ -255,27 +259,84 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-container">
|
<div class="setting-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="配置组">
|
<el-form-item label="配置组">
|
||||||
<el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery">
|
<el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery">
|
||||||
@@ -31,15 +34,17 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 配置列表 -->
|
<!-- 配置列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="settingList"
|
:data="settingList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -93,6 +98,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 配置表单对话框 -->
|
<!-- 配置表单对话框 -->
|
||||||
@@ -509,27 +515,86 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-group-container">
|
<div class="setting-group-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="关键词筛选">
|
<el-form-item label="关键词筛选">
|
||||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||||
@@ -21,15 +24,17 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 配置组列表 -->
|
<!-- 配置组列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="groupList"
|
:data="groupList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -65,6 +70,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 配置组表单对话框 -->
|
<!-- 配置组表单对话框 -->
|
||||||
@@ -318,27 +324,86 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="system-file-container">
|
<div class="system-file-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="关键词筛选">
|
<el-form-item label="关键词筛选">
|
||||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||||
@@ -28,17 +31,18 @@
|
|||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 文件列表 -->
|
<!-- 文件列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="fileList"
|
:data="fileList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -92,6 +96,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 文件详情对话框 -->
|
<!-- 文件详情对话框 -->
|
||||||
@@ -250,7 +255,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture } from '@element-plus/icons-vue'
|
import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture, Refresh } from '@element-plus/icons-vue'
|
||||||
import { getFileList, getFileDetail, updateFile, deleteFile, uploadFile } from '@/api/admin/file'
|
import { getFileList, getFileDetail, updateFile, deleteFile, uploadFile } from '@/api/admin/file'
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
@@ -672,22 +677,56 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-icon {
|
.file-icon {
|
||||||
@@ -700,11 +739,6 @@ onMounted(() => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 24px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-detail-container {
|
.file-detail-container {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
@@ -767,5 +801,35 @@ onMounted(() => {
|
|||||||
:deep(.el-descriptions__label) {
|
:deep(.el-descriptions__label) {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
||||||
|
|||||||
+81
-98
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="users-container">
|
<div class="users-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="用户名">
|
<el-form-item label="用户名">
|
||||||
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
|
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||||
<el-option label="启用" value="1" />
|
<el-option label="启用" value="1" />
|
||||||
<el-option label="禁用" value="0" />
|
<el-option label="禁用" value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -20,36 +23,41 @@
|
|||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<el-icon><Search /></el-icon>查询
|
||||||
|
</el-button>
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><plus /></el-icon>新增用户
|
<el-icon><Plus /></el-icon>新增用户
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
|
||||||
<el-icon><delete /></el-icon>批量删除
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success">
|
<el-button type="success">
|
||||||
<el-icon><upload /></el-icon>导入
|
<el-icon><Upload /></el-icon>导入
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button>
|
<el-button type="primary" plain>
|
||||||
<el-icon><download /></el-icon>导出
|
<el-icon><Download /></el-icon>导出
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||||
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户列表 -->
|
<!-- 用户列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<div class="table-section">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="userList"
|
:data="userList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -77,7 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button type="primary" link @click="handleRoleAssign(row)">分配角色</el-button>
|
<el-button type="primary" link @click="handleRoleAssign(row)">分配角色</el-button>
|
||||||
@@ -98,6 +106,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 用户表单对话框 -->
|
<!-- 用户表单对话框 -->
|
||||||
@@ -168,7 +177,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete, Upload, Download } from '@element-plus/icons-vue'
|
import { Plus, Delete, Upload, Download, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
@@ -439,57 +448,56 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
}
|
||||||
|
|
||||||
|
.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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
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 {
|
.action-bar {
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式优化 */
|
.pagination {
|
||||||
:deep(.el-table) {
|
margin-top: 20px;
|
||||||
border-radius: 8px;
|
padding: 16px 20px;
|
||||||
overflow: hidden;
|
border-top: 1px solid #e1e8ed;
|
||||||
background: transparent;
|
background: #fafbfc;
|
||||||
}
|
justify-content: flex-end;
|
||||||
|
|
||||||
:deep(.el-table th) {
|
|
||||||
background-color: #f8f9fb !important;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
height: 50px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table td) {
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__body tr:hover > td) {
|
|
||||||
background-color: #f1f5f9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__body tr) {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@@ -514,65 +522,40 @@ onMounted(() => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页样式优化 */
|
|
||||||
.pagination {
|
|
||||||
margin-top: 24px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
--el-pagination-hover-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination button:disabled) {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li) {
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 2px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li.active) {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination .el-pager li:hover:not(.active)) {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式优化 */
|
/* 表格样式优化 */
|
||||||
@media (max-width: 768px) {
|
:deep(.el-table) {
|
||||||
.el-form-item {
|
border: none;
|
||||||
margin-bottom: 12px;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
:deep(.el-table__header) {
|
||||||
flex-direction: column;
|
background: #f8f9fa;
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar .el-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
padding: 6px 0;
|
background: #f8f9fa !important;
|
||||||
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table td) {
|
:deep(.el-table td) {
|
||||||
padding: 8px 0;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-table tr:hover > td) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+303
-117
@@ -33,7 +33,7 @@
|
|||||||
@click="selectTicket(ticket)"
|
@click="selectTicket(ticket)"
|
||||||
>
|
>
|
||||||
<div class="ticket-avatar">
|
<div class="ticket-avatar">
|
||||||
<el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar>
|
<el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar>
|
||||||
</div>
|
</div>
|
||||||
<div class="ticket-content">
|
<div class="ticket-content">
|
||||||
<div class="ticket-top">
|
<div class="ticket-top">
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
|
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
|
||||||
>
|
>
|
||||||
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
|
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
|
||||||
<el-avatar :size="36" :src="getUserAvatar(message.userId)">
|
<el-avatar :size="36" :src="message.avatar">
|
||||||
{{ currentTicket.username.charAt(0) }}
|
{{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<div class="message-time">{{ formatMessageTime(message.time) }}</div>
|
<div class="message-time">{{ formatMessageTime(message.time) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
|
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
|
||||||
<el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar>
|
<el-avatar :size="36" :src="message.avatar">A</el-avatar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,15 +210,18 @@ import {
|
|||||||
closeTicket,
|
closeTicket,
|
||||||
getUserAvatar,
|
getUserAvatar,
|
||||||
getFileImage,
|
getFileImage,
|
||||||
parseFilesToImages
|
parseFilesToImages,
|
||||||
|
getTicketCount
|
||||||
} from '@/api/ticket'
|
} from '@/api/ticket'
|
||||||
|
import notificationSound from '@/assets/7.wav'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
|
||||||
// 路由相关
|
// 路由相关
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 管理员ID列表(客服ID)
|
// 用户 store
|
||||||
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 头像
|
// 头像
|
||||||
const adminAvatar = ref('')
|
const adminAvatar = ref('')
|
||||||
@@ -271,6 +274,11 @@ const stats = reactive({
|
|||||||
isLoadingStats: false
|
isLoadingStats: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 上一次的待处理数量,用于判断是否有新工单
|
||||||
|
const previousPendingCount = ref(0)
|
||||||
|
// 音频对象
|
||||||
|
const audio = new Audio(notificationSound)
|
||||||
|
|
||||||
// 快捷回复选项
|
// 快捷回复选项
|
||||||
const quickReplies = ref([
|
const quickReplies = ref([
|
||||||
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
|
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
|
||||||
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
|
|||||||
const mappedTickets = tickets.map(item => ({
|
const mappedTickets = tickets.map(item => ({
|
||||||
id: item.work_id,
|
id: item.work_id,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息
|
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||||||
userId: item.user_id,
|
userId: item.user?.userId,
|
||||||
|
avatar: item.user?.coverUrl || '',
|
||||||
createTime: new Date(item.created_at).toLocaleString(),
|
createTime: new Date(item.created_at).toLocaleString(),
|
||||||
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||||||
status: convertStatusToString(item.status),
|
status: convertStatusToString(item.status),
|
||||||
@@ -336,9 +345,24 @@ const fetchTicketList = async (append = false) => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
ticketList.value = [...ticketList.value, ...mappedTickets]
|
// 翻页时:合并列表,使用work_id去重,然后按work_id从大到小排序
|
||||||
|
const existingIds = new Set(ticketList.value.map(t => t.id))
|
||||||
|
const newTickets = mappedTickets.filter(t => !existingIds.has(t.id))
|
||||||
|
const mergedTickets = [...ticketList.value, ...newTickets]
|
||||||
|
|
||||||
|
// 按work_id从大到小排序
|
||||||
|
ticketList.value = mergedTickets.sort((a, b) => {
|
||||||
|
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
|
||||||
|
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
|
||||||
|
return idB - idA
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
ticketList.value = mappedTickets
|
// 首次加载或切换类别:直接使用新数据,按work_id从大到小排序
|
||||||
|
ticketList.value = mappedTickets.sort((a, b) => {
|
||||||
|
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
|
||||||
|
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
|
||||||
|
return idB - idA
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMore.value = ticketList.value.length < res.data.all_count
|
hasMore.value = ticketList.value.length < res.data.all_count
|
||||||
@@ -353,44 +377,35 @@ const fetchTicketList = async (append = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取单个状态的工单数量
|
|
||||||
const fetchStatusStat = async (status) => {
|
|
||||||
try {
|
|
||||||
// 将状态字符串转换为API所需的状态值
|
|
||||||
let statusValue = '';
|
|
||||||
if (status === 'pending') statusValue = '0';
|
|
||||||
else if (status === 'processing') statusValue = '1';
|
|
||||||
else if (status === 'replied') statusValue = '2';
|
|
||||||
else if (status === 'completed') statusValue = '3';
|
|
||||||
|
|
||||||
const res = await getTickerList(10, 1, statusValue) // 只请求一条数据,但获取总数
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
if (status === '') {
|
|
||||||
stats.total = res.data.all_count
|
|
||||||
} else {
|
|
||||||
stats[status] = res.data.all_count
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`获取${status || '全部'}工单统计失败:`, res.message)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`获取${status || '全部'}工单统计出错:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有状态的工单数量
|
// 获取所有状态的工单数量
|
||||||
const fetchAllStats = async () => {
|
const fetchAllStats = async () => {
|
||||||
stats.isLoadingStats = true
|
stats.isLoadingStats = true
|
||||||
try {
|
try {
|
||||||
// 并行获取各个状态的工单数量
|
const res = await getTicketCount()
|
||||||
await Promise.all([
|
if (res.code === 200) {
|
||||||
fetchStatusStat(''), // 获取全部工单数量
|
const data = res.data
|
||||||
fetchStatusStat('pending'), // 待处理
|
|
||||||
fetchStatusStat('processing'), // 处理中
|
// 检查是否有新工单(待处理数量增加)
|
||||||
fetchStatusStat('replied'), // 已回复
|
if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
|
||||||
fetchStatusStat('completed') // 已完成
|
try {
|
||||||
])
|
audio.play().catch(e => console.error('播放提示音失败:', e))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('播放提示音出错:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上一次的数量
|
||||||
|
previousPendingCount.value = data.wait_count
|
||||||
|
|
||||||
|
stats.total = data.all_count
|
||||||
|
stats.pending = data.wait_count
|
||||||
|
stats.replied = data.reply_count
|
||||||
|
stats.completed = data.close_count
|
||||||
|
// 计算处理中的数量:总数 - 待处理 - 已回复 - 已完成
|
||||||
|
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
|
||||||
|
} else {
|
||||||
|
console.error('获取工单统计失败:', res.message)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取工单统计数据出错:', error)
|
console.error('获取工单统计数据出错:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -398,6 +413,12 @@ const fetchAllStats = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新统计数据(用于定时刷新)
|
||||||
|
const fetchCurrentStatusStat = async () => {
|
||||||
|
await fetchAllStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 加载更多工单
|
// 加载更多工单
|
||||||
const loadMoreTickets = () => {
|
const loadMoreTickets = () => {
|
||||||
if (!hasMore.value || isLoading.value) return
|
if (!hasMore.value || isLoading.value) return
|
||||||
@@ -436,22 +457,22 @@ const filteredTickets = computed(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按最后回复时间排序(优先显示有新消息的)
|
// 按work_id从大到小排序(优先显示待处理)
|
||||||
return result.sort((a, b) => {
|
return result.sort((a, b) => {
|
||||||
// 优先显示待处理
|
// 优先显示待处理
|
||||||
if (a.status === 'pending' && b.status !== 'pending') return -1
|
if (a.status === 'pending' && b.status !== 'pending') return -1
|
||||||
if (a.status !== 'pending' && b.status === 'pending') return 1
|
if (a.status !== 'pending' && b.status === 'pending') return 1
|
||||||
|
|
||||||
// 然后按最后回复时间
|
// 然后按work_id从大到小排序
|
||||||
const timeA = new Date(a.lastReplyTime || a.createTime)
|
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
|
||||||
const timeB = new Date(b.lastReplyTime || b.createTime)
|
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
|
||||||
return timeB - timeA
|
return idB - idA
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否是客服
|
// 判断是否是当前登录的管理员
|
||||||
const isAdmin = (userId) => {
|
const isAdmin = (userId) => {
|
||||||
return adminUserIds.includes(userId)
|
return userId === userStore.userInfo?.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态转换
|
// 状态转换
|
||||||
@@ -513,25 +534,25 @@ const fetchTicketMessages = async (workId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息列表
|
// 处理消息列表
|
||||||
if (detail.Content && detail.Content.length > 0) {
|
if (detail.content && detail.content.length > 0) {
|
||||||
// 使用Promise.all一次性处理所有消息和图片
|
// 处理所有消息
|
||||||
const messagesPromises = detail.Content.map(async (msg) => {
|
const messages = detail.content.map((msg) => {
|
||||||
const isAdminMsg = isAdmin(msg.UserId)
|
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||||
const images = await parseFilesToImages(msg.Flies)
|
// 从 flies 数组中提取图片 URL
|
||||||
|
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: msg.Id,
|
id: msg.id,
|
||||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
content: msg.content !== 'empty' ? msg.content : null,
|
||||||
images: images,
|
images: images,
|
||||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
||||||
isAdmin: isAdminMsg,
|
isAdmin: isAdminMsg,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
userId: msg.UserId
|
userId: msg.user?.userId,
|
||||||
|
avatar: msg.user?.coverUrl || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 等待所有消息处理完成
|
|
||||||
const messages = await Promise.all(messagesPromises)
|
|
||||||
currentMessages.value = messages
|
currentMessages.value = messages
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -562,23 +583,29 @@ const sendMessage = async () => {
|
|||||||
const fileIds = []
|
const fileIds = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 添加一个临时的"正在发送"消息
|
// 保存输入内容
|
||||||
|
const inputMsg = messageInput.value.trim()
|
||||||
|
const inputImages = [...selectedImages.value]
|
||||||
|
|
||||||
|
// 清空输入和已选图片
|
||||||
|
messageInput.value = ''
|
||||||
|
selectedImages.value = []
|
||||||
|
|
||||||
|
// 立即添加消息到界面(不显示 loading)
|
||||||
const tempMsg = {
|
const tempMsg = {
|
||||||
content: messageInput.value.trim() || null,
|
id: Date.now(), // 临时 ID
|
||||||
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null,
|
content: inputMsg || null,
|
||||||
|
images: inputImages.length > 0 ? inputImages : [],
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isLoading: true,
|
isSystem: false,
|
||||||
|
userId: userStore.userInfo?.user_id,
|
||||||
|
avatar: userStore.userInfo?.cover_url || '',
|
||||||
isTempMessage: true
|
isTempMessage: true
|
||||||
}
|
}
|
||||||
|
|
||||||
currentMessages.value.push(tempMsg)
|
currentMessages.value.push(tempMsg)
|
||||||
|
|
||||||
// 清空输入和已选图片
|
|
||||||
const inputMsg = messageInput.value
|
|
||||||
messageInput.value = ''
|
|
||||||
selectedImages.value = []
|
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
@@ -606,6 +633,7 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
// 恢复输入内容
|
// 恢复输入内容
|
||||||
messageInput.value = inputMsg
|
messageInput.value = inputMsg
|
||||||
|
selectedImages.value = inputImages
|
||||||
|
|
||||||
ElMessage.error(res.message || '发送失败')
|
ElMessage.error(res.message || '发送失败')
|
||||||
}
|
}
|
||||||
@@ -685,7 +713,8 @@ const filterByStatus = (status) => {
|
|||||||
activeStatus.value = status
|
activeStatus.value = status
|
||||||
currentPage.value = 1 // 切换状态后重置页码
|
currentPage.value = 1 // 切换状态后重置页码
|
||||||
hasMore.value = true // 重置加载更多标志
|
hasMore.value = true // 重置加载更多标志
|
||||||
fetchTicketList() // 重新获取数据
|
ticketList.value = [] // 清空列表,不缓存
|
||||||
|
fetchTicketList(false) // 从头重新获取数据,不追加
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
@@ -701,43 +730,150 @@ const updateTicketStats = () => {
|
|||||||
|
|
||||||
// 格式化消息时间
|
// 格式化消息时间
|
||||||
const formatMessageTime = (timeStr) => {
|
const formatMessageTime = (timeStr) => {
|
||||||
|
if (!timeStr) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
const date = new Date(timeStr)
|
const date = new Date(timeStr)
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
if (isNaN(date.getTime())) return ''
|
||||||
|
|
||||||
|
// 格式化为 HH:MM
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
} catch (e) {
|
||||||
|
console.error('时间格式化失败:', e)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化列表项时间
|
// 格式化列表项时间
|
||||||
const formatTime = (timeStr) => {
|
const formatTime = (timeStr) => {
|
||||||
const date = new Date(timeStr)
|
if (!timeStr) return ''; // 空值兜底
|
||||||
const now = new Date()
|
|
||||||
const diff = now - date
|
|
||||||
|
|
||||||
// 今天内的消息只显示时间
|
// 步骤1:解析中文时间字符串(核心适配点)
|
||||||
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
let date;
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
try {
|
||||||
|
// 先尝试原生解析(兼容ISO格式)
|
||||||
|
date = new Date(timeStr);
|
||||||
|
// 若原生解析失败(返回Invalid Date),解析中文格式
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
// 正则提取中文时间的年、月、日、时、分、秒
|
||||||
|
const cnTimeMatch = timeStr.match(
|
||||||
|
/(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/
|
||||||
|
);
|
||||||
|
if (cnTimeMatch) {
|
||||||
|
const [, year, month, day, period, hour, minute, second] = cnTimeMatch;
|
||||||
|
// 处理下午/上午的小时转换(12小时制转24小时制)
|
||||||
|
let hour24 = parseInt(hour, 10);
|
||||||
|
if (period === '下午' && hour24 !== 12) {
|
||||||
|
hour24 += 12;
|
||||||
|
}
|
||||||
|
if (period === '上午' && hour24 === 12) {
|
||||||
|
hour24 = 0; // 上午12点转为0点
|
||||||
|
}
|
||||||
|
// 构造日期(月份从0开始,需-1)
|
||||||
|
date = new Date(
|
||||||
|
parseInt(year, 10),
|
||||||
|
parseInt(month, 10) - 1,
|
||||||
|
parseInt(day, 10),
|
||||||
|
hour24,
|
||||||
|
parseInt(minute, 10),
|
||||||
|
parseInt(second, 10)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return '无效时间'; // 既不是ISO也不是中文格式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('时间解析失败:', e);
|
||||||
|
return '无效时间';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一周内的显示星期几
|
const now = new Date();
|
||||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
const dateTime = date.getTime();
|
||||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
const nowTime = now.getTime();
|
||||||
return weekdays[date.getDay()]
|
const diff = nowTime - dateTime;
|
||||||
|
|
||||||
|
// 步骤2:判断“今天”(年/月/日完全一致)
|
||||||
|
const isToday = date.getFullYear() === now.getFullYear() &&
|
||||||
|
date.getMonth() === now.getMonth() &&
|
||||||
|
date.getDate() === now.getDate();
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// 格式化今天的时间(24小时制,补零)
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${hour}:${minute}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他显示日期
|
// 步骤3:判断“一周内”
|
||||||
return date.toLocaleDateString()
|
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
if (diff < oneWeek) {
|
||||||
|
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||||
|
return weekdays[date.getDay()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 步骤4:格式化其他日期(补零,统一格式:YYYY/MM/DD)
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}/${month}/${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
const formatDate = (timeStr) => {
|
const formatDate = (timeStr) => {
|
||||||
const date = new Date(timeStr)
|
console.log("原始时间字符串:", timeStr);
|
||||||
const now = new Date()
|
if (!timeStr) return ''; // 空值兜底
|
||||||
|
|
||||||
if (date.toDateString() === now.toDateString()) {
|
let date;
|
||||||
return '今天'
|
// 1. 先尝试原生解析(兼容ISO等标准格式)
|
||||||
|
date = new Date(timeStr);
|
||||||
|
|
||||||
|
// 2. 若原生解析失败,专门解析中文时间格式
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
// 正则匹配:xxxx年xx月xx日 上午/下午 xx:xx:xx
|
||||||
|
const cnTimeReg = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/;
|
||||||
|
const match = timeStr.match(cnTimeReg);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, year, month, day, period, hour, minute, second] = match;
|
||||||
|
// 处理12小时制转24小时制(关键适配)
|
||||||
|
let hour24 = parseInt(hour, 10);
|
||||||
|
if (period === '下午') {
|
||||||
|
hour24 = hour24 === 12 ? 12 : hour24 + 12; // 下午12点=12点,下午1-11点+12
|
||||||
|
} else { // 上午
|
||||||
|
hour24 = hour24 === 12 ? 0 : hour24; // 上午12点=0点,上午1-11点不变
|
||||||
|
}
|
||||||
|
// 手动构造合法的Date对象(月份从0开始,需-1)
|
||||||
|
date = new Date(
|
||||||
|
parseInt(year, 10),
|
||||||
|
parseInt(month, 10) - 1,
|
||||||
|
parseInt(day, 10),
|
||||||
|
hour24,
|
||||||
|
parseInt(minute, 10),
|
||||||
|
parseInt(second, 10)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return '无效时间'; // 非目标格式,返回兜底
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toLocaleDateString()
|
const now = new Date();
|
||||||
|
// 3. 对比“今天”(按日期维度,忽略时分秒)
|
||||||
|
const isToday = date.getFullYear() === now.getFullYear() &&
|
||||||
|
date.getMonth() === now.getMonth() &&
|
||||||
|
date.getDate() === now.getDate();
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
return '今天';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 格式化非今天的日期(统一格式,避免环境差异)
|
||||||
|
const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
|
|
||||||
// 监听显示工单变化,更新消息和滚动
|
// 监听显示工单变化,更新消息和滚动
|
||||||
watch(currentTicket, (newVal) => {
|
watch(currentTicket, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@@ -775,8 +911,8 @@ const startAutoRefresh = () => {
|
|||||||
// 静默刷新工单列表,保持当前页码
|
// 静默刷新工单列表,保持当前页码
|
||||||
refreshTicketList()
|
refreshTicketList()
|
||||||
|
|
||||||
// 刷新工单统计
|
// 只刷新当前分类的统计数据,减少请求数量
|
||||||
fetchAllStats()
|
fetchCurrentStatusStat()
|
||||||
}, refreshInterval)
|
}, refreshInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +936,8 @@ const refreshTicketList = async () => {
|
|||||||
else if (activeStatus.value === 'completed') statusParam = '3'
|
else if (activeStatus.value === 'completed') statusParam = '3'
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getTickerList(pageSize.value, currentPage.value, statusParam)
|
// 刷新时只获取第一页数据,用于更新最新数据
|
||||||
|
const res = await getTickerList(pageSize.value, 1, statusParam)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const tickets = res.data.data || []
|
const tickets = res.data.data || []
|
||||||
@@ -809,16 +946,36 @@ const refreshTicketList = async () => {
|
|||||||
const mappedTickets = tickets.map(item => ({
|
const mappedTickets = tickets.map(item => ({
|
||||||
id: item.work_id,
|
id: item.work_id,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
username: `用户${item.user_id}`,
|
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||||||
userId: item.user_id,
|
userId: item.user?.userId,
|
||||||
|
avatar: item.user?.coverUrl || '',
|
||||||
createTime: new Date(item.created_at).toLocaleString(),
|
createTime: new Date(item.created_at).toLocaleString(),
|
||||||
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||||||
status: convertStatusToString(item.status),
|
status: convertStatusToString(item.status),
|
||||||
content: item.name
|
content: item.name
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 更新列表
|
// 合并到现有列表,去重并按work_id从大到小排序
|
||||||
ticketList.value = mappedTickets
|
// 使用Map来更新已存在的工单信息,添加新的工单
|
||||||
|
const ticketMap = new Map()
|
||||||
|
// 先添加现有列表
|
||||||
|
ticketList.value.forEach(ticket => {
|
||||||
|
ticketMap.set(ticket.id, ticket)
|
||||||
|
})
|
||||||
|
// 更新或添加新数据(新数据会覆盖旧数据)
|
||||||
|
mappedTickets.forEach(ticket => {
|
||||||
|
ticketMap.set(ticket.id, ticket)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 转换为数组并按work_id从大到小排序
|
||||||
|
ticketList.value = Array.from(ticketMap.values()).sort((a, b) => {
|
||||||
|
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
|
||||||
|
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
|
||||||
|
return idB - idA
|
||||||
|
})
|
||||||
|
|
||||||
|
// 不重置页码,保持用户当前的浏览位置
|
||||||
|
// 更新加载更多标志
|
||||||
hasMore.value = ticketList.value.length < res.data.all_count
|
hasMore.value = ticketList.value.length < res.data.all_count
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -826,6 +983,39 @@ const refreshTicketList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:去除 URL 中的查询参数
|
||||||
|
const normalizeUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
return url.split('?')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:比较两个消息数组是否相同(忽略 URL 查询参数)
|
||||||
|
const areMessagesEqual = (messages1, messages2) => {
|
||||||
|
if (messages1.length !== messages2.length) return false
|
||||||
|
|
||||||
|
for (let i = 0; i < messages1.length; i++) {
|
||||||
|
const msg1 = messages1[i]
|
||||||
|
const msg2 = messages2[i]
|
||||||
|
|
||||||
|
// 比较消息 ID 和内容
|
||||||
|
if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false
|
||||||
|
|
||||||
|
// 比较图片数量
|
||||||
|
if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false
|
||||||
|
|
||||||
|
// 比较图片 URL(去除查询参数)
|
||||||
|
if (msg1.images && msg2.images) {
|
||||||
|
for (let j = 0; j < msg1.images.length; j++) {
|
||||||
|
if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 静默刷新聊天记录(不显示loading状态)
|
// 静默刷新聊天记录(不显示loading状态)
|
||||||
const refreshTicketMessages = async (workId) => {
|
const refreshTicketMessages = async (workId) => {
|
||||||
try {
|
try {
|
||||||
@@ -838,37 +1028,33 @@ const refreshTicketMessages = async (workId) => {
|
|||||||
if (currentTicket.value) {
|
if (currentTicket.value) {
|
||||||
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
|
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
|
||||||
if (currentTicket.value.status !== 'pending') {
|
if (currentTicket.value.status !== 'pending') {
|
||||||
currentTicket.value.status = convertStatusToString(detail.Status)
|
currentTicket.value.status = convertStatusToString(detail.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息列表
|
// 处理消息列表
|
||||||
if (detail.Content && detail.Content.length > 0) {
|
if (detail.content && detail.content.length > 0) {
|
||||||
// 检查是否有新消息
|
// 构建新消息列表
|
||||||
const lastMsgId = currentMessages.value.length > 0 ?
|
const newMessages = detail.content.map((msg) => {
|
||||||
currentMessages.value[currentMessages.value.length - 1].id : 0;
|
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||||
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId);
|
// 从 flies 数组中提取图片 URL
|
||||||
|
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||||
if (hasNewMessage) {
|
|
||||||
// 有新消息时才更新
|
|
||||||
const messagesPromises = detail.Content.map(async (msg) => {
|
|
||||||
const isAdminMsg = isAdmin(msg.UserId)
|
|
||||||
const images = await parseFilesToImages(msg.Flies)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: msg.Id,
|
id: msg.id,
|
||||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
content: msg.content !== 'empty' ? msg.content : null,
|
||||||
images: images,
|
images: images,
|
||||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
||||||
isAdmin: isAdminMsg,
|
isAdmin: isAdminMsg,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
userId: msg.UserId
|
userId: msg.user?.userId,
|
||||||
|
avatar: msg.user?.coverUrl || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 等待所有消息处理完成
|
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化)
|
||||||
const messages = await Promise.all(messagesPromises)
|
if (!areMessagesEqual(currentMessages.value, newMessages)) {
|
||||||
currentMessages.value = messages
|
currentMessages.value = newMessages
|
||||||
|
|
||||||
// 如果有新消息,滚动到底部
|
// 如果有新消息,滚动到底部
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ticket-list-page">
|
||||||
|
<!-- 顶部工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="status-tabs">
|
||||||
|
<div class="tab-item pending" :class="{ active: activeStatus === 'pending' }" @click="filterByStatus('pending')">
|
||||||
|
待处理 <span class="count">{{ stats.pending }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tab-item processing" :class="{ active: activeStatus === 'processing' }" @click="filterByStatus('processing')">
|
||||||
|
处理中 <span class="count">{{ stats.processing }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tab-item replied" :class="{ active: activeStatus === 'replied' }" @click="filterByStatus('replied')">
|
||||||
|
已回复 <span class="count">{{ stats.replied }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tab-item completed" :class="{ active: activeStatus === 'completed' }" @click="filterByStatus('completed')">
|
||||||
|
已完成 <span class="count">{{ stats.completed }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tab-item" :class="{ active: activeStatus === '' }" @click="filterByStatus('')">
|
||||||
|
全部 <span class="count">{{ stats.total }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-select v-model="sortBy" placeholder="排序方式" clearable style="width: 140px" @change="handleSortChange">
|
||||||
|
<el-option label="不排序" value="" />
|
||||||
|
<el-option label="创建时间" value="created_at" />
|
||||||
|
<el-option label="更新时间" value="updated_at" />
|
||||||
|
<el-option label="工单号" value="id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="sortOrder" placeholder="排序顺序" clearable style="width: 100px" @change="handleSortChange">
|
||||||
|
<el-option label="默认" value="" />
|
||||||
|
<el-option label="降序" value="desc" />
|
||||||
|
<el-option label="升序" value="asc" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索工单号、标题、用户名"
|
||||||
|
prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工单表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="isLoading"
|
||||||
|
:data="filteredTickets"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="工单号" width="100" />
|
||||||
|
<el-table-column label="用户" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="32" :src="row.avatar">{{ row.username?.charAt(0) }}</el-avatar>
|
||||||
|
<span class="username">{{ row.username }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="工单标题" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)" size="small">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column prop="lastReplyTime" label="最后回复" width="180" />
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" @click.stop="goToDetail(row)">
|
||||||
|
回复
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status !== 'completed'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click.stop="handleComplete(row)"
|
||||||
|
>
|
||||||
|
结束
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="totalCount"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, onActivated } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getTickerList,
|
||||||
|
closeTicket,
|
||||||
|
getTicketCount
|
||||||
|
} from '@/api/ticket'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 工单数据
|
||||||
|
const ticketList = ref([])
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const activeStatus = ref('pending') // 默认选中"待处理"
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
const sortBy = ref('') // 默认不排序
|
||||||
|
const sortOrder = ref('') // 默认不选择排序顺序
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
pending: 0,
|
||||||
|
processing: 0,
|
||||||
|
replied: 0,
|
||||||
|
completed: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 状态转换
|
||||||
|
const convertStatusToString = (status) => {
|
||||||
|
const statusMap = { 0: 'pending', 1: 'processing', 2: 'replied', 3: 'completed' }
|
||||||
|
return statusMap[status] || 'processing'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = { pending: '待处理', processing: '处理中', replied: '已回复', completed: '已完成' }
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const typeMap = { pending: 'warning', processing: 'primary', replied: 'info', completed: 'success' }
|
||||||
|
return typeMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工单列表
|
||||||
|
const fetchTicketList = async () => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
let statusParam = ''
|
||||||
|
if (activeStatus.value) {
|
||||||
|
const statusMap = { pending: '0', processing: '1', replied: '2', completed: '3' }
|
||||||
|
statusParam = statusMap[activeStatus.value] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('调用getTickerList,排序参数:', { sortBy: sortBy.value, sortOrder: sortOrder.value })
|
||||||
|
const res = await getTickerList(
|
||||||
|
pageSize.value,
|
||||||
|
currentPage.value,
|
||||||
|
statusParam,
|
||||||
|
sortBy.value,
|
||||||
|
sortOrder.value
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
ticketList.value = (res.data.data || []).map(item => ({
|
||||||
|
id: item.work_id,
|
||||||
|
title: item.name,
|
||||||
|
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||||||
|
userId: item.user?.userId,
|
||||||
|
avatar: item.user?.coverUrl || '',
|
||||||
|
createTime: new Date(item.created_at).toLocaleString(),
|
||||||
|
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||||||
|
status: convertStatusToString(item.status)
|
||||||
|
}))
|
||||||
|
totalCount.value = res.data.all_count || 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取工单列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取工单列表出错:', error)
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getTicketCount()
|
||||||
|
if (res.code === 200) {
|
||||||
|
const data = res.data
|
||||||
|
stats.total = data.all_count
|
||||||
|
stats.pending = data.wait_count
|
||||||
|
stats.replied = data.reply_count
|
||||||
|
stats.completed = data.close_count
|
||||||
|
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计数据出错:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤后的工单列表
|
||||||
|
const filteredTickets = computed(() => {
|
||||||
|
if (!searchKeyword.value) return ticketList.value
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
return ticketList.value.filter(ticket =>
|
||||||
|
ticket.title.toLowerCase().includes(keyword) ||
|
||||||
|
ticket.username.toLowerCase().includes(keyword) ||
|
||||||
|
String(ticket.id).includes(keyword)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按状态过滤
|
||||||
|
const filterByStatus = (status) => {
|
||||||
|
if (activeStatus.value === status) return
|
||||||
|
activeStatus.value = status
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchTicketList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序变化处理
|
||||||
|
const handleSortChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchTicketList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handleSizeChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchTicketList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = () => {
|
||||||
|
fetchTicketList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const refreshList = () => {
|
||||||
|
fetchTicketList()
|
||||||
|
fetchStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到详情页
|
||||||
|
const goToDetail = (row) => {
|
||||||
|
router.push({ path: '/ticket/detail', query: { id: row.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowClick = (row) => {
|
||||||
|
goToDetail(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束工单
|
||||||
|
const handleComplete = (ticket) => {
|
||||||
|
ElMessageBox.confirm('确定要结束此工单吗?结束后将无法继续回复。', '确认操作', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await closeTicket(ticket.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('工单已成功结束')
|
||||||
|
refreshList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '结束工单失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFirstLoad = true
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTicketList()
|
||||||
|
fetchStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当页面被激活时(从详情页返回时)
|
||||||
|
onActivated(() => {
|
||||||
|
// 跳过首次加载,只在从其他页面返回时刷新
|
||||||
|
if (!isFirstLoad) {
|
||||||
|
refreshList()
|
||||||
|
}
|
||||||
|
isFirstLoad = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ticket-list-page {
|
||||||
|
padding: 0;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.pending.active { background: #e6a23c; }
|
||||||
|
.tab-item.processing.active { background: #409eff; }
|
||||||
|
.tab-item.replied.active { background: #909399; }
|
||||||
|
.tab-item.completed.active { background: #67c23a; }
|
||||||
|
|
||||||
|
.tab-item .count {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table tr) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+185
-25
@@ -1,49 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="admin-group-container">
|
<div class="admin-group-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 操作栏 -->
|
<!-- 操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
<el-row :gutter="20">
|
<div class="filter-content">
|
||||||
<el-col :span="8">
|
<div class="search-form">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.key"
|
v-model="queryParams.key"
|
||||||
placeholder="搜索关键词"
|
placeholder="搜索关键词"
|
||||||
clearable
|
clearable
|
||||||
@clear="fetchGroupList"
|
@clear="fetchGroupList"
|
||||||
@keyup.enter="fetchGroupList"
|
@keyup.enter="fetchGroupList"
|
||||||
|
style="width: 200px"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Search /></el-icon>
|
<el-icon><Search /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="16">
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增管理员组
|
<el-icon><Plus /></el-icon>新增管理员组
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" @click="fetchGroupList">
|
<el-button type="success" @click="fetchGroupList">
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
|
||||||
<!-- 管理员组列表 -->
|
<!-- 管理员组列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-note"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="groupList"
|
:data="groupList"
|
||||||
style="width: 100%"
|
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="id" label="组ID" width="100" />
|
||||||
<el-table-column prop="name" label="组名称" min-width="200" />
|
<el-table-column prop="name" label="组名称" min-width="200">
|
||||||
<el-table-column prop="note" label="备注" min-width="250" />
|
<template #default="{ row }">
|
||||||
<!-- <el-table-column prop="member_count" label="成员数量" width="120" /> -->
|
<span class="group-name">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
|
||||||
<el-table-column prop="CreatedAt" label="创建时间" width="180" />
|
<el-table-column prop="CreatedAt" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="250" fixed="right">
|
<el-table-column label="操作" width="250" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<div class="action-buttons">
|
||||||
<el-button type="success" link @click="handleViewMembers(row)">查看成员</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-icon><Edit /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" link @click="handleViewMembers(row)">
|
||||||
|
<el-icon><User /></el-icon>成员
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -60,6 +87,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 管理员组表单对话框 -->
|
<!-- 管理员组表单对话框 -->
|
||||||
@@ -67,6 +95,7 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增管理员组' : '编辑管理员组'"
|
:title="dialogType === 'add' ? '新增管理员组' : '编辑管理员组'"
|
||||||
width="600px"
|
width="600px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="groupFormRef"
|
ref="groupFormRef"
|
||||||
@@ -82,8 +111,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -92,26 +123,36 @@
|
|||||||
v-model="memberDialogVisible"
|
v-model="memberDialogVisible"
|
||||||
title="管理员组成员"
|
title="管理员组成员"
|
||||||
width="900px"
|
width="900px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
|
<div class="member-search">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="memberParams.key"
|
v-model="memberParams.key"
|
||||||
placeholder="搜索成员"
|
placeholder="搜索成员"
|
||||||
clearable
|
clearable
|
||||||
@clear="fetchMemberList"
|
@clear="fetchMemberList"
|
||||||
@keyup.enter="fetchMemberList"
|
@keyup.enter="fetchMemberList"
|
||||||
style="margin-bottom: 16px"
|
style="width: 200px"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Search /></el-icon>
|
<el-icon><Search /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="memberLoading"
|
v-loading="memberLoading"
|
||||||
:data="memberList"
|
:data="memberList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
<el-table-column prop="UserId" label="用户ID" width="100" />
|
||||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
<el-table-column prop="UserName" label="用户名" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="username">{{ row.UserName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="Email" label="邮箱" min-width="200" />
|
<el-table-column prop="Email" label="邮箱" min-width="200" />
|
||||||
<el-table-column prop="Phone" label="手机号" width="130" />
|
<el-table-column prop="Phone" label="手机号" width="130" />
|
||||||
<el-table-column label="性别" width="80">
|
<el-table-column label="性别" width="80">
|
||||||
@@ -146,7 +187,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
import { Plus, Refresh, Search, Edit, User, Delete } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getAdminGroupList,
|
getAdminGroupList,
|
||||||
getAdminGroupMemberList,
|
getAdminGroupMemberList,
|
||||||
@@ -341,18 +382,137 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.filter-section {
|
||||||
border-radius: 8px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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-note { flex: 1; min-width: 250px; }
|
||||||
|
.skeleton-time { width: 180px; }
|
||||||
|
.skeleton-action { width: 250px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+436
-1093
File diff suppressed because it is too large
Load Diff
+424
-1415
File diff suppressed because it is too large
Load Diff
+180
-112
@@ -1,21 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-group-container">
|
<div class="user-group-container">
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 操作栏 -->
|
<!-- 操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
|
<div class="action-bar">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增用户组
|
<el-icon><Plus /></el-icon>新增用户组
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" @click="fetchGroupList">
|
<el-button type="success" @click="fetchGroupList">
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户组列表 -->
|
<!-- 用户组列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-auth"></div>
|
||||||
|
<div class="skeleton-cell skeleton-price"></div>
|
||||||
|
<div class="skeleton-cell skeleton-level"></div>
|
||||||
|
<div class="skeleton-cell skeleton-type"></div>
|
||||||
|
<div class="skeleton-cell skeleton-count"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-else
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="groupList"
|
:data="groupList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column label="组ID" width="100">
|
<el-table-column label="组ID" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -24,17 +47,17 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="组名称" min-width="200">
|
<el-table-column label="组名称" min-width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.group_name || row.name || row.Name }}
|
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="权限" min-width="200" show-overflow-tooltip>
|
<el-table-column label="权限" min-width="200" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag type="info">{{ row.auth || row.Auth || '-' }}</el-tag>
|
<el-tag type="info" effect="plain">{{ row.auth || row.Auth || '-' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="升级金额" width="120">
|
<el-table-column label="升级金额" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.floor_price || row.FloorPrice">¥{{ row.floor_price || row.FloorPrice }}</span>
|
<span v-if="row.floor_price || row.FloorPrice" class="price-text">¥{{ row.floor_price || row.FloorPrice }}</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -52,20 +75,29 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="成员数量" width="100" align="center">
|
<el-table-column label="成员数量" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small" effect="plain">
|
||||||
{{ row.member_count || row.MemberCount || 0 }}
|
{{ row.member_count || row.MemberCount || 0 }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" width="160" show-overflow-tooltip>
|
<el-table-column label="创建时间" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
|
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<div class="action-buttons">
|
||||||
<el-button type="success" link @click="handleViewMembers(row)">查看成员</el-button>
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
<!-- <el-button type="warning" link @click="handleAddMember(row)">添加成员</el-button> -->
|
<el-icon><Edit /></el-icon>编辑
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
</el-button>
|
||||||
|
<el-button type="success" link @click="handleViewMembers(row)">
|
||||||
|
<el-icon><User /></el-icon>成员
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -82,6 +114,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 用户组表单对话框 -->
|
<!-- 用户组表单对话框 -->
|
||||||
@@ -90,6 +123,7 @@
|
|||||||
:title="dialogType === 'add' ? '新增用户组' : '编辑用户组'"
|
:title="dialogType === 'add' ? '新增用户组' : '编辑用户组'"
|
||||||
width="650px"
|
width="650px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="groupFormRef"
|
ref="groupFormRef"
|
||||||
@@ -137,8 +171,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -147,11 +183,13 @@
|
|||||||
v-model="memberDialogVisible"
|
v-model="memberDialogVisible"
|
||||||
title="用户组成员"
|
title="用户组成员"
|
||||||
width="800px"
|
width="800px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="memberLoading"
|
v-loading="memberLoading"
|
||||||
:data="memberList"
|
:data="memberList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column label="用户ID" width="100">
|
<el-table-column label="用户ID" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -160,7 +198,9 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="用户名" min-width="150">
|
<el-table-column label="用户名" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.username || row.Username || row.UserName || row.name || row.Name }}
|
<div class="user-info">
|
||||||
|
<span class="username">{{ row.username || row.Username || row.UserName || row.name || row.Name }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="邮箱" min-width="200">
|
<el-table-column label="邮箱" min-width="200">
|
||||||
@@ -193,6 +233,7 @@
|
|||||||
v-model="addMemberDialogVisible"
|
v-model="addMemberDialogVisible"
|
||||||
title="添加用户组成员"
|
title="添加用户组成员"
|
||||||
width="500px"
|
width="500px"
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="memberFormRef"
|
ref="memberFormRef"
|
||||||
@@ -205,8 +246,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<el-button @click="addMemberDialogVisible = false">取消</el-button>
|
<el-button @click="addMemberDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitAddMember">确定</el-button>
|
<el-button type="primary" @click="submitAddMember">确定</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +258,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
import { Plus, Refresh, Edit, User, Delete } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getUserGroupList,
|
getUserGroupList,
|
||||||
getUserGroupMemberList,
|
getUserGroupMemberList,
|
||||||
@@ -286,16 +329,9 @@ const memberFormRef = ref(null)
|
|||||||
|
|
||||||
// 获取用户组列表
|
// 获取用户组列表
|
||||||
const fetchGroupList = async () => {
|
const fetchGroupList = async () => {
|
||||||
console.log('=== 获取用户组列表 ===')
|
|
||||||
console.log('请求参数:', queryParams)
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getUserGroupList(queryParams)
|
const res = await getUserGroupList(queryParams)
|
||||||
console.log('用户组列表响应:', res)
|
|
||||||
console.log('响应状态码:', res.data?.code || res.code)
|
|
||||||
console.log('响应数据:', res.data?.data || res.data)
|
|
||||||
|
|
||||||
const code = res.data?.code || res.code
|
const code = res.data?.code || res.code
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
let responseData = res.data?.data || res.data
|
let responseData = res.data?.data || res.data
|
||||||
@@ -303,7 +339,6 @@ const fetchGroupList = async () => {
|
|||||||
item.CreatedAt = formatTime(item.CreatedAt)
|
item.CreatedAt = formatTime(item.CreatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理不同的数据结构
|
|
||||||
if (Array.isArray(responseData)) {
|
if (Array.isArray(responseData)) {
|
||||||
groupList.value = responseData
|
groupList.value = responseData
|
||||||
total.value = responseData.length
|
total.value = responseData.length
|
||||||
@@ -317,20 +352,10 @@ const fetchGroupList = async () => {
|
|||||||
groupList.value = []
|
groupList.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('用户组列表数据:', groupList.value)
|
|
||||||
console.log('总数:', total.value)
|
|
||||||
|
|
||||||
if (groupList.value.length > 0) {
|
|
||||||
console.log('第一个用户组示例:', groupList.value[0])
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error('获取用户组列表失败:', res.data)
|
|
||||||
ElMessage.error(res.data?.message || '获取用户组列表失败')
|
ElMessage.error(res.data?.message || '获取用户组列表失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户组列表错误:', error)
|
|
||||||
console.error('错误详情:', error.response || error.message)
|
|
||||||
ElMessage.error('获取用户组列表失败')
|
ElMessage.error('获取用户组列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -339,21 +364,12 @@ const fetchGroupList = async () => {
|
|||||||
|
|
||||||
// 获取成员列表
|
// 获取成员列表
|
||||||
const fetchMemberList = async () => {
|
const fetchMemberList = async () => {
|
||||||
console.log('=== 获取用户组成员列表 ===')
|
|
||||||
console.log('请求参数:', memberParams)
|
|
||||||
|
|
||||||
memberLoading.value = true
|
memberLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getUserGroupMemberList(memberParams)
|
const res = await getUserGroupMemberList(memberParams)
|
||||||
console.log('成员列表响应:', res)
|
|
||||||
console.log('响应状态码:', res.data?.code || res.code)
|
|
||||||
console.log('响应数据:', res.data?.data || res.data)
|
|
||||||
|
|
||||||
const code = res.data?.code || res.code
|
const code = res.data?.code || res.code
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
const responseData = res.data?.data || res.data
|
const responseData = res.data?.data || res.data
|
||||||
|
|
||||||
// 处理不同的数据结构
|
|
||||||
if (Array.isArray(responseData)) {
|
if (Array.isArray(responseData)) {
|
||||||
memberList.value = responseData
|
memberList.value = responseData
|
||||||
memberTotal.value = responseData.length
|
memberTotal.value = responseData.length
|
||||||
@@ -367,20 +383,10 @@ const fetchMemberList = async () => {
|
|||||||
memberList.value = []
|
memberList.value = []
|
||||||
memberTotal.value = 0
|
memberTotal.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('成员列表数据:', memberList.value)
|
|
||||||
console.log('成员总数:', memberTotal.value)
|
|
||||||
|
|
||||||
if (memberList.value.length > 0) {
|
|
||||||
console.log('第一个成员示例:', memberList.value[0])
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error('获取成员列表失败:', res.data)
|
|
||||||
ElMessage.error(res.data?.message || '获取成员列表失败')
|
ElMessage.error(res.data?.message || '获取成员列表失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取成员列表错误:', error)
|
|
||||||
console.error('错误详情:', error.response || error.message)
|
|
||||||
ElMessage.error('获取成员列表失败')
|
ElMessage.error('获取成员列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
memberLoading.value = false
|
memberLoading.value = false
|
||||||
@@ -410,7 +416,6 @@ const handleMemberCurrentChange = (page) => {
|
|||||||
|
|
||||||
// 新增用户组
|
// 新增用户组
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
console.log('=== 打开新增用户组对话框 ===')
|
|
||||||
dialogType.value = 'add'
|
dialogType.value = 'add'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
Object.assign(groupForm, {
|
Object.assign(groupForm, {
|
||||||
@@ -426,13 +431,9 @@ const handleAdd = () => {
|
|||||||
|
|
||||||
// 编辑用户组
|
// 编辑用户组
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
console.log('=== 打开编辑用户组对话框 ===')
|
|
||||||
console.log('用户组数据:', row)
|
|
||||||
|
|
||||||
dialogType.value = 'edit'
|
dialogType.value = 'edit'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
|
|
||||||
// 适配不同的字段名
|
|
||||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||||
const groupName = row.group_name || row.name || row.Name
|
const groupName = row.group_name || row.name || row.Name
|
||||||
const groupAuth = row.auth || row.Auth || ''
|
const groupAuth = row.auth || row.Auth || ''
|
||||||
@@ -448,19 +449,11 @@ const handleEdit = (row) => {
|
|||||||
floor_price: floorPrice,
|
floor_price: floorPrice,
|
||||||
fixed: fixed
|
fixed: fixed
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('表单数据:', groupForm)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看成员
|
// 查看成员
|
||||||
const handleViewMembers = (row) => {
|
const handleViewMembers = (row) => {
|
||||||
// 适配不同的字段名
|
|
||||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||||
|
|
||||||
console.log('=== 查看用户组成员 ===')
|
|
||||||
console.log('用户组数据:', row)
|
|
||||||
console.log('组ID:', groupId)
|
|
||||||
|
|
||||||
memberParams.group_id = groupId
|
memberParams.group_id = groupId
|
||||||
memberParams.page = 1
|
memberParams.page = 1
|
||||||
memberDialogVisible.value = true
|
memberDialogVisible.value = true
|
||||||
@@ -469,13 +462,7 @@ const handleViewMembers = (row) => {
|
|||||||
|
|
||||||
// 添加成员
|
// 添加成员
|
||||||
const handleAddMember = (row) => {
|
const handleAddMember = (row) => {
|
||||||
// 适配不同的字段名
|
|
||||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||||
|
|
||||||
console.log('=== 打开添加成员对话框 ===')
|
|
||||||
console.log('用户组数据:', row)
|
|
||||||
console.log('组ID:', groupId)
|
|
||||||
|
|
||||||
memberForm.group_id = groupId
|
memberForm.group_id = groupId
|
||||||
memberForm.user_ids = ''
|
memberForm.user_ids = ''
|
||||||
addMemberDialogVisible.value = true
|
addMemberDialogVisible.value = true
|
||||||
@@ -483,15 +470,9 @@ const handleAddMember = (row) => {
|
|||||||
|
|
||||||
// 删除用户组
|
// 删除用户组
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
// 适配不同的字段名
|
|
||||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||||
const groupName = row.group_name || row.name || row.Name
|
const groupName = row.group_name || row.name || row.Name
|
||||||
|
|
||||||
console.log('=== 删除用户组 ===')
|
|
||||||
console.log('用户组数据:', row)
|
|
||||||
console.log('组ID:', groupId)
|
|
||||||
console.log('组名称:', groupName)
|
|
||||||
|
|
||||||
ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', {
|
ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
@@ -499,20 +480,14 @@ const handleDelete = (row) => {
|
|||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await deleteUserGroup({ group_id: groupId })
|
const res = await deleteUserGroup({ group_id: groupId })
|
||||||
console.log('删除响应:', res)
|
|
||||||
console.log('响应状态码:', res.data?.code || res.code)
|
|
||||||
|
|
||||||
const code = res.data?.code || res.code
|
const code = res.data?.code || res.code
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
} else {
|
} else {
|
||||||
console.error('删除失败:', res.data)
|
|
||||||
ElMessage.error(res.data?.message || '删除失败')
|
ElMessage.error(res.data?.message || '删除失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除错误:', error)
|
|
||||||
console.error('错误详情:', error.response || error.message)
|
|
||||||
ElMessage.error('删除失败')
|
ElMessage.error('删除失败')
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
@@ -522,10 +497,6 @@ const handleDelete = (row) => {
|
|||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
groupFormRef.value?.validate(async (valid) => {
|
groupFormRef.value?.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
console.log('=== 提交用户组表单 ===')
|
|
||||||
console.log('操作类型:', dialogType.value)
|
|
||||||
console.log('表单数据:', groupForm)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res
|
let res
|
||||||
const submitData = {
|
const submitData = {
|
||||||
@@ -533,40 +504,30 @@ const submitForm = () => {
|
|||||||
auth: groupForm.auth
|
auth: groupForm.auth
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加可选字段(如果有值才添加)
|
|
||||||
if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) {
|
if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) {
|
||||||
submitData.higher_level_id = groupForm.higher_level_id
|
submitData.higher_level_id = groupForm.higher_level_id
|
||||||
}
|
}
|
||||||
if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) {
|
if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) {
|
||||||
submitData.floor_price = groupForm.floor_price
|
submitData.floor_price = groupForm.floor_price
|
||||||
}
|
}
|
||||||
// fixed 字段总是传递(boolean 值)
|
|
||||||
submitData.fixed = groupForm.fixed
|
submitData.fixed = groupForm.fixed
|
||||||
|
|
||||||
if (dialogType.value === 'add') {
|
if (dialogType.value === 'add') {
|
||||||
console.log('新增用户组,提交数据:', submitData)
|
|
||||||
res = await createUserGroup(submitData)
|
res = await createUserGroup(submitData)
|
||||||
} else {
|
} else {
|
||||||
submitData.group_id = groupForm.group_id
|
submitData.group_id = groupForm.group_id
|
||||||
console.log('更新用户组,提交数据:', submitData)
|
|
||||||
res = await updateUserGroupInfo(submitData)
|
res = await updateUserGroupInfo(submitData)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('提交响应:', res)
|
|
||||||
console.log('响应状态码:', res.data?.code || res.code)
|
|
||||||
|
|
||||||
const code = res.data?.code || res.code
|
const code = res.data?.code || res.code
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
} else {
|
} else {
|
||||||
console.error('操作失败:', res.data)
|
|
||||||
ElMessage.error(res.data?.message || '操作失败')
|
ElMessage.error(res.data?.message || '操作失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error)
|
|
||||||
console.error('错误详情:', error.response || error.message)
|
|
||||||
ElMessage.error('操作失败')
|
ElMessage.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,26 +538,17 @@ const submitForm = () => {
|
|||||||
const submitAddMember = () => {
|
const submitAddMember = () => {
|
||||||
memberFormRef.value?.validate(async (valid) => {
|
memberFormRef.value?.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
console.log('=== 提交添加成员 ===')
|
|
||||||
console.log('表单数据:', memberForm)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await addUserGroupMember(memberForm)
|
const res = await addUserGroupMember(memberForm)
|
||||||
console.log('添加成员响应:', res)
|
|
||||||
console.log('响应状态码:', res.data?.code || res.code)
|
|
||||||
|
|
||||||
const code = res.data?.code || res.code
|
const code = res.data?.code || res.code
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
ElMessage.success('添加成功')
|
ElMessage.success('添加成功')
|
||||||
addMemberDialogVisible.value = false
|
addMemberDialogVisible.value = false
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
} else {
|
} else {
|
||||||
console.error('添加失败:', res.data)
|
|
||||||
ElMessage.error(res.data?.message || '添加失败')
|
ElMessage.error(res.data?.message || '添加失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('添加错误:', error)
|
|
||||||
console.error('错误详情:', error.response || error.message)
|
|
||||||
ElMessage.error('添加失败')
|
ElMessage.error('添加失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,7 +557,6 @@ const submitAddMember = () => {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('=== 用户组管理页面初始化 ===')
|
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -615,18 +566,135 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.filter-section {
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.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-auth { flex: 1; min-width: 200px; }
|
||||||
|
.skeleton-price { width: 120px; }
|
||||||
|
.skeleton-level { width: 120px; }
|
||||||
|
.skeleton-type { width: 100px; }
|
||||||
|
.skeleton-count { width: 100px; }
|
||||||
|
.skeleton-time { width: 180px; }
|
||||||
|
.skeleton-action { width: 280px; height: 32px; }
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+289
-46
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-list-container">
|
<div class="user-list-container">
|
||||||
|
<!-- 搜索和用户列表 -->
|
||||||
|
<el-card class="main-container" shadow="never">
|
||||||
<!-- 搜索和操作栏 -->
|
<!-- 搜索和操作栏 -->
|
||||||
<el-card class="filter-container" shadow="never">
|
<div class="filter-section">
|
||||||
|
<div class="filter-content">
|
||||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
<el-form-item label="关键字">
|
<el-form-item label="关键字">
|
||||||
<el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" />
|
<el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" />
|
||||||
@@ -24,15 +27,42 @@
|
|||||||
<el-icon><Delete /></el-icon>批量删除
|
<el-icon><Delete /></el-icon>批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户列表 -->
|
<!-- 用户列表 -->
|
||||||
<el-card class="table-container" shadow="never">
|
<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-checkbox"></div>
|
||||||
|
<div class="skeleton-cell skeleton-id"></div>
|
||||||
|
<div class="skeleton-cell skeleton-user">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text-group">
|
||||||
|
<div class="skeleton-text skeleton-text-primary"></div>
|
||||||
|
<div class="skeleton-text skeleton-text-secondary"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="skeleton-cell skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-cell skeleton-phone"></div>
|
||||||
|
<div class="skeleton-cell skeleton-realname">
|
||||||
|
<div class="skeleton-text skeleton-text-primary"></div>
|
||||||
|
<div class="skeleton-text skeleton-text-secondary"></div>
|
||||||
|
</div>
|
||||||
|
<div class="skeleton-cell skeleton-group"></div>
|
||||||
|
<div class="skeleton-cell skeleton-status"></div>
|
||||||
|
<div class="skeleton-cell skeleton-time"></div>
|
||||||
|
<div class="skeleton-cell skeleton-action"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 数据表格 -->
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-else
|
||||||
:data="userList"
|
:data="userList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
<el-table-column prop="UserId" label="用户ID" width="100" />
|
||||||
@@ -99,6 +129,7 @@
|
|||||||
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
||||||
<el-dropdown-item command="group">修改用户组</el-dropdown-item>
|
<el-dropdown-item command="group">修改用户组</el-dropdown-item>
|
||||||
<el-dropdown-item command="realname">实名信息</el-dropdown-item>
|
<el-dropdown-item command="realname">实名信息</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="balance">余额管理</el-dropdown-item>
|
||||||
<el-dropdown-item command="loginHistory">登录记录</el-dropdown-item>
|
<el-dropdown-item command="loginHistory">登录记录</el-dropdown-item>
|
||||||
<el-dropdown-item command="operationHistory">操作记录</el-dropdown-item>
|
<el-dropdown-item command="operationHistory">操作记录</el-dropdown-item>
|
||||||
<el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item>
|
<el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item>
|
||||||
@@ -123,6 +154,7 @@
|
|||||||
background
|
background
|
||||||
class="pagination"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 用户编辑对话框 -->
|
<!-- 用户编辑对话框 -->
|
||||||
@@ -481,12 +513,24 @@ const fetchUserList = async () => {
|
|||||||
const res = await getUserList(queryParams)
|
const res = await getUserList(queryParams)
|
||||||
console.log("获取用户列表:", res)
|
console.log("获取用户列表:", res)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
userList.value = res.data.data.data || []
|
// 映射 API 返回的字段到组件使用的字段格式
|
||||||
|
userList.value = (res.data.data.data || []).map(user => ({
|
||||||
|
UserId: user.user_id,
|
||||||
|
UserName: user.user_name,
|
||||||
|
Phone: user.phone,
|
||||||
|
Email: user.email,
|
||||||
|
Sex: user.sex,
|
||||||
|
Age: user.age,
|
||||||
|
IsAdmin: user.is_admin,
|
||||||
|
CoverID: user.cover_id,
|
||||||
|
avatarUrl: user.cover || '', // 直接使用 cover 字段作为头像 URL
|
||||||
|
UserGroup: user.user_group,
|
||||||
|
RealName: user.real_name,
|
||||||
|
IsDeleted: user.is_deleted,
|
||||||
|
CreatedAt: user.created_at
|
||||||
|
}))
|
||||||
console.log("用户列表:", userList.value)
|
console.log("用户列表:", userList.value)
|
||||||
total.value = res.data.data.all_count || 0
|
total.value = res.data.data.all_count || 0
|
||||||
|
|
||||||
// 为每个用户加载头像URL
|
|
||||||
await loadAvatarsForUsers()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取用户列表失败')
|
ElMessage.error('获取用户列表失败')
|
||||||
@@ -495,26 +539,6 @@ const fetchUserList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为用户列表加载头像URL
|
|
||||||
const loadAvatarsForUsers = async () => {
|
|
||||||
const promises = userList.value.map(async (user) => {
|
|
||||||
if (user.CoverID) {
|
|
||||||
try {
|
|
||||||
const res = await getFileDetail({ file_id: user.CoverID })
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
user.avatarUrl = res.data.data.url
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载头像失败:', error)
|
|
||||||
user.avatarUrl = ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user.avatarUrl = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await Promise.all(promises)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户列表
|
// 查询用户列表
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
@@ -645,6 +669,9 @@ const handleCommand = (command, row) => {
|
|||||||
case 'realname':
|
case 'realname':
|
||||||
handleRealnameModify(row)
|
handleRealnameModify(row)
|
||||||
break
|
break
|
||||||
|
case 'balance':
|
||||||
|
handleBalanceManage(row)
|
||||||
|
break
|
||||||
case 'loginHistory':
|
case 'loginHistory':
|
||||||
handleLoginHistory(row)
|
handleLoginHistory(row)
|
||||||
break
|
break
|
||||||
@@ -660,6 +687,14 @@ const handleCommand = (command, row) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 余额管理
|
||||||
|
const handleBalanceManage = (row) => {
|
||||||
|
router.push({
|
||||||
|
path: '/user/balance',
|
||||||
|
query: { user_id: row.UserId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 模拟登录
|
// 模拟登录
|
||||||
const handleSimulateLogin = async (row) => {
|
const handleSimulateLogin = async (row) => {
|
||||||
@@ -894,22 +929,198 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.main-container {
|
||||||
margin-bottom: 20px;
|
border: 1px solid #e1e8ed;
|
||||||
border-radius: 8px;
|
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 {
|
.search-form {
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item__label) {
|
||||||
|
margin-right: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
@media (max-width: 768px) {
|
||||||
border-radius: 8px;
|
.filter-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-checkbox {
|
||||||
|
width: 55px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-id {
|
||||||
|
width: 100px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-user {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
height: 14px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text-primary {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text-secondary {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-phone {
|
||||||
|
width: 130px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-realname {
|
||||||
|
width: 150px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-group {
|
||||||
|
width: 120px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-status {
|
||||||
|
width: 100px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-time {
|
||||||
|
width: 180px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-action {
|
||||||
|
width: 200px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@@ -926,31 +1137,36 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email {
|
.email {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.real-name {
|
.real-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.id-card {
|
.id-card {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-gray {
|
.text-gray {
|
||||||
color: #999;
|
color: #95a5a6;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,12 +1174,9 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-gray {
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -980,13 +1193,12 @@ onMounted(() => {
|
|||||||
.avatar-preview {
|
.avatar-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-preview:hover {
|
.avatar-preview:hover {
|
||||||
transform: scale(1.05);
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-preview:hover .avatar-overlay {
|
.avatar-preview:hover .avatar-overlay {
|
||||||
@@ -1006,7 +1218,7 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.2s ease;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,5 +1238,36 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
+18
-598
@@ -7,293 +7,13 @@
|
|||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/admin/server/setting/group/list": {
|
"/api/v1/admin/order/list": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "获取配置分组列表",
|
"summary": "获取订单列表",
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"in": "query",
|
|
||||||
"description": "获取页码 默认 1",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "count",
|
|
||||||
"in": "query",
|
|
||||||
"description": "获取条数 默认 10",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"in": "query",
|
|
||||||
"description": "关键词筛选",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/group/info": {
|
|
||||||
"get": {
|
|
||||||
"summary": "获取配置分组信息",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "setting_group_id",
|
|
||||||
"in": "query",
|
|
||||||
"description": "",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/group/create": {
|
|
||||||
"post": {
|
|
||||||
"summary": "创建配置分组",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"description": "名称",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"description": "备注",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"code": 200,
|
|
||||||
"message": "Success"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/group/update": {
|
|
||||||
"post": {
|
|
||||||
"summary": "修改配置分组",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"description": "ID",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"description": "名称",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"description": "备注",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/group/delete": {
|
|
||||||
"delete": {
|
|
||||||
"summary": "删除配置分组",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "setting_group_id",
|
|
||||||
"in": "query",
|
|
||||||
"description": "",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/list": {
|
|
||||||
"get": {
|
|
||||||
"summary": "获取配置列表",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"in": "query",
|
|
||||||
"description": "获取页码 默认 1",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "count",
|
"name": "count",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
@@ -304,18 +24,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "group_id",
|
"name": "page",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "组id(与组名称二选一)",
|
"description": "获取页码 默认 1",
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "group_name",
|
|
||||||
"in": "query",
|
|
||||||
"description": "组名称(与组id二选一)",
|
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -331,53 +42,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "state",
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/info": {
|
|
||||||
"get": {
|
|
||||||
"summary": "获取配置信息",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "配置id (与name二选一)",
|
"description": "状态筛选",
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "user_id",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "配置名称 (与id二选一)",
|
"description": "用户id筛选",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_key",
|
||||||
|
"in": "query",
|
||||||
|
"description": "用户关键词筛选(用户名 手机号 邮箱)",
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -410,276 +95,11 @@
|
|||||||
},
|
},
|
||||||
"security": []
|
"security": []
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/create": {
|
|
||||||
"post": {
|
|
||||||
"summary": "创建配置",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"description": "名称",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"description": "备注",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"description": "类型 string/int/float/bool/",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"setting_group_id": {
|
|
||||||
"description": "配置组id",
|
|
||||||
"example": 0,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"open": {
|
|
||||||
"description": "是否开放访问",
|
|
||||||
"example": "",
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"value",
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/update": {
|
|
||||||
"post": {
|
|
||||||
"summary": "修改配置",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"example": 0,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"description": "名称",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"description": "备注",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"description": "类型 string/int/float/bool/",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"setting_group_id": {
|
|
||||||
"description": "配置组id",
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/set_open": {
|
|
||||||
"post": {
|
|
||||||
"summary": "修改配置是否开放访问",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"example": 0,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"open": {
|
|
||||||
"description": "是否开放",
|
|
||||||
"example": "",
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"open"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/server/setting/delete": {
|
|
||||||
"delete": {
|
|
||||||
"summary": "删除配置",
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "",
|
|
||||||
"tags": [],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"description": "",
|
|
||||||
"example": "Bearer {{token}}",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"multipart/form-data": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"example": "",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
"responses": {},
|
||||||
"securitySchemes": {}
|
"securitySchemes": {}
|
||||||
},
|
},
|
||||||
"servers": [],
|
"servers": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user