Merge pull request 'master' (#12) from master into deploy
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#12
This commit was merged in pull request #12.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# 管理员后台pc端
|
||||
|
||||
# 007UI 后台管理系统
|
||||
|
||||
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
||||
|
||||
+323
-12
@@ -16,6 +16,7 @@ import {getUserInfo} from "@/api/login.js";
|
||||
const userStore = useUserStore()
|
||||
onMounted(async () => {
|
||||
let resp = await getUserInfo()
|
||||
console.log("用户信息:",resp)
|
||||
userStore.setUserInfo(resp.data)
|
||||
console.log(userStore.userInfo)
|
||||
})
|
||||
@@ -87,40 +88,350 @@ html, body {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Element Plus样式优化 */
|
||||
/* Element Plus全局配色优化 */
|
||||
|
||||
/* 按钮扁平化 */
|
||||
.el-button {
|
||||
font-weight: 400;
|
||||
border-radius: 4px;
|
||||
border-radius: 0 !important;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 500;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
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 {
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
color: #34495e;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,69 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/* -------------------------------------------------------------- */
|
||||
/**管理员权限管理 */
|
||||
/**-------------------------------------------------------- */
|
||||
/**路由权限管理 */
|
||||
|
||||
/**获取权限列表 */
|
||||
export const getPermissionList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/permission/path/list', {params: params})
|
||||
}
|
||||
/**新增权限信息 */
|
||||
export const addPermissionInfo = (data) => {
|
||||
return http2.post('/api/v1/admin/server/permission/path/add', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改权限信息 */
|
||||
export const updatePermissionInfo = (data) => {
|
||||
return http2.post('/api/v1/admin/server/permission/path/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除权限信息 */
|
||||
export const deletePermissionInfo = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/permission/path/delete', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**-------------------------------------------------------- */
|
||||
/**管理员权限分配 */
|
||||
|
||||
/**获取指定管理员的权限列表 */
|
||||
export const getPermissionListByAdmin = (params) => {
|
||||
return http2.get('/api/v1/admin/server/permission/admin/list', {params: params})
|
||||
}
|
||||
/**新增管理员权限 */
|
||||
export const addPermissionAdmin = (data) => {
|
||||
return http2.post('/api/v1/admin/server/permission/admin/add', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改管理员权限 */
|
||||
export const updatePermissionAdmin = (data) => {
|
||||
return http2.post('/api/v1/admin/server/permission/admin/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除管理员权限 */
|
||||
export const deletePermissionAdmin = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/permission/admin/delete', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**新增签到奖励 */
|
||||
export const addSignReward = (data) => {
|
||||
return http2.post('/api/v1/admin/activity/signin/add_reward', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**新增签到奖励类型 */
|
||||
export const addSignRewardType = (data) => {
|
||||
return http2.post('/api/v1/admin/activity/signin/add_reward_type', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**新增goedge服务器 */
|
||||
export const addGoedgeServer = (data) => {
|
||||
return http2.post('/api/v1/admin/api/goedge/add_server', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
|
||||
/**---------------------------------- */
|
||||
/**优惠码/代金券管理 (统一接口) */
|
||||
|
||||
/**获取优惠码/代金券列表 */
|
||||
export const getDiscountCodeList = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/list', {params: params})
|
||||
}
|
||||
|
||||
/**获取优惠码/代金券详情 */
|
||||
export const getDiscountCodeDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/detail', {params: params})
|
||||
}
|
||||
|
||||
/**创建优惠码/代金券 */
|
||||
export const createDiscountCode = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**更新优惠码/代金券 */
|
||||
export const updateDiscountCode = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**删除优惠码/代金券 */
|
||||
export const deleteDiscountCode = (data) => {
|
||||
return http2.delete('/api/v1/admin/code/discount/delete?code_id=' + data.code_id)
|
||||
}
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品关联管理 */
|
||||
|
||||
/**获取优惠码/代金券商品列表 */
|
||||
export const getDiscountGoodsList = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/goods/list', {params: params})
|
||||
}
|
||||
|
||||
/**新增优惠码/代金券商品关联 */
|
||||
export const addDiscountGoods = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/goods/add', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**修改优惠码/代金券商品关联 */
|
||||
export const updateDiscountGoods = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/goods/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**删除优惠码/代金券商品关联 */
|
||||
export const deleteDiscountGoods = (data) => {
|
||||
return http2.delete('/api/v1/admin/code/discount/goods/delete', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**---------------------------------- */
|
||||
/**用户关联管理 */
|
||||
|
||||
/**获取优惠码/代金券用户关联列表 */
|
||||
export const getDiscountUsersList = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/users/list', {params: params})
|
||||
}
|
||||
|
||||
/**新增优惠码/代金券用户关联 */
|
||||
export const addDiscountUsers = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/users/add', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**修改优惠码/代金券用户关联 */
|
||||
export const updateDiscountUsers = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/users/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**删除优惠码/代金券用户关联 */
|
||||
export const deleteDiscountUsers = (data) => {
|
||||
return http2.delete('/api/v1/admin/code/discount/users/delete', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**---------------------------------- */
|
||||
/**用户代金券管理 */
|
||||
|
||||
/**获取用户优惠码/代金券列表 */
|
||||
export const getUserVoucherList = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/user/list', {params: params})
|
||||
}
|
||||
|
||||
/**为用户添加代金券 */
|
||||
export const addUserVoucher = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/user/add_coupon', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**修改用户代金券 */
|
||||
export const updateUserVoucher = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/user/update_coupon', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**删除用户代金券 */
|
||||
export const deleteUserVoucher = (data) => {
|
||||
return http2.delete('/api/v1/admin/code/discount/user/delete_coupon', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**获取用户优惠码/代金券使用记录 */
|
||||
export const getUserVoucherHistory = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/user/history', {params: params})
|
||||
}
|
||||
|
||||
/**为用户分配代金券 */
|
||||
export const allocateVoucher = (data) => {
|
||||
return http2.post('/api/v1/admin/code/discount/coupon/allocate', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**查询代金券的拥有者列表 */
|
||||
export const getVoucherHolderList = (params) => {
|
||||
return http2.get('/api/v1/admin/code/discount/coupon/holder_list', {params: params})
|
||||
}
|
||||
|
||||
/**---------------------------------- */
|
||||
/**兼容旧接口别名 */
|
||||
export const getVoucherList = getDiscountCodeList
|
||||
export const getVoucherDetail = getDiscountCodeDetail
|
||||
export const createVoucher = createDiscountCode
|
||||
export const updateVoucher = updateDiscountCode
|
||||
export const deleteVoucher = deleteDiscountCode
|
||||
@@ -0,0 +1,42 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取文件列表 */
|
||||
export const getFileList = (params) => {
|
||||
return http2.get('/api/v1/admin/file/list',{params})
|
||||
}
|
||||
|
||||
/**获取文件详情 */
|
||||
export const getFileDetail = (data) => {
|
||||
return http2.get('/api/v1/admin/file/detail?file_id='+data.file_id)
|
||||
}
|
||||
|
||||
/**删除文件 */
|
||||
export const deleteFile = (data) => {
|
||||
return http2.delete('/api/v1/admin/file/delete', {
|
||||
params: data
|
||||
})
|
||||
}
|
||||
/**修改文件信息 */
|
||||
export const updateFile = (data) => {
|
||||
return http2.post('/api/v1/admin/file/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**公共接口 获取文件信息 */
|
||||
export const getFile = (data) => {
|
||||
return http2.get('/api/v1/tools/file/info?file_id='+data.file_id)
|
||||
}
|
||||
|
||||
/**文件上传 */
|
||||
export const uploadFile = (data) => {
|
||||
return http2.post('/api/v1/tools/file/upload', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**文件下载 */
|
||||
export const downloadFile = (data) => {
|
||||
return http2.get('/api/v1/tool/file/down?file_id='+data.file_id)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取管理员组列表 */
|
||||
export const getAdminGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/admin_group/list', {params: params})
|
||||
}
|
||||
/**获取管理员组成员列表 */
|
||||
export const getAdminGroupMemberList = (params) => {
|
||||
return http2.get('/api/v1/admin/admin_group/member_list', {params:params})
|
||||
}
|
||||
/**获取管理员组详情 */
|
||||
export const getAdminGroupDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/admin_group/detail', {params: params})
|
||||
}
|
||||
/**新增管理员组 */
|
||||
export const addAdminGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/admin_group/create', data)
|
||||
}
|
||||
/**更新管理员组信息 */
|
||||
export const updateAdminGroupInfo = (data) => {
|
||||
return http2.post('/api/v1/admin/admin_group/update', data)
|
||||
}
|
||||
/**删除管理员组 */
|
||||
export const deleteAdminGroup = (data) => {
|
||||
return http2.delete('/api/v1/admin/admin_group/delete?group_id=' + data.group_id)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取订单列表 */
|
||||
export const getOrderList = (params) => {
|
||||
return http2.get('/api/v1/admin/order/list', {params: params})
|
||||
}
|
||||
/**获取订单详情 */
|
||||
export const getOrderDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/order/detail', {params: params})
|
||||
}
|
||||
/**删除订单 (未提供删除接口,暂时保留) */
|
||||
/**删除订单 */
|
||||
export const deleteOrder = (data) => {
|
||||
return http2.delete('/api/v1/admin/trades/delete_trade', {
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**创建订单 */
|
||||
export const createOrder = (data) => {
|
||||
return http2.post('/api/v1/admin/order/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改订单 */
|
||||
export const updateOrder = (data) => {
|
||||
return http2.post('/api/v1/admin/order/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**---------------------------------- */
|
||||
/**商品组管理 */
|
||||
|
||||
/**获取商品分组列表 */
|
||||
export const getProductGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/group/list', {params: params})
|
||||
}
|
||||
/**创建商品分组 */
|
||||
export const createProductGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品分组 */
|
||||
export const updateProductGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**隐藏商品组 */
|
||||
export const hideProductGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group/disable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**启动商品组 */
|
||||
export const startProductGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group/enable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品分组 */
|
||||
export const deleteProductGroup = (data) => {
|
||||
return http2.delete('/api/v1/admin/good/group/delete',{
|
||||
data: data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品管理 */
|
||||
|
||||
/**获取商品列表 */
|
||||
export const getProductList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/goods/list', {params: params})
|
||||
}
|
||||
/**创建商品 */
|
||||
export const createProduct = (data) => {
|
||||
return http2.post('/api/v1/admin/good/goods/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品 */
|
||||
export const updateProduct = (data) => {
|
||||
return http2.post('/api/v1/admin/good/goods/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品 */
|
||||
export const deleteProduct = (data) => {
|
||||
return http2.delete('/api/v1/admin/good/goods/delete',{
|
||||
data:data,
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品参数管理 */
|
||||
|
||||
/**获取商品参数列表 */
|
||||
export const getProductParameterList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/spec/list', {params: params})
|
||||
}
|
||||
/**创建商品参数 */
|
||||
export const createProductParameter = (data) => {
|
||||
return http2.post('/api/v1/admin/good/spec/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**获取商品参数详情 */
|
||||
export const getProductParameterDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/good/spec/detail', {params: params})
|
||||
}
|
||||
/**更新商品参数 */
|
||||
export const updateProductParameter = (data) => {
|
||||
return http2.post('/api/v1/admin/good/spec/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品参数 */
|
||||
export const deleteProductParameter = (data) => {
|
||||
return http2.delete('/api/v1/admin/good/spec/delete', {
|
||||
params: data
|
||||
})
|
||||
}
|
||||
/**增加商品参数值 */
|
||||
export const addProductParameterValue = (data) => {
|
||||
return http2.post('/api/v1/admin/good/spec/add_value', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品参数值 */
|
||||
export const deleteProductParameterValue = (data) => {
|
||||
return http2.delete('/api/v1/admin/good/spec/delete_value', {
|
||||
params: data
|
||||
})
|
||||
}
|
||||
/**更新商品参数值 */
|
||||
export const updateProductParameterValue = (data) => {
|
||||
return http2.post('/api/v1/admin/good/spec/update_value', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
|
||||
/**路由管理 */
|
||||
/**新增前端路由 */
|
||||
export const addRouter = (data) => {
|
||||
return http2.post('/api/v1/admin/web_routs/add', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新前端路由 */
|
||||
export const updateRouter = (data) => {
|
||||
return http2.post('/api/v1/admin/web_routs/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { http2 } from "@/utils/request.js"
|
||||
|
||||
// ========== 配置组管理 ==========
|
||||
|
||||
/** 获取配置分组列表 */
|
||||
export const getSettingGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/setting/group/list', { params })
|
||||
}
|
||||
|
||||
/** 获取配置分组信息 */
|
||||
export const getSettingGroupInfo = (params) => {
|
||||
return http2.get('/api/v1/admin/server/setting/group/info', { params })
|
||||
}
|
||||
|
||||
/** 创建配置分组 */
|
||||
export const createSettingGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/setting/group/create', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改配置分组 */
|
||||
export const updateSettingGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/setting/group/update', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除配置分组 */
|
||||
export const deleteSettingGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/setting/group/delete', { params })
|
||||
}
|
||||
|
||||
// ========== 配置管理 ==========
|
||||
|
||||
/** 获取配置列表 */
|
||||
export const getSettingList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/setting/list', { params })
|
||||
}
|
||||
|
||||
/** 获取配置信息 */
|
||||
export const getSettingInfo = (params) => {
|
||||
return http2.get('/api/v1/admin/server/setting/info', { params })
|
||||
}
|
||||
|
||||
/** 创建配置 */
|
||||
export const createSetting = (data) => {
|
||||
return http2.post('/api/v1/admin/server/setting/create', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改配置 */
|
||||
export const updateSetting = (data) => {
|
||||
return http2.post('/api/v1/admin/server/setting/update', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改配置是否开放访问 */
|
||||
export const setSettingOpen = (data) => {
|
||||
return http2.post('/api/v1/admin/server/setting/set_open', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除配置 */
|
||||
export const deleteSetting = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/setting/delete', {
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
|
||||
/**用户余额管理 */
|
||||
/**修改用户余额 */
|
||||
export const editUserBalance = (data) => {
|
||||
return http2.post('/api/v1/admin/user/balance/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**添加用户消费记录 */
|
||||
export const addUserConsumption = (data) => {
|
||||
return http2.post('/api/v1/admin/user/balance/add_history', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**查询用户余额 */
|
||||
export const getUserBalance = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/select?user_id='+data.user_id)
|
||||
}
|
||||
|
||||
/**获取用户余额记录 */
|
||||
export const getUserBalanceRecord = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/history?user_id='+data.user_id + '&balance_type=' + data.balance_type + '&page=' + data.page + '&count=' + data.count)
|
||||
}
|
||||
|
||||
/**获取用户余额 */
|
||||
export const getUserBalanceCount = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/get?user_id='+data.user_id)
|
||||
}
|
||||
/**获取用户信息 */
|
||||
export const getUserInfo = (data) => {
|
||||
return http2.get('/api/v1/admin/user/user/detail?user_id='+data.user_id)
|
||||
}
|
||||
|
||||
/**获取用户列表 */
|
||||
export const getUserList = (data) => {
|
||||
return http2.get('/api/v1/admin/user/user/list?page=' + data.page + '&count=' + data.count + '&key=' + data.key)
|
||||
}
|
||||
|
||||
/**更新用户信息 */
|
||||
export const updateUserInfo = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**删除用户 */
|
||||
export const deleteUser = (data) => {
|
||||
return http2.delete('/api/v1/admin/user/user/delete?group_id='+data.group_id)
|
||||
}
|
||||
/**修改用户头像 */
|
||||
export const updateUserAvatar = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/update_cover', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改用户密码 */
|
||||
export const updateUserPassword = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/update_password', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**修改用户组 */
|
||||
export const updateUserGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/update_group', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改用户管理员权限*/
|
||||
export const updateUserAdmin = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/user2admin', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改用户实名信息*/
|
||||
export const updateUserRealName = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/update_real_name', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**获取用户登录记录*/
|
||||
export const getUserLoginRecord = (data) => {
|
||||
return http2.get('/api/v1/admin/user/user/login_history?user_id='+data.user_id + '&page=' + data.page + '&count=' + data.count)
|
||||
}
|
||||
|
||||
/**获取用户操作记录 */
|
||||
export const getUserOperationRecord = (data) => {
|
||||
return http2.get('/api/v1/admin/user/user/manage_history?user_id='+data.user_id + '&page=' + data.page + '&count=' + data.count)
|
||||
}
|
||||
/**模拟用户登录 */
|
||||
export const mockUserLogin = (data) => {
|
||||
return http2.get('/api/v1/admin/user/user/simulation_login?user_id='+data.user_id)
|
||||
}
|
||||
/**新建任务 */
|
||||
export const createTask = (data) => {
|
||||
return http2.post('/api/v1/admin/user/user/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**用户组管理 */
|
||||
/**获取用户组列表 */
|
||||
export const getUserGroupList = (data) => {
|
||||
return http2.get('/api/v1/admin/user_group/list?page=' + data.page + '&count=' + data.count)
|
||||
}
|
||||
/**获取用户组成员列表 */
|
||||
export const getUserGroupMemberList = (data) => {
|
||||
return http2.get('/api/v1/admin/user_group/member_list?group_id=' + data.group_id + '&page=' + data.page + '&count=' + data.count)
|
||||
}
|
||||
/**获取用户组详情信息 */
|
||||
export const getUserGroupDetail = (data) => {
|
||||
return http2.get('/api/v1/admin/user_group/detail?group_id=' + data.group_id)
|
||||
}
|
||||
/**新建用户组 */
|
||||
export const createUserGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/user_group/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新用户组信息 */
|
||||
export const updateUserGroupInfo = (data) => {
|
||||
return http2.post('/api/v1/admin/user_group/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除用户组 */
|
||||
export const deleteUserGroup = (data) => {
|
||||
return http2.delete(`/api/v1/admin/user_group/delete?group_id=`+data.group_id,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**添加用户组成员 */
|
||||
export const addUserGroupMember = (data) => {
|
||||
return http2.post('/api/v1/admin/user_group/add_member', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
+8
-3
@@ -11,15 +11,20 @@ export function addDomain(data) {
|
||||
}
|
||||
|
||||
// 删除域名白名单
|
||||
export function deleteDomain(id) {
|
||||
return request.post("/api/v1/admin/server/domain_withe/delete",{domain_id: id})
|
||||
export function deleteDomain(data) {
|
||||
return request.post("/api/v1/admin/server/domain_withe/delete",data,{
|
||||
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除域名白名单
|
||||
export async function batchDeleteDomain(ids) {
|
||||
let promises = []
|
||||
for (let id of ids) {
|
||||
promises.push(deleteDomain(id))
|
||||
promises.push(deleteDomain({domain_id:id}))
|
||||
}
|
||||
return await Promise.all(promises)
|
||||
}
|
||||
+52
-3
@@ -29,12 +29,12 @@ export function getCompletedTicketList(count, page) {
|
||||
return getTickerList(count,page,3)
|
||||
}
|
||||
|
||||
// 获取详情
|
||||
// 获取工单详情
|
||||
export function getTicketDetail(work_id) {
|
||||
return request.get('/api/v1/admin/work_order/detail', { work_id })
|
||||
}
|
||||
|
||||
// 回复
|
||||
// 回复工单
|
||||
export function replyTicket(work_id, content, files) {
|
||||
return request.post('/api/v1/admin/work_order/reply', { work_id, content, files })
|
||||
}
|
||||
@@ -67,4 +67,53 @@ export async function parseFilesToImages(files) {
|
||||
|
||||
const fileIds = files.split(',')
|
||||
return await Promise.all(fileIds.map(async (id) => await getFileImage(id.trim())))
|
||||
}
|
||||
}
|
||||
|
||||
/**获取工单数量 */
|
||||
export function getTicketCount() {
|
||||
return request.get('/api/v1/admin/work_order/count')
|
||||
}
|
||||
/**修改工单信息 */
|
||||
export function updateTicketInfo(data) {
|
||||
return request.post('/api/v1/admin/work_order/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**添加工单类型 */
|
||||
export function addTicketType(data) {
|
||||
return request.post('/api/v1/admin/work_order/add_type', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**修改工单类型 */
|
||||
export function updateTicketType(data) {
|
||||
return request.post('/api/v1/admin/work_order/update_type', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除工单类型 */
|
||||
export function deleteTicketType(data) {
|
||||
return request.delete('/api/v1/admin/work_order/delete_type', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**获取工单类型列表 */
|
||||
export function getTicketTypeList(data) {
|
||||
return request.get('/api/v1/admin/work_order/type_list', data)
|
||||
}
|
||||
/**修改工单回复信息 */
|
||||
export function updateTicketReplayInfo(data){
|
||||
return request.post('/api/v1/admin/work_order/update_reply',data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择头像"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="avatar-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 用户文件列表 -->
|
||||
<el-tab-pane label="用户文件" name="userFiles">
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
<h4>用户文件列表</h4>
|
||||
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||
上传新头像
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="file-grid" v-loading="loading">
|
||||
<div
|
||||
v-for="file in fileList"
|
||||
:key="file.cover_id"
|
||||
class="file-item"
|
||||
:class="{ 'selected': selectedId === file.cover_id }"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
<div class="file-preview">
|
||||
<img
|
||||
v-if="isImageFile(file)"
|
||||
:src="file.url"
|
||||
:alt="file.realName"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<el-icon v-else class="file-icon"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<p class="file-name">{{ file.realName }}</p>
|
||||
<p class="file-size">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="fileList.length === 0 && !loading" description="暂无文件" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 上传头像 -->
|
||||
<el-tab-pane label="上传头像" name="upload">
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传jpg/png文件,且不超过2MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedId"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, UploadFilled, Document } from '@element-plus/icons-vue'
|
||||
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
|
||||
import { closeAllMessage } from '../../utils/message'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
currentCoverId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('userFiles')
|
||||
const fileList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedId = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
selectedId.value = props.currentCoverId
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取文件列表
|
||||
const fetchFileList = async () => {
|
||||
if (!props.userId) return
|
||||
|
||||
loading.value = true
|
||||
fileList.value = [] // 清空列表
|
||||
|
||||
try {
|
||||
const res = await getFileList({
|
||||
page: currentPage.value,
|
||||
count: pageSize.value
|
||||
})
|
||||
|
||||
console.log("获取文件列表:", res)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const list = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
|
||||
// 获取每个文件的详情
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
try {
|
||||
console.log("获取文件详情:", list[i].id)
|
||||
const res2 = await getFileDetail({ file_id: list[i].id })
|
||||
if (res2.data.code === 200) {
|
||||
fileList.value.push({
|
||||
url: res2.data.data.url,
|
||||
cover_id: res2.data.data.data.id,
|
||||
size: res2.data.data.data.size,
|
||||
realName: res2.data.data.data.realName
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件详情失败:', error)
|
||||
}
|
||||
}
|
||||
console.log("文件列表1237:", fileList.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
ElMessage.error('获取文件列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.name === 'userFiles') {
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 切换到上传标签页
|
||||
const switchToUpload = () => {
|
||||
activeTab.value = 'upload'
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file) => {
|
||||
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
|
||||
const extension = file.realName?.split('.').pop()?.toLowerCase()
|
||||
return imageTypes.includes(extension)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (!size) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let unitIndex = 0
|
||||
let fileSize = size
|
||||
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
const selectFile = (file) => {
|
||||
selectedId.value = file.cover_id
|
||||
}
|
||||
|
||||
// 上传前验证
|
||||
const beforeUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 自定义上传
|
||||
const handleUpload = async (options) => {
|
||||
const { file } = options
|
||||
const formData = new FormData()
|
||||
formData.append('files', file)
|
||||
formData.append('file_names', file.name)
|
||||
formData.append('update_type', 'cover')
|
||||
|
||||
try {
|
||||
const res = await uploadFile(formData)
|
||||
console.log("上传文件:", res)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success("上传成功")
|
||||
// 重置到第一页并刷新文件列表
|
||||
currentPage.value = 1
|
||||
await fetchFileList()
|
||||
// 切换到文件列表标签页
|
||||
activeTab.value = 'userFiles'
|
||||
// 自动选择新上传的文件
|
||||
if (res.data.data?.id) {
|
||||
selectedId.value = res.data.data.id
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载错误处理
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedId.value = ''
|
||||
fileList.value = []
|
||||
currentPage.value = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedId.value) {
|
||||
const selectedFile = fileList.value.find(file => file.cover_id === selectedId.value)
|
||||
emit('confirm', {
|
||||
cover_id: selectedId.value,
|
||||
url: selectedFile?.url || ''
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.avatar-selector {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.file-list-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-list-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #409EFF;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
border-color: #409EFF;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-preview .file-icon {
|
||||
font-size: 32px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
margin: 0 0 4px 0;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,15 +3,15 @@
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="logo-container">
|
||||
<h1 class="title">零零七云计算后台控制面板</h1>
|
||||
<img src="@/assets/logo.png" alt="Logo" class="logo-img" />
|
||||
</div>
|
||||
<el-scrollbar>
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="sidebar-menu"
|
||||
background-color="#ffffff"
|
||||
text-color="#333333"
|
||||
active-text-color="#1890ff"
|
||||
background-color="transparent"
|
||||
text-color="#34495e"
|
||||
active-text-color="#2c3e50"
|
||||
:unique-opened="true"
|
||||
router
|
||||
>
|
||||
@@ -143,42 +143,39 @@ const handleLogout = () => {
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-right: 1px solid #e1e8ed;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
height: 60px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
color: #333;
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
.logo-img {
|
||||
height: 50px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #1890ff;
|
||||
.sidebar-scrollbar {
|
||||
height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 60px);
|
||||
min-height: 100%;
|
||||
background-color: transparent !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 主容器样式 */
|
||||
@@ -193,9 +190,9 @@ const handleLogout = () => {
|
||||
/* 顶部导航栏样式 */
|
||||
.navbar {
|
||||
height: 60px;
|
||||
padding: 0 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 0 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -205,32 +202,35 @@ const handleLogout = () => {
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
padding: 0 10px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #606266;
|
||||
color: #34495e;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
@@ -239,16 +239,31 @@ const handleLogout = () => {
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
height: 60px;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.avatar-container:hover {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 8px;
|
||||
margin: 0;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 内容区域样式 */
|
||||
@@ -271,12 +286,141 @@ const handleLogout = () => {
|
||||
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) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #34495e;
|
||||
transition: all 0.2s ease;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item i) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 侧边栏滚动条样式优化 */
|
||||
:deep(.sidebar-scrollbar .el-scrollbar__wrap) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:deep(.sidebar-scrollbar .el-scrollbar__view) {
|
||||
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 菜单项样式优化 */
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 50px;
|
||||
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) {
|
||||
height: 50px;
|
||||
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>
|
||||
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.breadcrumb-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
color: #7f8c8d;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner a) {
|
||||
color: #606266;
|
||||
font-weight: normal;
|
||||
transition: color 0.2s ease;
|
||||
color: #7f8c8d;
|
||||
font-weight: 400;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
: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) {
|
||||
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>
|
||||
@@ -39,65 +39,49 @@ const hasChildren = computed(() => {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
color: #7f8c8d;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 激活菜单项特效 */
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
background-color: #e6f7ff !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
.el-menu-item .el-icon, :deep(.el-sub-menu__title .el-icon) {
|
||||
color: #7f8c8d !important;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
.el-menu-item:hover .el-icon,
|
||||
:deep(.el-sub-menu__title:hover .el-icon) {
|
||||
color: #34495e !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
/* 激活菜单项图标 */
|
||||
.el-menu-item.is-active .el-icon {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
/* 修复图标颜色 */
|
||||
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
||||
color: #1890ff !important;
|
||||
/* 菜单文字样式 */
|
||||
.el-menu-item span, :deep(.el-sub-menu__title span) {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
/* 修复箭头颜色 */
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
color: #1890ff !important;
|
||||
/* 子菜单项样式优化 */
|
||||
:deep(.el-sub-menu .el-menu-item) {
|
||||
font-size: 13px;
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
:deep(.el-menu--inline) {
|
||||
background-color: #fafafa;
|
||||
:deep(.el-sub-menu .el-menu-item .el-icon) {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
+104
-106
@@ -59,14 +59,16 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||
|
||||
const router = useRouter()
|
||||
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 top = ref(0)
|
||||
@@ -77,101 +79,67 @@ const selectedTag = ref({})
|
||||
const initTags = () => {
|
||||
// 如果当前路由不在访问过的标签中,添加它
|
||||
if (route.name) {
|
||||
addVisitedView(route)
|
||||
tagsViewStore.addVisitedView(route)
|
||||
}
|
||||
// 添加固定标签(仪表盘)
|
||||
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
||||
if (dashboardRoute) {
|
||||
affixTags.value.push(dashboardRoute)
|
||||
addVisitedView(dashboardRoute)
|
||||
// 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
|
||||
// 简单起见,我们假设 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 { fullPath } = view
|
||||
router.replace('/redirect' + fullPath)
|
||||
}
|
||||
|
||||
// 关闭选中的标签
|
||||
const closeSelectedTag = (view) => {
|
||||
// 从访问过的标签中移除
|
||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||
if (index > -1) {
|
||||
visitedViews.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 如果关闭的是当前标签,则跳转到下一个标签
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews.value, view)
|
||||
}
|
||||
tagsViewStore.delVisitedView(view).then((visitedViews) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews, view)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭其他标签
|
||||
const closeOthersTags = () => {
|
||||
// 保留固定标签和当前选中的标签
|
||||
visitedViews.value = visitedViews.value.filter(v => {
|
||||
return isAffix(v) || v.path === selectedTag.value.path
|
||||
router.push(selectedTag.value)
|
||||
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
|
||||
// moveToCurrentTag() // 如果有滚动逻辑
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭左侧标签
|
||||
const closeLeftTags = () => {
|
||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
||||
if (selectedIndex === -1) return
|
||||
|
||||
// 保留固定标签和右侧标签
|
||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||
return isAffix(v) || i >= selectedIndex
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
|
||||
if (!visitedViews.find(i => i.path === route.path)) {
|
||||
toLastView(visitedViews)
|
||||
}
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧标签
|
||||
const closeRightTags = () => {
|
||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
||||
if (selectedIndex === -1) return
|
||||
|
||||
// 保留固定标签和左侧标签
|
||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||
return isAffix(v) || i <= selectedIndex
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
|
||||
if (!visitedViews.find(i => i.path === route.path)) {
|
||||
toLastView(visitedViews)
|
||||
}
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭所有标签
|
||||
const closeAllTags = () => {
|
||||
// 仅保留固定标签
|
||||
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
|
||||
|
||||
// 跳转到第一个标签或首页
|
||||
toLastView(visitedViews.value)
|
||||
tagsViewStore.delAllViews().then((visitedViews) => {
|
||||
toLastView(visitedViews)
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到最后一个标签或首页
|
||||
@@ -182,7 +150,6 @@ const toLastView = (visitedViews, view) => {
|
||||
} else {
|
||||
// 如果没有标签,则跳转到首页
|
||||
if (view && view.name === 'Dashboard') {
|
||||
// 如果当前是首页,则刷新页面
|
||||
router.push('/redirect' + '/dashboard')
|
||||
} else {
|
||||
router.push('/')
|
||||
@@ -197,7 +164,7 @@ const isActive = (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) => {
|
||||
if (newRoute.name) {
|
||||
addVisitedView(newRoute)
|
||||
tagsViewStore.addVisitedView(newRoute)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -245,9 +212,8 @@ onBeforeUnmount(() => {
|
||||
.tags-view-container {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -256,7 +222,7 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
@@ -270,41 +236,56 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tag, .active-tag {
|
||||
height: 28px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
background-color: #f4f4f5;
|
||||
padding: 0 12px;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border: 1px solid #e8e8e8;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: #7f8c8d;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #34495e;
|
||||
background-color: #e8ecf0;
|
||||
}
|
||||
|
||||
.active-tag {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-bottom: 2px solid #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
margin-right: 4px;
|
||||
margin-right: 6px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #95a5a6;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tag:hover .tag-icon {
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.active-tag .tag-icon {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tag-title {
|
||||
@@ -315,36 +296,46 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.tag-close {
|
||||
margin-left: 5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
margin-left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 0;
|
||||
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 {
|
||||
color: #666;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.active-tag .tag-close {
|
||||
color: #1890ff;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.active-tag:hover .tag-close {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
.active-tag .tag-close:hover {
|
||||
color: #e74c3c;
|
||||
background-color: rgba(231, 76, 60, 0.1);
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
background-color: #ffffff;
|
||||
list-style-type: none;
|
||||
padding: 6px 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 4px 0;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
|
||||
font-size: 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.contextmenu li {
|
||||
@@ -352,15 +343,22 @@ onBeforeUnmount(() => {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #34495e;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.contextmenu li:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #1890ff;
|
||||
background-color: #f8f9fa;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.contextmenu li .el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.contextmenu li:hover .el-icon {
|
||||
color: #2c3e50;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="detailData" class="detail-content">
|
||||
<table class="detail-table">
|
||||
<!-- 优惠码特有字段 -->
|
||||
<tr v-if="type === 'code'">
|
||||
<td class="label">优惠码</td>
|
||||
<td class="value">{{ detailData.code }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 名称 -->
|
||||
<tr>
|
||||
<td class="label">{{ type === 'code' ? '名称' : '代金券名称' }}</td>
|
||||
<td class="value">{{ detailData.name }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 备注 -->
|
||||
<tr>
|
||||
<td class="label">备注</td>
|
||||
<td class="value secondary">{{ detailData.note || '无' }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 优惠类型(仅优惠码) -->
|
||||
<tr v-if="type === 'code'" class="alternate">
|
||||
<td class="label">优惠类型</td>
|
||||
<td class="value">
|
||||
<span :class="['type-tag', detailData.percentage ? 'percentage' : 'amount']">
|
||||
{{ detailData.percentage ? '百分比折扣' : '固定金额' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 优惠值/面额 -->
|
||||
<tr :class="type === 'code' ? '' : 'alternate'">
|
||||
<td class="label">{{ type === 'code' ? '优惠值' : '面额' }}</td>
|
||||
<td class="value">
|
||||
<span v-if="detailData.percentage" class="highlight-value percentage">
|
||||
{{ (detailData.percentage / 100).toFixed(0) }}%
|
||||
</span>
|
||||
<span v-else class="highlight-value amount">
|
||||
¥{{ (detailData.amount / 100).toFixed(2) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 最低消费 -->
|
||||
<tr>
|
||||
<td class="label">最低消费</td>
|
||||
<td class="value">¥{{ (detailData.minAmount / 100).toFixed(2) }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 最大抵扣 -->
|
||||
<tr>
|
||||
<td class="label">最大抵扣</td>
|
||||
<td class="value">
|
||||
<span v-if="detailData.maxAmount">¥{{ (detailData.maxAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else class="secondary">无限制</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 最大使用次数 -->
|
||||
<tr class="alternate">
|
||||
<td class="label">最大使用次数</td>
|
||||
<td class="value">
|
||||
<span v-if="detailData.maxTimes">{{ detailData.maxTimes }}</span>
|
||||
<span v-else class="secondary">无限制</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 单用户次数 -->
|
||||
<tr>
|
||||
<td class="label">单用户次数</td>
|
||||
<td class="value">
|
||||
<span v-if="detailData.userTimes">{{ detailData.userTimes }}</span>
|
||||
<span v-else class="secondary">无限制</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 有效期(仅代金券) -->
|
||||
<tr v-if="type === 'coupon'" class="alternate">
|
||||
<td class="label">有效期(天)</td>
|
||||
<td class="value">
|
||||
{{ detailData.duration ? (detailData.duration / 86400).toFixed(0) + '天' : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 有效期开始 -->
|
||||
<tr :class="type === 'coupon' ? '' : 'alternate'">
|
||||
<td class="label">{{ type === 'code' ? '有效期开始' : '发放时间开始' }}</td>
|
||||
<td class="value">{{ formatISODate(detailData.startTime) }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 有效期结束 -->
|
||||
<tr :class="type === 'coupon' ? 'alternate' : ''">
|
||||
<td class="label">{{ type === 'code' ? '有效期结束' : '发放时间结束' }}</td>
|
||||
<td class="value">{{ formatISODate(detailData.endTime) }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 续费可用 -->
|
||||
<tr :class="type === 'coupon' ? '' : 'alternate'">
|
||||
<td class="label">续费可用</td>
|
||||
<td class="value">
|
||||
<span :class="['status-icon', detailData.renew ? 'success' : 'danger']">
|
||||
{{ detailData.renew ? '✓ 是' : '✗ 否' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 同类型可叠加 -->
|
||||
<tr :class="type === 'coupon' ? 'alternate' : ''">
|
||||
<td class="label">同类型可叠加</td>
|
||||
<td class="value">
|
||||
<span :class="['status-icon', detailData.canStacking ? 'success' : 'danger']">
|
||||
{{ detailData.canStacking ? '✓ 是' : '✗ 否' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 其他类型可叠加 -->
|
||||
<tr :class="type === 'coupon' ? '' : 'alternate'">
|
||||
<td class="label">其他类型可叠加</td>
|
||||
<td class="value">
|
||||
<span :class="['status-icon', detailData.canCombine ? 'success' : 'danger']">
|
||||
{{ detailData.canCombine ? '✓ 是' : '✗ 否' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<tr :class="type === 'coupon' ? 'alternate' : ''">
|
||||
<td class="label">创建时间</td>
|
||||
<td class="value timestamp">{{ formatISODate(detailData.CreatedAt) }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- 更新时间 -->
|
||||
<tr>
|
||||
<td class="label">更新时间</td>
|
||||
<td class="value timestamp">{{ formatISODate(detailData.UpdatedAt) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['code', 'coupon'].includes(value)
|
||||
},
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return props.type === 'code' ? '优惠码详情' : '代金券详情'
|
||||
})
|
||||
|
||||
// 格式化ISO 8601日期字符串
|
||||
const formatISODate = (isoStr) => {
|
||||
if (!isoStr) return '-'
|
||||
try {
|
||||
const date = new Date(isoStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
} catch {
|
||||
return isoStr
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.detail-table tr {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-table tr.alternate {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.detail-table td {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.detail-table .label {
|
||||
width: 140px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-table .value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-table .value.secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.detail-table .value.timestamp {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 类型标签 */
|
||||
.type-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.type-tag.percentage {
|
||||
background-color: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.type-tag.amount {
|
||||
background-color: #eff6ff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 突出显示的值 */
|
||||
.highlight-value {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.highlight-value.percentage {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.highlight-value.amount {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 状态图标 */
|
||||
.status-icon.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.status-icon.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
+115
-16
@@ -5,10 +5,86 @@ export const menus = [
|
||||
icon: 'DataBoard'
|
||||
},
|
||||
{
|
||||
path : '/ticket',
|
||||
path: '/ticket',
|
||||
title: '工单处理',
|
||||
icon: 'DataBoard'
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
title: '用户管理',
|
||||
icon: 'User',
|
||||
children: [
|
||||
{
|
||||
path: '/user/list',
|
||||
title: '用户列表'
|
||||
},
|
||||
{
|
||||
path: '/user/balance',
|
||||
title: '用户余额管理'
|
||||
},
|
||||
{
|
||||
path: '/user/group',
|
||||
title: '用户组管理'
|
||||
},
|
||||
{
|
||||
path: '/user/admin-group',
|
||||
title: '管理员组管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/product',
|
||||
title: '商品管理',
|
||||
icon: 'Goods',
|
||||
children: [
|
||||
{
|
||||
path: '/product/list',
|
||||
title: '商品列表'
|
||||
},
|
||||
{
|
||||
path: '/product/group',
|
||||
title: '商品分组'
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/order',
|
||||
title: '订单管理',
|
||||
icon: 'Document',
|
||||
children: [
|
||||
{
|
||||
path: '/order/list',
|
||||
title: '订单列表'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/marketing',
|
||||
title: '优惠营销',
|
||||
icon: 'Present',
|
||||
children: [
|
||||
{
|
||||
path: '/marketing/discount',
|
||||
title: '优惠码管理'
|
||||
},
|
||||
{
|
||||
path: '/marketing/voucher',
|
||||
title: '代金券管理'
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/activity',
|
||||
title: '活动管理',
|
||||
icon: 'TrophyBase',
|
||||
children: [
|
||||
{
|
||||
path: '/activity/signin',
|
||||
title: '签到活动'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/acs',
|
||||
@@ -33,27 +109,27 @@ export const menus = [
|
||||
{ path: '/acs/images/categories', title: '镜像分类' }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/acs/nodes', title: '节点管理'
|
||||
{
|
||||
path: '/acs/nodes',
|
||||
title: '节点管理'
|
||||
},
|
||||
{
|
||||
path: '/acs/guacamole',
|
||||
title: '远程桌面网关管理',
|
||||
icon: 'Monitor'
|
||||
},{
|
||||
title: '远程桌面网关管理'
|
||||
},
|
||||
{
|
||||
path: '/audit',
|
||||
title: '站点审计',
|
||||
icon: 'Monitor',
|
||||
children: [
|
||||
{ path: '/audit/all', title: '所有站点' },
|
||||
{ path: '/audit/violation', title: '违规站点' }
|
||||
]
|
||||
},{
|
||||
path:'/setting',
|
||||
title:'全局设置管理',
|
||||
icon:'Setting',
|
||||
children:[
|
||||
{path:'/setting/global',title:'全局设置'}
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
title: '全局设置管理',
|
||||
children: [
|
||||
{ path: '/setting/global', title: '全局设置' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -63,9 +139,32 @@ export const menus = [
|
||||
title: '系统管理',
|
||||
icon: 'Setting',
|
||||
children: [
|
||||
// { path: '/system/users', title: '用户管理' },
|
||||
// { path: '/system/operation-log', title: '操作日志' },
|
||||
{ path: '/system/domain-whitelist', title: '域名白名单' }
|
||||
{
|
||||
path: '/system/permission',
|
||||
title: '权限管理',
|
||||
children: [
|
||||
{ path: '/system/permission/route', title: '路由权限' },
|
||||
{ path: '/system/permission/admin', title: '管理员权限' }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/system/file',
|
||||
title: '文件管理'
|
||||
},
|
||||
|
||||
{
|
||||
path: '/system/domain-whitelist',
|
||||
title: '域名白名单'
|
||||
},
|
||||
{
|
||||
path: '/system/setting-group',
|
||||
title: '配置组管理'
|
||||
},
|
||||
{
|
||||
path: '/system/setting-list',
|
||||
title: '配置管理'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
+228
-33
@@ -41,7 +41,7 @@ const routes = [
|
||||
},
|
||||
component: () => import('../views/ticket/TicketChat.vue'),
|
||||
},
|
||||
|
||||
|
||||
// ACS管理路由
|
||||
{
|
||||
path: 'acs',
|
||||
@@ -130,7 +130,19 @@ const routes = [
|
||||
meta: {
|
||||
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',
|
||||
name: 'Guacamole',
|
||||
component: () => import('../views/acs/guacamole/Guacamole.vue'),
|
||||
@@ -140,6 +152,166 @@ const routes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// 用户管理路由
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'User'
|
||||
},
|
||||
redirect: '/user/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'UserList',
|
||||
component: () => import('../views/user/UserList.vue'),
|
||||
meta: {
|
||||
title: '用户列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'UserDetail',
|
||||
component: () => import('../views/user/UserDetail.vue'),
|
||||
meta: {
|
||||
title: '用户详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'balance',
|
||||
name: 'UserBalance',
|
||||
component: () => import('../views/user/UserBalance.vue'),
|
||||
meta: {
|
||||
title: '用户余额管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'group',
|
||||
name: 'UserGroup',
|
||||
component: () => import('../views/user/UserGroup.vue'),
|
||||
meta: {
|
||||
title: '用户组管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'admin-group',
|
||||
name: 'AdminGroup',
|
||||
component: () => import('../views/user/AdminGroup.vue'),
|
||||
meta: {
|
||||
title: '管理员组管理'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 商品管理路由
|
||||
{
|
||||
path: 'product',
|
||||
name: 'Product',
|
||||
meta: {
|
||||
title: '商品管理',
|
||||
icon: 'Goods'
|
||||
},
|
||||
redirect: '/product/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'ProductList',
|
||||
component: () => import('../views/product/ProductList.vue'),
|
||||
meta: {
|
||||
title: '商品列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'group',
|
||||
name: 'ProductGroup',
|
||||
component: () => import('../views/product/ProductGroup.vue'),
|
||||
meta: {
|
||||
title: '商品分组'
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
// 订单管理路由
|
||||
{
|
||||
path: 'order',
|
||||
name: 'Order',
|
||||
meta: {
|
||||
title: '订单管理',
|
||||
icon: 'Document'
|
||||
},
|
||||
redirect: '/order/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'OrderList',
|
||||
component: () => import('../views/order/OrderList.vue'),
|
||||
meta: {
|
||||
title: '订单列表'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 优惠营销路由
|
||||
{
|
||||
path: 'marketing',
|
||||
name: 'Marketing',
|
||||
meta: {
|
||||
title: '优惠营销',
|
||||
icon: 'Present'
|
||||
},
|
||||
redirect: '/marketing/discount',
|
||||
children: [
|
||||
{
|
||||
path: 'discount',
|
||||
name: 'DiscountCode',
|
||||
component: () => import('../views/marketing/DiscountCode.vue'),
|
||||
meta: {
|
||||
title: '优惠码管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'voucher',
|
||||
name: 'Voucher',
|
||||
component: () => import('../views/marketing/Voucher.vue'),
|
||||
meta: {
|
||||
title: '代金券管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'voucher/:id/manage',
|
||||
name: 'VoucherManagement',
|
||||
component: () => import('../views/marketing/VoucherManagement.vue'),
|
||||
meta: {
|
||||
title: '代金券详情管理',
|
||||
hidden: true,
|
||||
activeMenu: '/marketing/voucher'
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
// 活动管理路由
|
||||
{
|
||||
path: 'activity',
|
||||
name: 'Activity',
|
||||
meta: {
|
||||
title: '活动管理',
|
||||
icon: 'TrophyBase'
|
||||
},
|
||||
redirect: '/activity/signin',
|
||||
children: [
|
||||
{
|
||||
path: 'signin',
|
||||
name: 'SigninActivity',
|
||||
component: () => import('../views/activity/SigninActivity.vue'),
|
||||
meta: {
|
||||
title: '签到活动'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
name: 'System',
|
||||
@@ -147,28 +319,51 @@ const routes = [
|
||||
title: '系统管理',
|
||||
icon: 'Setting'
|
||||
},
|
||||
redirect: '/system/domain-whitelist',
|
||||
redirect: '/system/permission/route',
|
||||
children: [
|
||||
// 注释掉的用户管理和操作日志路由,与菜单配置保持一致
|
||||
// {
|
||||
// path: 'users',
|
||||
// name: 'Users',
|
||||
// component: () => import('../views/system/Users.vue'),
|
||||
// meta: {
|
||||
// title: '用户管理'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'operation-log',
|
||||
// name: 'OperationLog',
|
||||
// component: OperationLog,
|
||||
// meta: { title: '操作日志' }
|
||||
// },
|
||||
{
|
||||
path: 'permission/route',
|
||||
name: 'PermissionRoute',
|
||||
component: () => import('../views/system/PermissionRoute.vue'),
|
||||
meta: {
|
||||
title: '路由权限'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'permission/admin',
|
||||
name: 'PermissionAdmin',
|
||||
component: () => import('../views/system/PermissionAdmin.vue'),
|
||||
meta: {
|
||||
title: '管理员权限'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'file',
|
||||
name: 'SystemFile',
|
||||
component: () => import('../views/system/SystemFile.vue'),
|
||||
meta: {
|
||||
title: '文件管理'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'domain-whitelist',
|
||||
name: 'DomainWhitelist',
|
||||
component: () => import('../views/system/DomainWhitelist.vue'),
|
||||
meta: { title: '域名白名单' }
|
||||
},
|
||||
{
|
||||
path: 'setting-group',
|
||||
name: 'SettingGroup',
|
||||
component: () => import('../views/system/SettingGroup.vue'),
|
||||
meta: { title: '配置组管理' }
|
||||
},
|
||||
{
|
||||
path: 'setting-list',
|
||||
name: 'SettingList',
|
||||
component: () => import('../views/system/Setting.vue'),
|
||||
meta: { title: '配置管理' }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -269,21 +464,21 @@ const routes = [
|
||||
title: '容器详情',
|
||||
hidden: true
|
||||
}
|
||||
},{
|
||||
path:'servers/container/console',
|
||||
name:'ContainerConsole',
|
||||
component:()=>import('../views/acs/nodes/containerConsole.vue'),
|
||||
meta:{
|
||||
title:'终端容器',
|
||||
hidden:true
|
||||
}, {
|
||||
path: 'servers/container/console',
|
||||
name: 'ContainerConsole',
|
||||
component: () => import('../views/acs/nodes/containerConsole.vue'),
|
||||
meta: {
|
||||
title: '终端容器',
|
||||
hidden: true
|
||||
}
|
||||
},{
|
||||
path:'servers/container/files',
|
||||
name:'ContainerFiles',
|
||||
component:()=>import('../views/acs/nodes/containFile.vue'),
|
||||
meta:{
|
||||
title:'容器文件管理',
|
||||
hidden:true
|
||||
}, {
|
||||
path: 'servers/container/files',
|
||||
name: 'ContainerFiles',
|
||||
component: () => import('../views/acs/nodes/containFile.vue'),
|
||||
meta: {
|
||||
title: '容器文件管理',
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -317,7 +512,7 @@ const router = createRouter({
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
|
||||
|
||||
|
||||
// 这里可以添加登录验证逻辑
|
||||
const isAuthenticated = localStorage.getItem('token')
|
||||
if (to.path !== '/login' && !isAuthenticated) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -3,15 +3,15 @@ import {http2} from "@/utils/request.js";
|
||||
|
||||
/**获取所有站点 */
|
||||
export const getSiteList = (data) => {
|
||||
return http2.get(`/v1/admin/audit/list?page=${data.page}&server_id=${data.server_id}&user_id=${data.user_id}&count=${data.count}&key=${data.key}`)
|
||||
return http2.get(`/acs/v1/admin/audit/list?page=${data.page}&server_id=${data.server_id}&user_id=${data.user_id}&count=${data.count}&key=${data.key}`)
|
||||
}
|
||||
/**手动触发站点审计 */
|
||||
export const auditSite = () => {
|
||||
return http2.get(`/v1/admin/audit/start`)
|
||||
return http2.get(`/acs/v1/admin/audit/start`)
|
||||
}
|
||||
/**删除违规网页审计 传入参数: web_key 站点名*/
|
||||
export const delAudit = (data) => {
|
||||
return http2.post(`/v1/admin/audit/delete`,data,{
|
||||
return http2.post(`/acs/v1/admin/audit/delete`,data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -19,5 +19,5 @@ export const delAudit = (data) => {
|
||||
}
|
||||
/**获取违规网页审计列表 */
|
||||
export const getAuditList = (data) => {
|
||||
return http2.get(`/v1/admin/audit/violation_list?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
return http2.get(`/acs/v1/admin/audit/violation_list?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
export const getFileList = (data) => {
|
||||
return http2.get(`/v1/file/list?container_id=${data.container_id}&path=${data.path}`)
|
||||
return http2.get(`/acs/v1/file/list?container_id=${data.container_id}&path=${data.path}`)
|
||||
}
|
||||
/** 读取文件内容 */
|
||||
export const readFile = (data) => {
|
||||
return http2.post(`/v1/file/read`,data, {
|
||||
return http2.post(`/acs/v1/file/read`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export const readFile = (data) => {
|
||||
}
|
||||
/*删除文件或文件夹 */
|
||||
export const deleteFile = (data) => {
|
||||
return http2.post(`/v1/file/delete`,data, {
|
||||
return http2.post(`/acs/v1/file/delete`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export const deleteFile = (data) => {
|
||||
}
|
||||
/*写入文件 */
|
||||
export const writeFile = (data) => {
|
||||
return http2.post(`/v1/file/write`,data, {
|
||||
return http2.post(`/acs/v1/file/write`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export const writeFile = (data) => {
|
||||
}
|
||||
/*创建文件夹 */
|
||||
export const createFolder = (data) => {
|
||||
return http2.post(`/v1/file/mkdir`,data, {
|
||||
return http2.post(`/acs/v1/file/mkdir`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export const createFolder = (data) => {
|
||||
}
|
||||
/**上传文件 */
|
||||
export const uploadFile = (data) => {
|
||||
return http2.post(`/v1/file/upload_file`,data, {
|
||||
return http2.post(`/acs/v1/file/upload_file`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export const uploadFile = (data) => {
|
||||
}
|
||||
/**下载文件链接 */
|
||||
export const downloadFile = (data) => {
|
||||
return http2.post(`/v1/file/get_down_link`,data, {
|
||||
return http2.post(`/acs/v1/file/get_down_link`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export const downloadFile = (data) => {
|
||||
}
|
||||
/**压缩文件 */
|
||||
export const compressFile = (data) => {
|
||||
return http2.post(`/v1/file/zip_file`,data, {
|
||||
return http2.post(`/acs/v1/file/zip_file`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export const compressFile = (data) => {
|
||||
}
|
||||
/**解压文件 */
|
||||
export const decompressFile = (data) => {
|
||||
return http2.post(`/v1/file/unzip_file`,data, {
|
||||
return http2.post(`/acs/v1/file/unzip_file`,data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import {http2} from "@/utils/request.js";
|
||||
|
||||
/**获取 guacamole 列表 */
|
||||
export const getGuacamoleList = data => {
|
||||
return http2.get(`/v1/admin/server/get_guacamole_list`);
|
||||
return http2.get(`/acs/v1/admin/server/get_guacamole_list`);
|
||||
};
|
||||
/**获取服务器 guacamole 信息 */
|
||||
export const getGuacamoleInfo = data => {
|
||||
return http2.get(`/v1/admin/server/get_server_guacamole?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/server/get_server_guacamole?server_id=${data}`);
|
||||
};
|
||||
/**新增 guacamole 参数 url:string,username:string,password:string*/
|
||||
export const addGuacamoleInfo = data => {
|
||||
return http2.post(`/v1/admin/server/add_guacamole`, data,{
|
||||
return http2.post(`/acs/v1/admin/server/add_guacamole`, data,{
|
||||
headers:{
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export const addGuacamoleInfo = data => {
|
||||
};
|
||||
/**修改guacamole 参数 id:string,url:string,username:string,password:string*/
|
||||
export const updateGuacamoleInfo = data => {
|
||||
return http2.post(`/v1/admin/server/edit_guacamole`, data,{
|
||||
return http2.post(`/acs/v1/admin/server/edit_guacamole`, data,{
|
||||
headers:{
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const updateGuacamoleInfo = data => {
|
||||
};
|
||||
/**删除guacamole 参数 id:string */
|
||||
export const deleteGuacamoleInfo = data => {
|
||||
return http2.post(`/v1/admin/server/delete_guacamole`, data,{
|
||||
return http2.post(`/acs/v1/admin/server/delete_guacamole`, data,{
|
||||
headers:{
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,15 +1,15 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取消息列表 */
|
||||
export const getMessageList = (data) => {
|
||||
return http2.get(`/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
|
||||
return http2.get(`/acs/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
|
||||
}
|
||||
/**获取单条消息 */
|
||||
export const getMessage = (data) => {
|
||||
return http2.get(`/v1/messages/get_message?message_id=${data}`)
|
||||
return http2.get(`/acs/v1/messages/get_message?message_id=${data}`)
|
||||
}
|
||||
/**添加消息 */
|
||||
export const addMessage = (data) => {
|
||||
return http2.post(`/v1/messages/add_message`, data,{
|
||||
return http2.post(`/acs/v1/messages/add_message`, data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -17,7 +17,7 @@ headers: {
|
||||
}
|
||||
/**删除消息 */
|
||||
export const deleteMessage = (data) => {
|
||||
return http2.post(`/v1/messages/delete_message`, data,{
|
||||
return http2.post(`/acs/v1/messages/delete_message`, data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -25,7 +25,7 @@ headers: {
|
||||
}
|
||||
/**修改消息 */
|
||||
export const editMessage = (data) => {
|
||||
return http2.post(`/v1/messages/update_message`, data,{
|
||||
return http2.post(`/acs/v1/messages/update_message`, data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -33,11 +33,11 @@ headers: {
|
||||
}
|
||||
/**获取附件列表 */
|
||||
export const getFileList = (data) => {
|
||||
return http2.get(`/v1/attachment/get_attachment_list?page=${data.page}&count=${data.count}&key=${data.key}&user_type=${data.user_type}`)
|
||||
return http2.get(`/acs/v1/attachment/get_attachment_list?page=${data.page}&count=${data.count}&key=${data.key}&user_type=${data.user_type}`)
|
||||
}
|
||||
/**上传附件 */
|
||||
export const uploadFile = (data) => {
|
||||
return http2.post(`/v1/attachment/add_attachment`, data,{
|
||||
return http2.post(`/acs/v1/attachment/add_attachment`, data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -45,20 +45,20 @@ headers: {
|
||||
}
|
||||
/**删除附件 */
|
||||
export const deleteFile = (data) => {
|
||||
return http2.get(`/v1/attachment/delete_attachment?aid=${data}`)
|
||||
return http2.get(`/acs/v1/attachment/delete_attachment?aid=${data}`)
|
||||
}
|
||||
/**用户获取消息列表 */
|
||||
export const getUserMessageList = (data) => {
|
||||
return http2.get(`/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
|
||||
return http2.get(`/acs/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
|
||||
}
|
||||
/**用户获取单条消息 */
|
||||
export const getUserMessage = (data) => {
|
||||
return http2.get(`/v1/messages/get_message?message_id=${data}`)
|
||||
return http2.get(`/acs/v1/messages/get_message?message_id=${data}`)
|
||||
}
|
||||
|
||||
/**获取消息详情 */
|
||||
export const getMessageDetail = (data) => {
|
||||
return http2.get(`/v1/messages/get_message?message_id=${data.message_id}`)
|
||||
return http2.get(`/acs/v1/messages/get_message?message_id=${data.message_id}`)
|
||||
}
|
||||
/**修改图片大小 */
|
||||
export const compressAndConvertFileToBase64=async(file)=> {
|
||||
|
||||
+13
-13
@@ -2,19 +2,19 @@ import {http2} from "@/utils/request.js";
|
||||
/**获取镜像列表 */
|
||||
export const getMirrorList = data => {
|
||||
if(typeof data == "string"){
|
||||
return http2.get("/v1/image/list?server_id=" + data + "&count=9999999")
|
||||
return http2.get("/acs/v1/image/list?server_id=" + data + "&count=9999999")
|
||||
}
|
||||
return http2.get(`/v1/image/list?server_id=${data.server_id}&page=${data.page}&count=${data.count}&key=${data.key}&class_id=${data.class_id}`);
|
||||
return http2.get(`/acs/v1/image/list?server_id=${data.server_id}&page=${data.page}&count=${data.count}&key=${data.key}&class_id=${data.class_id}`);
|
||||
};
|
||||
/*用户获取镜像列表 */
|
||||
export const getUserMirrorList = data => {
|
||||
return http2.get(
|
||||
`/v1/image/list?server_id=${data.server_id}&count=${data.count}&page=${data.page}&key=${data.key}`
|
||||
`/acs/v1/image/list?server_id=${data.server_id}&count=${data.count}&page=${data.page}&key=${data.key}`
|
||||
);
|
||||
};
|
||||
/**上传镜像 */
|
||||
export const uploadMirror = data => {
|
||||
return http2.post("/v1/image/pull", data, {
|
||||
return http2.post("/acs/v1/image/pull", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export const uploadMirror = data => {
|
||||
};
|
||||
/**编辑镜像 */
|
||||
export const editMirror = data => {
|
||||
return http2.post("/v1/image/update", data, {
|
||||
return http2.post("/acs/v1/image/update", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const editMirror = data => {
|
||||
};
|
||||
/**删除镜像 */
|
||||
export const delMirror = data => {
|
||||
return http2.post("/v1/image/delete", data, {
|
||||
return http2.post("/acs/v1/image/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -38,11 +38,11 @@ export const delMirror = data => {
|
||||
};
|
||||
/**镜像同步 */
|
||||
export const syncMirror = data => {
|
||||
return http2.get(`/v1/image/sync?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/image/sync?server_id=${data}`);
|
||||
};
|
||||
/**重新拉取镜像 */
|
||||
export const pullMirror = data => {
|
||||
return http2.post(`/v1/image/repull`, data, {
|
||||
return http2.post(`/acs/v1/image/repull`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -53,12 +53,12 @@ export const pullMirror = data => {
|
||||
export const Mirrorinfo = data => {
|
||||
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
||||
return http2.get(
|
||||
`/v1/image/info?image_id=${data.image_id}&server_type=${serverType}`
|
||||
`/acs/v1/image/info?image_id=${data.image_id}&server_type=${serverType}`
|
||||
);
|
||||
};
|
||||
|
||||
export const addVirtualMirror = data => {
|
||||
return http2.post("/v1/image/create", data, {
|
||||
return http2.post("/acs/v1/image/create", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -67,11 +67,11 @@ export const addVirtualMirror = data => {
|
||||
|
||||
|
||||
export const getImageTypeList = (server_id) => {
|
||||
return http2.get(`/v1/image/class_list?server_id=${server_id}`);
|
||||
return http2.get(`/acs/v1/image/class_list?server_id=${server_id}`);
|
||||
};
|
||||
|
||||
export const createImageType = (server_id,class_name,class_ico) => {
|
||||
return http2.post("/v1/image/class_create", {
|
||||
return http2.post("/acs/v1/image/class_create", {
|
||||
server_id,
|
||||
class_name,
|
||||
class_ico
|
||||
@@ -83,7 +83,7 @@ export const createImageType = (server_id,class_name,class_ico) => {
|
||||
};
|
||||
|
||||
export const updateImageType = (class_id,class_name,class_ico) => {
|
||||
return http2.post("/v1/image/class_update", {
|
||||
return http2.post("/acs/v1/image/class_update", {
|
||||
class_id,
|
||||
class_name,
|
||||
class_ico
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取订单列表 */
|
||||
export const getOrderList = (data) => {
|
||||
return http2.get(`/v1/admin/trades/get_trades?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
return http2.get(`/acs/v1/admin/trades/get_trades?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
}
|
||||
/**编辑订单 */
|
||||
export const editOrder = (data) => {
|
||||
return http2.post('/v1/admin/trades/update_trades',data,{
|
||||
return http2.post('/acs/v1/admin/trades/update_trades',data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -13,7 +13,7 @@ headers: {
|
||||
}
|
||||
/**删除订单 */
|
||||
export const deleteOrder = (data) => {
|
||||
return http2.post('/v1/admin/trades/delete_trade',data,{
|
||||
return http2.post('/acs/v1/admin/trades/delete_trade',data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -21,5 +21,5 @@ headers: {
|
||||
}
|
||||
/**用户获取订单列表 */
|
||||
export const getUserOrderList = (data) => {
|
||||
return http2.get(`/v1/user/procedure/get_trade_list?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
return http2.get(`/acs/v1/user/procedure/get_trade_list?page=${data.page}&count=${data.count}&key=${data.key}`)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export const get_pay_code = data => {
|
||||
};
|
||||
// /**email验证码 */
|
||||
// export const ask_update_user_email = data => {
|
||||
// return http2.post("/v1/user/info/ask_update_user_email", data, {
|
||||
// return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
|
||||
// headers: {
|
||||
// "Content-Type": "multipart/form-data"
|
||||
// }
|
||||
@@ -15,7 +15,7 @@ export const get_pay_code = data => {
|
||||
// };
|
||||
/**获取容器订单金额 */
|
||||
export const procedure_get_price = data => {
|
||||
return http2.post("/v1/user/procedure/get_price", data, {
|
||||
return http2.post("/acs/v1/user/procedure/get_price", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const procedure_get_price = data => {
|
||||
|
||||
/**获取虚拟机订单金额 */
|
||||
export const procedure_vir_price = data => {
|
||||
return http2.post("/v1/user/procedure/get_vm_price", data, {
|
||||
return http2.post("/acs/v1/user/procedure/get_vm_price", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
+70
-70
@@ -3,13 +3,13 @@ import {http2} from "@/utils/request.js";
|
||||
/** 获取所有服务器 */
|
||||
export const getServer = (page, count, key, type = "dockerContainer") => {
|
||||
return http2.get(
|
||||
`/v1/admin/server/get_server_list?page=${page}&count=${count}&key=${key}&server_type=${type}`
|
||||
`/acs/v1/admin/server/get_server_list?page=${page}&count=${count}&key=${key}&server_type=${type}`
|
||||
);
|
||||
};
|
||||
|
||||
/**新增服务器 */
|
||||
export const addServer = data => {
|
||||
return http2.post("/v1/admin/server/add_server", data, {
|
||||
return http2.post("/acs/v1/admin/server/add_server", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export const addServer = data => {
|
||||
};
|
||||
/**编辑服务器 */
|
||||
export const editServer = data => {
|
||||
return http2.post("/v1/admin/server/update_server", data, {
|
||||
return http2.post("/acs/v1/admin/server/update_server", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const editServer = data => {
|
||||
};
|
||||
/**删除服务器 */
|
||||
export const deleteServer = data => {
|
||||
return http2.post("/v1/admin/server/delete_server", data, {
|
||||
return http2.post("/acs/v1/admin/server/delete_server", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const deleteServer = data => {
|
||||
};
|
||||
/**查询指定服务器 */
|
||||
export const selectServer = data => {
|
||||
return http2.post("/v1/admin/server/select_server", data, {
|
||||
return http2.post("/acs/v1/admin/server/select_server", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -42,12 +42,12 @@ export const selectServer = data => {
|
||||
/**获取服务器套餐列表*/
|
||||
export const getServerPlan = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/container_plan/get_server_plan_list?server_id=${data.server_id}&count=${data.count}`
|
||||
`/acs/v1/admin/container_plan/get_server_plan_list?server_id=${data.server_id}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
/**获取指定套餐 */
|
||||
export const selectServerPlan = data => {
|
||||
return http2.post("/v1/admin/container_plan/get_server_plan_detail", data, {
|
||||
return http2.post("/acs/v1/admin/container_plan/get_server_plan_detail", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export const selectServerPlan = data => {
|
||||
};
|
||||
/**新增容器 */
|
||||
export const addContainer = data => {
|
||||
return http2.post("/v1/admin/container/add_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/add_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export const addContainer = data => {
|
||||
};
|
||||
/**删除容器网络 */
|
||||
export const deleteContainerNetwork = data => {
|
||||
return http2.post("/v1/user/container/delete_connect", data, {
|
||||
return http2.post("/acs/v1/user/container/delete_connect", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const deleteContainerNetwork = data => {
|
||||
|
||||
/**修改套餐信息 */
|
||||
export const editServerPlan = data => {
|
||||
return http2.post("/v1/admin/container_plan/update_server_plan", data, {
|
||||
return http2.post("/acs/v1/admin/container_plan/update_server_plan", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export const editServerPlan = data => {
|
||||
};
|
||||
/**新增套餐 */
|
||||
export const addServerPlan = data => {
|
||||
return http2.post("/v1/admin/container_plan/add_server_plan", data, {
|
||||
return http2.post("/acs/v1/admin/container_plan/add_server_plan", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export const addServerPlan = data => {
|
||||
};
|
||||
/**删除套餐 */
|
||||
export const deleteServerPlan = data => {
|
||||
return http2.post("/v1/admin/container_plan/delete_server_plan", data, {
|
||||
return http2.post("/acs/v1/admin/container_plan/delete_server_plan", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -97,19 +97,19 @@ export const deleteServerPlan = data => {
|
||||
/**获取容器列表 */
|
||||
export const getContainer = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/container/get_container_list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
`/acs/v1/admin/container/get_container_list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
);
|
||||
};
|
||||
|
||||
/**获取虚拟机列表 */
|
||||
export const getInstanceList = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/instance/list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
`/acs/v1/admin/instance/list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
);
|
||||
};
|
||||
/**获取单个指定容器 */
|
||||
export const getOneContainer = data => {
|
||||
return http2.post("/v1/admin/container/get_container_detail", data, {
|
||||
return http2.post("/acs/v1/admin/container/get_container_detail", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -117,11 +117,11 @@ export const getOneContainer = data => {
|
||||
};
|
||||
/**查询指定虚拟机信息(管理员查询) */
|
||||
export const getVmAdminContainer = id => {
|
||||
return http2.get(`/v1/admin/instance/detail/${id}`);
|
||||
return http2.get(`/acs/v1/admin/instance/detail/${id}`);
|
||||
};
|
||||
// 暂停容器
|
||||
export const pauseContainer = data => {
|
||||
return http2.post("/v1/admin/container/pause_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/pause_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export const pauseContainer = data => {
|
||||
};
|
||||
// 暂停虚拟机
|
||||
export const pauseInstance = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/pause/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/pause/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export const pauseInstance = (data, id) => {
|
||||
};
|
||||
/**恢复虚拟机 */
|
||||
export const unpauseInstance = (id, data = "") => {
|
||||
return http2.post(`/v1/admin/instance/resume/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/resume/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export const unpauseInstance = (id, data = "") => {
|
||||
|
||||
// 解除暂停
|
||||
export const unpauseContainer = data => {
|
||||
return http2.post("/v1/admin/container/unpause_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/unpause_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export const unpauseContainer = data => {
|
||||
};
|
||||
/**获取容器状态 */
|
||||
export const getContainerStatus = data => {
|
||||
return http2.post("/v1/admin/container/get_container_status", data, {
|
||||
return http2.post("/acs/v1/admin/container/get_container_status", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -162,15 +162,15 @@ export const getContainerStatus = data => {
|
||||
};
|
||||
/**获取虚拟机状态 */
|
||||
export const getInstanceStatus = id => {
|
||||
return http2.get(`/v1/admin/instance/get_state/${id}`);
|
||||
return http2.get(`/acs/v1/admin/instance/get_state/${id}`);
|
||||
};
|
||||
/**查询服务器状态 */
|
||||
export const getServerStatus = id => {
|
||||
return http2.get(`/v1/admin/server/send_server_status?server_id=${id}`);
|
||||
return http2.get(`/acs/v1/admin/server/send_server_status?server_id=${id}`);
|
||||
};
|
||||
/**开通容器 */
|
||||
export const openContainer = data => {
|
||||
return http2.post("/v1/admin/container/open_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/open_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -178,7 +178,7 @@ export const openContainer = data => {
|
||||
};
|
||||
/**开通虚拟机 */
|
||||
export const openInstance = (id, data = "") => {
|
||||
return http2.post(`/v1/admin/instance/approve/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/approve/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export const openInstance = (id, data = "") => {
|
||||
};
|
||||
/**启动容器 */
|
||||
export const startContainer = data => {
|
||||
return http2.post("/v1/admin/container/start_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/start_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -194,11 +194,11 @@ export const startContainer = data => {
|
||||
};
|
||||
/**启动虚拟机 */
|
||||
export const startInstance = data => {
|
||||
return http2.get(`/v1/admin/instance/start/${data}`);
|
||||
return http2.get(`/acs/v1/admin/instance/start/${data}`);
|
||||
};
|
||||
/**重装容器 */
|
||||
export const reinstallC = data => {
|
||||
return http2.post("/v1/admin/container/reinstall_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/reinstall_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export const reinstallC = data => {
|
||||
};
|
||||
/**重装虚拟机 */
|
||||
export const reinstallI = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/reinstall/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/reinstall/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -215,7 +215,7 @@ export const reinstallI = (data, id) => {
|
||||
|
||||
/**获取容器日志 */
|
||||
export const getContainerLog = data => {
|
||||
return http2.post(`/v1/admin/container/get_container_log`, data, {
|
||||
return http2.post(`/acs/v1/admin/container/get_container_log`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -224,13 +224,13 @@ export const getContainerLog = data => {
|
||||
/**获取虚拟机操作日志 */
|
||||
export const getInstanceLog = (id, data) => {
|
||||
return http2.get(
|
||||
`/v1/admin/instance/log/${id}?page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/admin/instance/log/${id}?page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
|
||||
/**重启容器 */
|
||||
export const restartContainer = data => {
|
||||
return http2.post("/v1/admin/container/reboot_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/reboot_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -238,12 +238,12 @@ export const restartContainer = data => {
|
||||
};
|
||||
/**重启虚拟机 */
|
||||
export const restartInstance = data => {
|
||||
return http2.get(`/v1/admin/instance/reboot/${data}`);
|
||||
return http2.get(`/acs/v1/admin/instance/reboot/${data}`);
|
||||
};
|
||||
|
||||
/**停止容器 */
|
||||
export const stopContainer = data => {
|
||||
return http2.post("/v1/admin/container/stop_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/stop_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -251,12 +251,12 @@ export const stopContainer = data => {
|
||||
};
|
||||
/**停止虚拟机 */
|
||||
export const stopInstance = data => {
|
||||
return http2.get(`/v1/admin/instance/stop/${data}`);
|
||||
return http2.get(`/acs/v1/admin/instance/stop/${data}`);
|
||||
};
|
||||
|
||||
/**删除容器 */
|
||||
export const deleteContainer = data => {
|
||||
return http2.post("/v1/admin/container/delete_container", data, {
|
||||
return http2.post("/acs/v1/admin/container/delete_container", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export const deleteContainer = data => {
|
||||
};
|
||||
/**删除虚拟机 */
|
||||
export const deleteInstance = (id, data = "") => {
|
||||
return http2.post(`/v1/admin/instance/delete/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/delete/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -272,7 +272,7 @@ export const deleteInstance = (id, data = "") => {
|
||||
};
|
||||
/**清除容器流量 */
|
||||
export const clearContainerTraffic = data => {
|
||||
return http2.post("/v1/admin/container/clear_container_traffic", data, {
|
||||
return http2.post("/acs/v1/admin/container/clear_container_traffic", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export const clearContainerTraffic = data => {
|
||||
};
|
||||
/**连接控制台 */
|
||||
export const connectConsole = data => {
|
||||
return http2.post("/v1/admin/container/get_container_console", data, {
|
||||
return http2.post("/acs/v1/admin/container/get_container_console", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export const connectConsole = data => {
|
||||
};
|
||||
/**新增虚拟机 (管理员) */
|
||||
export const addInstance = data => {
|
||||
return http2.post("/v1/admin/instance/create_vm", data, {
|
||||
return http2.post("/acs/v1/admin/instance/create_vm", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -296,21 +296,21 @@ export const addInstance = data => {
|
||||
};
|
||||
/**获取虚拟机控制台 */
|
||||
export const getInstanceConsole = data => {
|
||||
return http2.get(`/v1/admin/instance/console/${data}`);
|
||||
return http2.get(`/acs/v1/admin/instance/console/${data}`);
|
||||
};
|
||||
/**查询容器所有卷信息 */
|
||||
export const getVolumeList = data => {
|
||||
return http2.get(`/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`);
|
||||
return http2.get(`/acs/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`);
|
||||
};
|
||||
/**查询虚拟机所有卷信息 */
|
||||
export const getInstanceVolumeList = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
/**新增卷 */
|
||||
export const addVolume = data => {
|
||||
return http2.post("/v1/admin/volume/add_volume", data, {
|
||||
return http2.post("/acs/v1/admin/volume/add_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export const addVolume = data => {
|
||||
};
|
||||
/**修改卷大小 */
|
||||
export const updateVolume = data => {
|
||||
return http2.post("/v1/admin/volume/update_volume_size", data, {
|
||||
return http2.post("/acs/v1/admin/volume/update_volume_size", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -326,7 +326,7 @@ export const updateVolume = data => {
|
||||
};
|
||||
/**删除数据卷 */
|
||||
export const deleteVolume = data => {
|
||||
return http2.post("/v1/admin/volume/delete_volume", data, {
|
||||
return http2.post("/acs/v1/admin/volume/delete_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -336,7 +336,7 @@ export const deleteVolume = data => {
|
||||
/**获取容器网络信息 */
|
||||
export const getNetworkList = data => {
|
||||
return http2.get(
|
||||
`/v1/container/proxy/get_container_proxy?container_id=${data}`
|
||||
`/acs/v1/container/proxy/get_container_proxy?container_id=${data}`
|
||||
);
|
||||
};
|
||||
/**获取虚拟机端口列表 */
|
||||
@@ -347,12 +347,12 @@ export const getInstancePortList = data => {
|
||||
if (data.internal_port !== undefined)
|
||||
params.append("internal_port", data.internal_port.toString());
|
||||
return http2.get(
|
||||
`/v1/admin/instance_port/list?instance_id=${data.instance_id}&${params.toString()}`
|
||||
`/acs/v1/admin/instance_port/list?instance_id=${data.instance_id}&${params.toString()}`
|
||||
);
|
||||
};
|
||||
/**添加容器网络 */
|
||||
export const addNetwork = data => {
|
||||
return http2.post("/v1/container/proxy/add_container_proxy", data, {
|
||||
return http2.post("/acs/v1/container/proxy/add_container_proxy", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -360,7 +360,7 @@ export const addNetwork = data => {
|
||||
};
|
||||
/**创建端口 */
|
||||
export const addPort = data => {
|
||||
return http2.post("/v1/admin/instance_port/create", data, {
|
||||
return http2.post("/acs/v1/admin/instance_port/create", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -369,12 +369,12 @@ export const addPort = data => {
|
||||
/**获取浮动ip列表 */
|
||||
export const getFloatingIpList = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/floating_ip/get_list?server_id=${data.server_id}&page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/admin/floating_ip/get_list?server_id=${data.server_id}&page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
/**新增浮动ip */
|
||||
export const addFloatingIp = data => {
|
||||
return http2.post("/v1/admin/floating_ip/add", data, {
|
||||
return http2.post("/acs/v1/admin/floating_ip/add", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -382,7 +382,7 @@ export const addFloatingIp = data => {
|
||||
};
|
||||
/**批量添加浮动ip */
|
||||
export const addFloatingIpBatch = data => {
|
||||
return http2.post("/v1/admin/floating_ip/add_list", data, {
|
||||
return http2.post("/acs/v1/admin/floating_ip/add_list", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -390,7 +390,7 @@ export const addFloatingIpBatch = data => {
|
||||
};
|
||||
/**删除浮动ip */
|
||||
export const delFloatingIp = data => {
|
||||
return http2.post("/v1/admin/floating_ip/delete", data, {
|
||||
return http2.post("/acs/v1/admin/floating_ip/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -400,13 +400,13 @@ export const delFloatingIp = data => {
|
||||
/**获取单个用户操作日志 */
|
||||
export const getUserLog = data => {
|
||||
return http2.get(
|
||||
`/v1/user/procedure/get_user_log?user_id=${data.user_id}&page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/user/procedure/get_user_log?user_id=${data.user_id}&page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
|
||||
/**管理员修改头像 */
|
||||
export const editAvatar = data => {
|
||||
return http2.post("/v1/admin/users/upload_user_avatar", data, {
|
||||
return http2.post("/acs/v1/admin/users/upload_user_avatar", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -415,28 +415,28 @@ export const editAvatar = data => {
|
||||
|
||||
/**获取服务器硬盘信息 */
|
||||
export const getDiskInfo = data => {
|
||||
return http2.get(`/v1/admin/server/get_server_disk?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/server/get_server_disk?server_id=${data}`);
|
||||
};
|
||||
/**获取服务器实际划分硬盘信息 */
|
||||
export const getRealDisk = data => {
|
||||
return http2.get(`/v1/admin/server/get_server_disk_info?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/server/get_server_disk_info?server_id=${data}`);
|
||||
};
|
||||
/**获取服务器流量信息 */
|
||||
export const getTraffic = data => {
|
||||
return http2.get(`/v1/admin/server/get_server_bandwidth?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/server/get_server_bandwidth?server_id=${data}`);
|
||||
};
|
||||
/**获取服务器总流量信息 */
|
||||
export const getTotalTraffic = data => {
|
||||
return http2.get(`/v1/admin/server/get_server_total_bandwidth?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/server/get_server_total_bandwidth?server_id=${data}`);
|
||||
};
|
||||
/**获取版本更新 */
|
||||
export const getVersion = () => {
|
||||
return http2.get(`/v1/admin/version`);
|
||||
return http2.get(`/acs/v1/admin/version`);
|
||||
};
|
||||
|
||||
// 管理员删除https网络
|
||||
export const AdminDelHttps = data => {
|
||||
return http2.post("/v1/container/proxy/del_https_connet", data, {
|
||||
return http2.post("/acs/v1/container/proxy/del_https_connet", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -445,7 +445,7 @@ export const AdminDelHttps = data => {
|
||||
|
||||
// 管理员添加https网络
|
||||
export const AdminAddHttps = data => {
|
||||
return http2.post("/v1/container/proxy/add_https_proxy", data, {
|
||||
return http2.post("/acs/v1/container/proxy/add_https_proxy", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -453,11 +453,11 @@ export const AdminAddHttps = data => {
|
||||
};
|
||||
/**获取指定端口信息 */
|
||||
export const getPortInfo = data => {
|
||||
return http2.get(`/v1/admin/instance_port/detail?port_id=${data}`);
|
||||
return http2.get(`/acs/v1/admin/instance_port/detail?port_id=${data}`);
|
||||
};
|
||||
/**新增卷 */
|
||||
export const addVolumeMount = data => {
|
||||
return http2.post("/v1/admin/volume/add_volume", data, {
|
||||
return http2.post("/acs/v1/admin/volume/add_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -466,17 +466,17 @@ export const addVolumeMount = data => {
|
||||
|
||||
/**进入救援系统 */
|
||||
export const rescueInstance = id => {
|
||||
return http2.get(`/v1/admin/instance/rescue/enter/${id}`);
|
||||
return http2.get(`/acs/v1/admin/instance/rescue/enter/${id}`);
|
||||
};
|
||||
|
||||
/**退出救援系统 */
|
||||
export const exitRescueInstance = id => {
|
||||
return http2.get(`/v1/admin/instance/rescue/exit/${id}`);
|
||||
return http2.get(`/acs/v1/admin/instance/rescue/exit/${id}`);
|
||||
};
|
||||
|
||||
/**修改虚拟机密码 */
|
||||
export const changeInstancePassword = (id, data) => {
|
||||
return http2.post(`/v1/admin/instance/update_password/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/update_password/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -484,7 +484,7 @@ export const changeInstancePassword = (id, data) => {
|
||||
};
|
||||
/**修改虚拟机密码(用户) */
|
||||
export const changeInstancePasswordUser = (id, data) => {
|
||||
return http2.post(`/v1/user/instance/update_password/${id}`, data, {
|
||||
return http2.post(`/acs/v1/user/instance/update_password/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -493,7 +493,7 @@ export const changeInstancePasswordUser = (id, data) => {
|
||||
|
||||
/**删除端口 */
|
||||
export const deletePort = data => {
|
||||
return http2.post("/v1/admin/instance_port/delete", data, {
|
||||
return http2.post("/acs/v1/admin/instance_port/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取全局配置 */
|
||||
export const getSetting = () => {
|
||||
return http2.get('/v1/admin/settings/get_settings')
|
||||
return http2.get('/acs/v1/admin/settings/get_settings')
|
||||
}
|
||||
/**变更设置 */
|
||||
export const updateSetting = (data) => {
|
||||
return http2.post('/v1/admin/settings/update_settings', data, {
|
||||
return http2.post('/acs/v1/admin/settings/update_settings', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const updateSetting = (data) => {
|
||||
}
|
||||
/**新增设置 */
|
||||
export const addSetting = (data) => {
|
||||
return http2.post('/v1/admin/settings/add_settings', data, {
|
||||
return http2.post('/acs/v1/admin/settings/add_settings', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const addSetting = (data) => {
|
||||
}
|
||||
/**删除设置 */
|
||||
export const deleteSetting = (data) => {
|
||||
return http2.post('/v1/admin/settings/delete_settings', data,{
|
||||
return http2.post('/acs/v1/admin/settings/delete_settings', data,{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -29,12 +29,12 @@ export const deleteSetting = (data) => {
|
||||
}
|
||||
/**获取单项配置 */
|
||||
export const getOneSetting = (data) => {
|
||||
return http2.get(`/v1/admin/settings/get_setting?name=${data}`)
|
||||
return http2.get(`/acs/v1/admin/settings/get_setting?name=${data}`)
|
||||
}
|
||||
/**获取多个配置 */
|
||||
export const getSettings = (data) => {
|
||||
// return http2.get(`/v1/admin/settings/get_settings?names=${data}`);
|
||||
// return http2.get(`/acs/v1/admin/settings/get_settings?names=${data}`);
|
||||
const namesParam = data.join(',');
|
||||
// 将处理后的namesParam放入URL中
|
||||
return http2.get(`/v1/admin/settings/get_setting?names=${encodeURIComponent(namesParam)}`);
|
||||
return http2.get(`/acs/v1/admin/settings/get_setting?names=${encodeURIComponent(namesParam)}`);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取用户列表 */
|
||||
export const ask_update_user_email11 = data => {
|
||||
return http2.get(`/v1/user/info/ask_update_user_email?email=${data.email}`);
|
||||
return http2.get(`/acs/v1/user/info/ask_update_user_email?email=${data.email}`);
|
||||
};
|
||||
/**email验证码 */
|
||||
export const ask_update_user_email = data => {
|
||||
return http2.post("/v1/user/info/ask_update_user_email", data, {
|
||||
return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const ask_update_user_email = data => {
|
||||
};
|
||||
/**email修改 */
|
||||
export const update_user_email = data => {
|
||||
return http2.post("/v1/user/info/update_user_email", data, {
|
||||
return http2.post("/acs/v1/user/info/update_user_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const update_user_email = data => {
|
||||
};
|
||||
/**phone验证码 */
|
||||
export const ask_update_user_phone = data => {
|
||||
return http2.post("/v1/user/info/ask_update_user_phone", data, {
|
||||
return http2.post("/acs/v1/user/info/ask_update_user_phone", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const ask_update_user_phone = data => {
|
||||
};
|
||||
/**phone修改 */
|
||||
export const update_user_phone = data => {
|
||||
return http2.post("/v1/user/info/update_user_phone", data, {
|
||||
return http2.post("/acs/v1/user/info/update_user_phone", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const update_user_phone = data => {
|
||||
};
|
||||
/**密码修改 */
|
||||
export const update_user_password = data => {
|
||||
return http2.post("/v1/user/info/update_user_password", data, {
|
||||
return http2.post("/acs/v1/user/info/update_user_password", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
+67
-67
@@ -4,12 +4,12 @@ import {http2} from "@/utils/request.js";
|
||||
|
||||
// 获取图像验证码
|
||||
export const Captch = data => {
|
||||
return http2.get(`/v1/user/check/get_code_img`);
|
||||
return http2.get(`/acs/v1/user/check/get_code_img`);
|
||||
};
|
||||
|
||||
/** 登录 */
|
||||
export const getLogin = data => {
|
||||
return http2.post("/v1/user/login", data, {
|
||||
return http2.post("/acs/v1/user/login", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -24,12 +24,12 @@ export const getLogin = data => {
|
||||
/**获取用户列表 */
|
||||
export const getUserList = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/users/get_user_list?page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
`/acs/v1/admin/users/get_user_list?page=${data.page}&count=${data.count}&key=${data.key}`
|
||||
);
|
||||
};
|
||||
/**添加用户 */
|
||||
export const addUser = data => {
|
||||
return http2.post("/v1/admin/users/add_user", data, {
|
||||
return http2.post("/acs/v1/admin/users/add_user", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const addUser = data => {
|
||||
};
|
||||
/**编辑用户信息 */
|
||||
export const editUser = data => {
|
||||
return http2.post("/v1/admin/users/update_user", data, {
|
||||
return http2.post("/acs/v1/admin/users/update_user", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const editUser = data => {
|
||||
};
|
||||
/**修改用户密码 */
|
||||
export const editPassword = data => {
|
||||
return http2.post("/v1/admin/users/update_user_password", data, {
|
||||
return http2.post("/acs/v1/admin/users/update_user_password", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const editPassword = data => {
|
||||
};
|
||||
/**删除用户 */
|
||||
export const deleteUser = data => {
|
||||
return http2.post("/v1/admin/users/del_user", data, {
|
||||
return http2.post("/acs/v1/admin/users/del_user", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export const deleteUser = data => {
|
||||
};
|
||||
/**查询单个用户 */
|
||||
export const userDetail = data => {
|
||||
return http2.post("/v1/admin/users/select_user", data, {
|
||||
return http2.post("/acs/v1/admin/users/select_user", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export const userDetail = data => {
|
||||
};
|
||||
/**修改用户余额 */
|
||||
export const editBalance = data => {
|
||||
return http2.post("/v1/admin/users/update_user_balance", data, {
|
||||
return http2.post("/acs/v1/admin/users/update_user_balance", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -79,12 +79,12 @@ export const editBalance = data => {
|
||||
export const getUserServer = (data = {}) => {
|
||||
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
||||
return http2.get(
|
||||
`/v1/user/procedure/get_server_list?server_type=${serverType}`
|
||||
`/acs/v1/user/procedure/get_server_list?server_type=${serverType}`
|
||||
);
|
||||
};
|
||||
/**用户获取虚拟机列表 */
|
||||
export const getVirtualList = data => {
|
||||
let url = `/v1/user/instance/list?page=${data.page}&count=${data.count}`;
|
||||
let url = `/acs/v1/user/instance/list?page=${data.page}&count=${data.count}`;
|
||||
if (data.key) {
|
||||
url += `&key=${data.key}`;
|
||||
}
|
||||
@@ -95,35 +95,35 @@ export const getVirtualList = data => {
|
||||
};
|
||||
/**用户获取服务器套餐 */
|
||||
export const getUserPackage = data => {
|
||||
return http2.get(`/v1/user/procedure/get_server_plan_list?server_id=${data}`);
|
||||
return http2.get(`/acs/v1/user/procedure/get_server_plan_list?server_id=${data}`);
|
||||
};
|
||||
/**获取用户容器列表 */
|
||||
export const getUserContainer = data => {
|
||||
return http2.get(
|
||||
`/v1/user/container/list?page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/user/container/list?page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
/**用户按地区获取容器 */
|
||||
export const getUserContainerD = data => {
|
||||
return http2.get(
|
||||
`/v1/user/container/list?page=${data.page}&count=${data.count}&server_id=${data.server_id}`
|
||||
`/acs/v1/user/container/list?page=${data.page}&count=${data.count}&server_id=${data.server_id}`
|
||||
);
|
||||
};
|
||||
/**获取用户操作日志 */
|
||||
export const get_user_log = () => {
|
||||
return http2.get(`/v1/user/procedure/get_user_log`);
|
||||
return http2.get(`/acs/v1/user/procedure/get_user_log`);
|
||||
};
|
||||
/**获取用户自身信息 */
|
||||
export const getUserInfo = () => {
|
||||
return http2.get(`/v1/user/procedure/get_user_info`);
|
||||
return http2.get(`/acs/v1/user/procedure/get_user_info`);
|
||||
};
|
||||
/**获取用户自身信息 */
|
||||
export const getUserInfoV1 = () => {
|
||||
return http2.get(`/v1/user/info/get_user_info`);
|
||||
return http2.get(`/acs/v1/user/info/get_user_info`);
|
||||
};
|
||||
/**用户实名 */
|
||||
export const realName = data => {
|
||||
return http2.post("/v1/external/real_name", data, {
|
||||
return http2.post("/acs/v1/external/real_name", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export const realName = data => {
|
||||
};
|
||||
/**发送手机验证码 */
|
||||
export const sendCode = data => {
|
||||
return http2.post("/v1/external/send_message", data, {
|
||||
return http2.post("/acs/v1/external/send_message", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export const sendCode = data => {
|
||||
};
|
||||
/**发送邮箱验证码 */
|
||||
export const sendEmailCode = data => {
|
||||
return http2.post("/v1/external/send_email", data, {
|
||||
return http2.post("/acs/v1/external/send_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export const sendEmailCode = data => {
|
||||
};
|
||||
/**用户注册 */
|
||||
export const register = data => {
|
||||
return http2.post("/v1/user/register", data, {
|
||||
return http2.post("/acs/v1/user/register", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export const register = data => {
|
||||
};
|
||||
/**手机号修改校证码 */
|
||||
export const CodePhone = data => {
|
||||
return http2.post("/v1/user/info/ask_update_user_phone", data, {
|
||||
return http2.post("/acs/v1/user/info/ask_update_user_phone", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export const CodePhone = data => {
|
||||
};
|
||||
/**修改手机号码 */
|
||||
export const SetPhone = data => {
|
||||
return http2.post("/v1/user/info/update_user_phone", data, {
|
||||
return http2.post("/acs/v1/user/info/update_user_phone", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export const SetPhone = data => {
|
||||
};
|
||||
/**邮箱修改校证码 */
|
||||
export const CodeEmail = data => {
|
||||
return http2.post("/v1/user/info/ask_update_user_email", data, {
|
||||
return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -179,7 +179,7 @@ export const CodeEmail = data => {
|
||||
};
|
||||
/**修改邮箱 */
|
||||
export const SetEmail = data => {
|
||||
return http2.post("/v1/user/info/update_user_email", data, {
|
||||
return http2.post("/acs/v1/user/info/update_user_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export const SetEmail = data => {
|
||||
};
|
||||
/**上传头像 */
|
||||
export const uploadAvatar = data => {
|
||||
return http2.post("/v1/user/info/upload_user_avatar", data, {
|
||||
return http2.post("/acs/v1/user/info/upload_user_avatar", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -195,7 +195,7 @@ export const uploadAvatar = data => {
|
||||
};
|
||||
/**手机号忘记密码 */
|
||||
export const forgetphone = data => {
|
||||
return http2.post("/v1/user/info/forget_user_password_phone", data, {
|
||||
return http2.post("/acs/v1/user/info/forget_user_password_phone", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export const forgetphone = data => {
|
||||
|
||||
/**邮箱忘记密码 */
|
||||
export const forgetemail = data => {
|
||||
return http2.post("/v1/user/info/forget_user_password_email", data, {
|
||||
return http2.post("/acs/v1/user/info/forget_user_password_email", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export const forgetemail = data => {
|
||||
};
|
||||
/**管理员全局搜索 */
|
||||
export const Find = data => {
|
||||
return http2.post("/v1/admin/search", data, {
|
||||
return http2.post("/acs/v1/admin/search", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -220,7 +220,7 @@ export const Find = data => {
|
||||
};
|
||||
// 管理员删除容器网络
|
||||
export const delContainer = data => {
|
||||
return http2.post("/v1/user/container/delete_connect", data, {
|
||||
return http2.post("/acs/v1/user/container/delete_connect", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -228,7 +228,7 @@ export const delContainer = data => {
|
||||
};
|
||||
// 删除端口
|
||||
export const delPort = data => {
|
||||
return http2.post("/v1/admin/instance_port/delete", data, {
|
||||
return http2.post("/acs/v1/admin/instance_port/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export const delPort = data => {
|
||||
};
|
||||
// 自定义容器价格
|
||||
export const Containerpay = data => {
|
||||
return http2.post("/v1/admin/container/update_container_price", data, {
|
||||
return http2.post("/acs/v1/admin/container/update_container_price", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -244,7 +244,7 @@ export const Containerpay = data => {
|
||||
};
|
||||
// 修改虚拟机续费价格
|
||||
export const Containerpaytime = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/update_price/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/update_price/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export const Containerpaytime = (data, id) => {
|
||||
};
|
||||
// 自定义容器到期时间
|
||||
export const Containertime = data => {
|
||||
return http2.post("/v1/admin/container/update_container_expire_time", data, {
|
||||
return http2.post("/acs/v1/admin/container/update_container_expire_time", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -260,7 +260,7 @@ export const Containertime = data => {
|
||||
};
|
||||
// 修改虚拟机续到期时间
|
||||
export const Containertimetime = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/update_expire_time/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/update_expire_time/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -268,7 +268,7 @@ export const Containertimetime = (data, id) => {
|
||||
};
|
||||
// 修改虚拟机信息
|
||||
export const editContainer = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/update/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/update/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export const editContainer = (data, id) => {
|
||||
/** 容器操作 */
|
||||
export const startUserContainer = (type, id) => {
|
||||
return http2.post(
|
||||
"/v1/user/container/" + type,
|
||||
"/acs/v1/user/container/" + type,
|
||||
{
|
||||
container_id: id
|
||||
},
|
||||
@@ -293,7 +293,7 @@ export const startUserContainer = (type, id) => {
|
||||
};
|
||||
/**用户容器退款 */
|
||||
export const backUserContainer = data => {
|
||||
return http2.post("/v1/user/container/delete", data, {
|
||||
return http2.post("/acs/v1/user/container/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -301,7 +301,7 @@ export const backUserContainer = data => {
|
||||
};
|
||||
/**重装容器 */
|
||||
export const reinContainer = data => {
|
||||
return http2.post("/v1/user/container/reinstall", data, {
|
||||
return http2.post("/acs/v1/user/container/reinstall", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -309,7 +309,7 @@ export const reinContainer = data => {
|
||||
};
|
||||
/**重装虚拟机 */
|
||||
export const reinVmContainer = (id, data) => {
|
||||
return http2.post(`/v1/user/instance/reinstall/${id}`, data, {
|
||||
return http2.post(`/acs/v1/user/instance/reinstall/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export const reinVmContainer = (id, data) => {
|
||||
/** 容器操作 */
|
||||
export const startAdminContainer = (type, id) => {
|
||||
return http2.post(
|
||||
"/v1/admin/container/" + type,
|
||||
"/acs/v1/admin/container/" + type,
|
||||
{
|
||||
container_id: id
|
||||
},
|
||||
@@ -331,7 +331,7 @@ export const startAdminContainer = (type, id) => {
|
||||
};
|
||||
/** 容器操作 */
|
||||
export const procedureUpdateContainerRenew = data => {
|
||||
return http2.post("/v1/user/procedure/update_container_renew", data, {
|
||||
return http2.post("/acs/v1/user/procedure/update_container_renew", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -339,15 +339,15 @@ export const procedureUpdateContainerRenew = data => {
|
||||
};
|
||||
/**获取容器完整信息 */
|
||||
export const getContainerDetail = id => {
|
||||
return http2.get(`/v1/user/container/detail?container_id=${id}`);
|
||||
return http2.get(`/acs/v1/user/container/detail?container_id=${id}`);
|
||||
};
|
||||
/**获取虚拟机完整信息 */
|
||||
export const getVmContainerDetail = id => {
|
||||
return http2.get(`/v1/user/instance/detail/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/detail/${id}`);
|
||||
};
|
||||
/**容器操作信息 */
|
||||
export const containerLog = data => {
|
||||
return http2.post("/v1/user/container/logs", data, {
|
||||
return http2.post("/acs/v1/user/container/logs", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -356,12 +356,12 @@ export const containerLog = data => {
|
||||
/**虚拟机操作日志 */
|
||||
export const vmLog = data => {
|
||||
return http2.get(
|
||||
`/v1/user/instance/log/${data.id}?page=${data.page}&count=${data.count}`
|
||||
`/acs/v1/user/instance/log/${data.id}?page=${data.page}&count=${data.count}`
|
||||
);
|
||||
};
|
||||
/**获取容器状态 */
|
||||
export const getContainerStatus = data => {
|
||||
return http2.post(`/v1/user/container/status`, data, {
|
||||
return http2.post(`/acs/v1/user/container/status`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -369,11 +369,11 @@ export const getContainerStatus = data => {
|
||||
};
|
||||
/**获取虚拟机状态 */
|
||||
export const getVmStatus = id => {
|
||||
return http2.get(`/v1/user/instance/get_state/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/get_state/${id}`);
|
||||
};
|
||||
/**获取容器运行日志 */
|
||||
export const getContainerLog = data => {
|
||||
return http2.post(`/v1/user/container/run_logs`, data, {
|
||||
return http2.post(`/acs/v1/user/container/run_logs`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -381,7 +381,7 @@ export const getContainerLog = data => {
|
||||
};
|
||||
/**获取容器购买网络订单 */
|
||||
export const getContainerList = data => {
|
||||
return http2.post(`/v1/user/procedure/add_network`, data, {
|
||||
return http2.post(`/acs/v1/user/procedure/add_network`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -389,7 +389,7 @@ export const getContainerList = data => {
|
||||
};
|
||||
/**计算容器网络价格 */
|
||||
export const getContainerPrice = data => {
|
||||
return http2.post(`/v1/user/procedure/get_price_network`, data, {
|
||||
return http2.post(`/acs/v1/user/procedure/get_price_network`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -397,33 +397,33 @@ export const getContainerPrice = data => {
|
||||
};
|
||||
/** 启动虚拟机 */
|
||||
export const start_vm = id => {
|
||||
return http2.get(`/v1/user/instance/start/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/start/${id}`);
|
||||
};
|
||||
/** 停止虚拟机(关机) */
|
||||
export const stop_vm = id => {
|
||||
return http2.get(`/v1/user/instance/stop/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/stop/${id}`);
|
||||
};
|
||||
/**重启虚拟机 */
|
||||
export const restart_vm = id => {
|
||||
return http2.get(`/v1/user/instance/reboot/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/reboot/${id}`);
|
||||
};
|
||||
/**获取虚拟机控制台 */
|
||||
export const get_vm_console = id => {
|
||||
return http2.get(`/v1/user/instance/console/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/console/${id}`);
|
||||
};
|
||||
/**进入救援系统 */
|
||||
export const rescue_vm = id => {
|
||||
return http2.get(`/v1/user/instance/rescue/enter/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/rescue/enter/${id}`);
|
||||
};
|
||||
/**退出救援系统 */
|
||||
export const unrescue_vm = id => {
|
||||
return http2.get(`/v1/user/instance/rescue/exit/${id}`);
|
||||
return http2.get(`/acs/v1/user/instance/rescue/exit/${id}`);
|
||||
};
|
||||
|
||||
// ******************************* new
|
||||
/** 提交充值订单 */
|
||||
export const user_update_container_recharge = data => {
|
||||
return http2.post("/v1/user/procedure/update_container_recharge", data, {
|
||||
return http2.post("/acs/v1/user/procedure/update_container_recharge", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -431,7 +431,7 @@ export const user_update_container_recharge = data => {
|
||||
};
|
||||
/** 提交容器订单 */
|
||||
export const user_update_plan_info = data => {
|
||||
return http2.post("/v1/user/procedure/update_plan_info", data, {
|
||||
return http2.post("/acs/v1/user/procedure/update_plan_info", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -439,7 +439,7 @@ export const user_update_plan_info = data => {
|
||||
};
|
||||
/** 提交虚拟机订单 */
|
||||
export const user_update_vm_info = data => {
|
||||
return http2.post("/v1/user/procedure/create_vm_trade", data, {
|
||||
return http2.post("/acs/v1/user/procedure/create_vm_trade", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -447,11 +447,11 @@ export const user_update_vm_info = data => {
|
||||
};
|
||||
/**获取订单简略信息 */
|
||||
export const getOrderDetail = id => {
|
||||
return http2.get(`/v1/user/procedure/get_low_trade_info?trade_id=${id}`);
|
||||
return http2.get(`/acs/v1/user/procedure/get_low_trade_info?trade_id=${id}`);
|
||||
};
|
||||
/**支付请求 */
|
||||
export const pay_request = data => {
|
||||
return http2.post("/v1/external/pay", data, {
|
||||
return http2.post("/acs/v1/external/pay", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -459,7 +459,7 @@ export const pay_request = data => {
|
||||
};
|
||||
/**用户删除容器网络 */
|
||||
export const deleteConNet = data => {
|
||||
return http2.post("/v1/user/container/delete_connect", data, {
|
||||
return http2.post("/acs/v1/user/container/delete_connect", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -468,7 +468,7 @@ export const deleteConNet = data => {
|
||||
|
||||
// 添加https
|
||||
export const additionHttp = data => {
|
||||
return http2.post("/v1/user/container/add_https_connet", data, {
|
||||
return http2.post("/acs/v1/user/container/add_https_connet", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -477,7 +477,7 @@ export const additionHttp = data => {
|
||||
|
||||
// 删除https
|
||||
export const DelHttp = data => {
|
||||
return http2.post("/v1/user/container/del_https_connet", data, {
|
||||
return http2.post("/acs/v1/user/container/del_https_connet", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -485,7 +485,7 @@ export const DelHttp = data => {
|
||||
};
|
||||
/**获取新增虚拟机端口价格 */
|
||||
export const getVmPortPrice = data => {
|
||||
return http2.post("/v1/user/procedure/get_price_instance_port", data, {
|
||||
return http2.post("/acs/v1/user/procedure/get_price_instance_port", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -493,7 +493,7 @@ export const getVmPortPrice = data => {
|
||||
};
|
||||
/**提交新增虚拟机端口订单 */
|
||||
export const addVmPort = data => {
|
||||
return http2.post("/v1/user/procedure/add_instance_port", data, {
|
||||
return http2.post("/acs/v1/user/procedure/add_instance_port", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
+22
-22
@@ -3,7 +3,7 @@ import {http2} from "@/utils/request.js";
|
||||
|
||||
/**获取虚拟机列表 */
|
||||
export const getVirtualList = data => {
|
||||
let url = `/v1/admin/instance/list?page=${data.page}&count=${data.count}`;
|
||||
let url = `/acs/v1/admin/instance/list?page=${data.page}&count=${data.count}`;
|
||||
if (data.key) {
|
||||
url += `&key=${data.key}`;
|
||||
}
|
||||
@@ -18,12 +18,12 @@ export const getVirtualList = data => {
|
||||
|
||||
/**新增虚拟机 */
|
||||
export const addVirtual = data => {
|
||||
return http2.post("/v1/admin/instance/create_vm", data);
|
||||
return http2.post("/acs/v1/admin/instance/create_vm", data);
|
||||
};
|
||||
|
||||
/**迁移数据卷 */
|
||||
export const migrate_disk = data => {
|
||||
return http2.post("/v1/admin/volume/migrate_volume", data, {
|
||||
return http2.post("/acs/v1/admin/volume/migrate_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -32,18 +32,18 @@ export const migrate_disk = data => {
|
||||
|
||||
/**获取虚拟机访问控制列表 */
|
||||
export const getVirtualAccessList = data => {
|
||||
let url = `/v1/admin/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
|
||||
let url = `/acs/v1/admin/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
|
||||
return http2.get(url);
|
||||
};
|
||||
/**获取虚拟机访问控制列表(用户) */
|
||||
export const getUserAccessList = data => {
|
||||
let url = `/v1/user/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
|
||||
let url = `/acs/v1/user/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
|
||||
return http2.get(url);
|
||||
};
|
||||
|
||||
/**创建访问控制 */
|
||||
export const createAccessControl = data => {
|
||||
return http2.post("/v1/admin/instance/access_control/create", data, {
|
||||
return http2.post("/acs/v1/admin/instance/access_control/create", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export const createAccessControl = data => {
|
||||
};
|
||||
/**创建访问控制(用户) */
|
||||
export const createUserAccessControl = data => {
|
||||
return http2.post("/v1/user/instance/access_control/create", data, {
|
||||
return http2.post("/acs/v1/user/instance/access_control/create", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export const createUserAccessControl = data => {
|
||||
};
|
||||
/**删除访问控制 */
|
||||
export const deleteAccessControl = data => {
|
||||
return http2.post("/v1/admin/instance/access_control/delete", data, {
|
||||
return http2.post("/acs/v1/admin/instance/access_control/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const deleteAccessControl = data => {
|
||||
};
|
||||
/**删除访问控制(用户) */
|
||||
export const deleteUserAccessControl = data => {
|
||||
return http2.post("/v1/user/instance/access_control/delete", data, {
|
||||
return http2.post("/acs/v1/user/instance/access_control/delete", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -76,17 +76,17 @@ export const deleteUserAccessControl = data => {
|
||||
|
||||
/**获取虚拟机快照列表 */
|
||||
export const getSnapshotList = data => {
|
||||
let url = `/v1/admin/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
|
||||
let url = `/acs/v1/admin/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
|
||||
return http2.get(url);
|
||||
};
|
||||
/**获取虚拟机快照列表(用户) */
|
||||
export const getUserSnapshotList = data => {
|
||||
let url = `/v1/user/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
|
||||
let url = `/acs/v1/user/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
|
||||
return http2.get(url);
|
||||
};
|
||||
/**创建虚拟机快照 */
|
||||
export const createSnapshot = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/snapshot/create/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/snapshot/create/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export const createSnapshot = (data, id) => {
|
||||
};
|
||||
/**创建虚拟机快照(用户) */
|
||||
export const createUserSnapshot = (data, id) => {
|
||||
return http2.post(`/v1/user/instance/snapshot/create/${id}`, data, {
|
||||
return http2.post(`/acs/v1/user/instance/snapshot/create/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export const createUserSnapshot = (data, id) => {
|
||||
|
||||
/**删除虚拟机快照 */
|
||||
export const deleteSnapshot = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/snapshot/delete/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/snapshot/delete/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export const deleteSnapshot = (data, id) => {
|
||||
|
||||
/**恢复虚拟机快照 */
|
||||
export const recoverSnapshot = (data, id) => {
|
||||
return http2.post(`/v1/admin/instance/snapshot/restore/${id}`, data, {
|
||||
return http2.post(`/acs/v1/admin/instance/snapshot/restore/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export const recoverSnapshot = (data, id) => {
|
||||
};
|
||||
/**恢复虚拟机快照(用户) */
|
||||
export const recoverUserSnapshot = (data, id) => {
|
||||
return http2.post(`/v1/user/instance/snapshot/restore/${id}`, data, {
|
||||
return http2.post(`/acs/v1/user/instance/snapshot/restore/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -129,24 +129,24 @@ export const recoverUserSnapshot = (data, id) => {
|
||||
/**获取实时监控 */
|
||||
export const getVirtualLog = data => {
|
||||
return http2.get(
|
||||
`/v1/admin/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
|
||||
`/acs/v1/admin/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
|
||||
);
|
||||
};
|
||||
/**获取实时监控(用户) */
|
||||
export const getUserVirtualLog = data => {
|
||||
return http2.get(
|
||||
`/v1/user/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
|
||||
`/acs/v1/user/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
|
||||
);
|
||||
};
|
||||
|
||||
/**获取新增虚拟机快照数量价格 */
|
||||
export const getSnapshotPrice = data => {
|
||||
return http2.get(`/v1/user/procedure/get_price_snapshot?num=${data}`);
|
||||
return http2.get(`/acs/v1/user/procedure/get_price_snapshot?num=${data}`);
|
||||
};
|
||||
|
||||
/**提交虚拟机购买快照订单 */
|
||||
export const submitSnapshotOrder = data => {
|
||||
return http2.post("/v1/user/procedure/update_container_snapshot", data, {
|
||||
return http2.post("/acs/v1/user/procedure/update_container_snapshot", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export const submitSnapshotOrder = data => {
|
||||
};
|
||||
// 获取购买虚拟机数据卷价格
|
||||
export const getVolumePrice = data => {
|
||||
return http2.post("/v1/user/procedure/get_price_volume", data, {
|
||||
return http2.post("/acs/v1/user/procedure/get_price_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export const getVolumePrice = data => {
|
||||
|
||||
// 提交虚拟机数据卷订单
|
||||
export const submitVolumeOrder = data => {
|
||||
return http2.post("/v1/user/procedure/update_container_volume", data, {
|
||||
return http2.post("/acs/v1/user/procedure/update_container_volume", data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
|
||||
@@ -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": "新加坡"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -93,8 +93,8 @@ class Request {
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
delete(url, config = {}) {
|
||||
return this.instance.delete(url, config)
|
||||
delete(url,data={}, config = {}) {
|
||||
return this.instance.delete(url,data, config)
|
||||
}
|
||||
|
||||
// PATCH 请求
|
||||
@@ -112,7 +112,8 @@ const request = new Request({
|
||||
}
|
||||
})
|
||||
|
||||
export const mainUrl = baseUrl + "/acs"
|
||||
export const mainUrl = baseUrl + '/acs'
|
||||
export const baseURL = baseUrl
|
||||
|
||||
export const http2 = axios.create({
|
||||
baseURL: baseUrl,
|
||||
@@ -132,7 +133,7 @@ http2.interceptors.request.use(config => {
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
config.url = '/acs' + config.url
|
||||
config.url = config.url
|
||||
return config
|
||||
})
|
||||
|
||||
|
||||
+56
-1
@@ -1,4 +1,59 @@
|
||||
export const FileName = (data) =>{
|
||||
let name = data.split("/").pop()
|
||||
return name
|
||||
}
|
||||
}
|
||||
export const formatTime = (time) => {
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
export const formatDate = (dateStr) => {
|
||||
if (!dateStr || dateStr === '0001-01-01T00:00:00Z' || dateStr === null) return '-'
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
/**
|
||||
* 时间格式转 Unix 时间戳(毫秒级)
|
||||
* @param {string|Date} time - 输入时间(支持 '2025-10-28 00:00:00'、'2025/10/28'、Date 对象等)
|
||||
* @returns {number|null} 转换后的毫秒级时间戳(失败返回 null)
|
||||
*/
|
||||
export function timeToTimestamp(time) {
|
||||
let date;
|
||||
|
||||
// 处理字符串格式(如 '2025-10-28 00:00:00' 或 '2025/10/28')
|
||||
if (typeof time === 'string') {
|
||||
// 替换 '-' 为 '/'(避免 Safari 等浏览器对 '-' 格式解析失败)
|
||||
const formattedTime = time.replace(/-/g, '/');
|
||||
date = new Date(formattedTime);
|
||||
}
|
||||
|
||||
// 处理 Date 对象
|
||||
else if (time instanceof Date) {
|
||||
date = time;
|
||||
}
|
||||
|
||||
// 无效输入
|
||||
else {
|
||||
console.error('无效的时间格式,支持字符串(如 "2025-10-28 00:00:00")或 Date 对象');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证时间是否有效
|
||||
const timestamp = date.getTime();
|
||||
if (isNaN(timestamp)) {
|
||||
console.error(`无法解析时间:${time}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000)
|
||||
}
|
||||
|
||||
|
||||
export function reducenum(num){
|
||||
return num / 100
|
||||
}
|
||||
@@ -108,6 +108,7 @@ const handleLogin = () => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
let resp = await userLogin(loginForm.username, loginForm.password)
|
||||
console.log("login:",resp)
|
||||
loading.value = false
|
||||
if(resp.code === 200){
|
||||
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<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="stat-card total-card">
|
||||
@@ -37,79 +25,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<el-input
|
||||
v-model="filterForm.url"
|
||||
placeholder="搜索 Guacamole URL"
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
<div class="filter-actions">
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
||||
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Guacamole 配置列表 -->
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="guacamoleData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
table-layout="auto"
|
||||
class="guacamole-table"
|
||||
>
|
||||
<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="username" label="用户名" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="密码" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="togglePasswordVisibility(scope.row)"
|
||||
:icon="scope.row.showPassword ? View : Hide"
|
||||
>
|
||||
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="filterForm" class="search-form">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="filterForm.url"
|
||||
placeholder="搜索 Guacamole URL"
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<div class="action-buttons">
|
||||
<el-tooltip content="编辑配置" placement="top" :hide-after="1500">
|
||||
<el-button
|
||||
type="warning"
|
||||
:icon="Edit"
|
||||
circle
|
||||
@click="handleEdit(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除配置" placement="top" :hide-after="1500">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
@click="handleDelete(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button @click="handleRefresh">
|
||||
<el-icon><refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<!-- Guacamole 配置列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="guacamoleData"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<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="username" label="用户名" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="密码" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="togglePasswordVisibility(scope.row)"
|
||||
>
|
||||
<el-icon style="margin-right: 4px"><component :is="scope.row.showPassword ? View : Hide" /></el-icon>
|
||||
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
@@ -119,9 +119,10 @@
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑配置对话框 -->
|
||||
<el-dialog
|
||||
@@ -510,42 +511,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.guacamole-container {
|
||||
padding: 20px;
|
||||
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;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
@@ -558,18 +524,18 @@ onMounted(async () => {
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
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 {
|
||||
@@ -608,60 +574,64 @@ onMounted(async () => {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.1;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 搜索和筛选部分 */
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.guacamole-table {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
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;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
/* 对话框底部 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
@@ -696,6 +658,37 @@ onMounted(async () => {
|
||||
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) {
|
||||
.stats-panel {
|
||||
@@ -708,17 +701,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
@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 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -727,13 +709,18 @@ onMounted(async () => {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
.filter-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: none;
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,59 @@
|
||||
<template>
|
||||
<div class="container-images-container" v-loading="loading">
|
||||
<div class="page-header">
|
||||
<h2>容器镜像</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleRefresh">
|
||||
<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-item label="服务器">
|
||||
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
||||
<el-option
|
||||
v-for="server in serverList"
|
||||
:key="server.server_id"
|
||||
:label="server.name"
|
||||
:value="server.server_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="镜像名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 当前服务器镜像列表 -->
|
||||
<div v-if="currentServer" class="server-section">
|
||||
<div class="server-header">
|
||||
<h3>{{ currentServer.name }}</h3>
|
||||
<div class="server-actions">
|
||||
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
|
||||
<el-icon><plus /></el-icon>上传镜像
|
||||
</el-button>
|
||||
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
|
||||
<el-icon><refresh /></el-icon>同步镜像
|
||||
</el-button>
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="服务器">
|
||||
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
||||
<el-option
|
||||
v-for="server in serverList"
|
||||
:key="server.server_id"
|
||||
:label="server.name"
|
||||
:value="server.server_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="镜像名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleRefresh">
|
||||
<el-icon><refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="table-card">
|
||||
<!-- 当前服务器镜像列表 -->
|
||||
<div v-if="currentServer" class="table-section">
|
||||
<div class="server-header">
|
||||
<h3>{{ currentServer.name }}</h3>
|
||||
<div class="server-actions">
|
||||
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
|
||||
<el-icon><plus /></el-icon>上传镜像
|
||||
</el-button>
|
||||
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
|
||||
<el-icon><refresh /></el-icon>同步镜像
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="currentMirrorList"
|
||||
border
|
||||
style="width: 100%"
|
||||
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 label="镜像信息" min-width="250">
|
||||
@@ -65,7 +63,7 @@
|
||||
<div class="image-info-content">
|
||||
<div class="image-name-row">
|
||||
<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 class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
|
||||
</div>
|
||||
@@ -104,17 +102,17 @@
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="10"
|
||||
:total="total"
|
||||
layout="prev, pager, next"
|
||||
@current-change="handleCurrentPageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="10"
|
||||
:total="total"
|
||||
layout="prev, pager, next"
|
||||
@current-change="handleCurrentPageChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 镜像详情对话框 -->
|
||||
<el-dialog
|
||||
@@ -201,12 +199,6 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</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="图标">
|
||||
<div class="image-icon-upload">
|
||||
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
|
||||
@@ -986,65 +978,65 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.container-images-container {
|
||||
padding: 24px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 60px);
|
||||
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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
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);
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.server-section {
|
||||
margin-bottom: 32px;
|
||||
.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;
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
@@ -1055,7 +1047,7 @@ onMounted(() => {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
height: 16px;
|
||||
background-color: #409EFF;
|
||||
margin-right: 10px;
|
||||
border-radius: 2px;
|
||||
@@ -1066,12 +1058,6 @@ onMounted(() => {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1118,11 +1104,12 @@ onMounted(() => {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
/* 详情对话框样式 */
|
||||
@@ -1263,75 +1250,34 @@ onMounted(() => {
|
||||
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) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #ecf5ff !important;
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-button--link) {
|
||||
padding: 4px 8px;
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-button--link):hover {
|
||||
background-color: #f0f2f5;
|
||||
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;
|
||||
}
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +1,105 @@
|
||||
<template>
|
||||
<div class="image-categories-container" v-loading="loading">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2>镜像分类管理</h2>
|
||||
<p class="page-description">管理不同服务器下的镜像分类</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="服务器">
|
||||
<el-select
|
||||
v-model="selectedServer"
|
||||
placeholder="请选择服务器"
|
||||
clearable
|
||||
@change="handleServerChange"
|
||||
style="width: 220px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in serverList"
|
||||
:key="item.server_id"
|
||||
:label="item.name"
|
||||
:value="item.server_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
placeholder="搜索分类名称"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAddCategory"
|
||||
:disabled="!selectedServer"
|
||||
>
|
||||
<el-icon><plus /></el-icon>添加分类
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredCategoryList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||
<el-table-column prop="name" label="分类名称" min-width="150" />
|
||||
<el-table-column label="分类图标" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-avatar
|
||||
v-if="scope.row.class_ico"
|
||||
:size="40"
|
||||
:src="scope.row.class_ico"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-icon v-else :size="20"><picture /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属服务器" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="scope">
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="服务器">
|
||||
<el-select
|
||||
v-model="selectedServer"
|
||||
placeholder="请选择服务器"
|
||||
clearable
|
||||
@change="handleServerChange"
|
||||
style="width: 220px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in serverList"
|
||||
:key="item.server_id"
|
||||
:label="item.name"
|
||||
:value="item.server_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
placeholder="搜索分类名称"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</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
|
||||
type="success"
|
||||
link
|
||||
@click="handleEditCategory(scope.row)"
|
||||
type="primary"
|
||||
@click="handleAddCategory"
|
||||
:disabled="!selectedServer"
|
||||
>
|
||||
<el-icon><edit /></el-icon>编辑
|
||||
<el-icon><plus /></el-icon>添加分类
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<!-- 表格 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredCategoryList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||
<el-table-column prop="name" label="分类名称" min-width="150" />
|
||||
<el-table-column label="分类图标" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-avatar
|
||||
v-if="scope.row.class_ico"
|
||||
:size="40"
|
||||
:src="scope.row.class_ico"
|
||||
fit="cover"
|
||||
style="background-color: #f5f7fa; border: 1px solid #ebeef5;"
|
||||
/>
|
||||
<el-icon v-else :size="20" color="#909399"><picture /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属服务器" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEditCategory(scope.row)"
|
||||
>
|
||||
<el-icon><edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalCount"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
|
||||
|
||||
<style scoped>
|
||||
.image-categories-container {
|
||||
padding: 24px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 60px);
|
||||
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;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24px;
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
.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;
|
||||
margin-top: 16px;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
/* 图片上传区域样式 */
|
||||
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
|
||||
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) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #ecf5ff !important;
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-button--link) {
|
||||
padding: 4px 8px;
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-button--link):hover {
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@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,103 +1,103 @@
|
||||
<template>
|
||||
<div class="image-requests-container">
|
||||
<div class="page-header">
|
||||
<h2>申请镜像</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>申请镜像
|
||||
</el-button>
|
||||
<el-button @click="handleRefresh">
|
||||
<el-icon><refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="镜像名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="镜像类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable style="width: 150px">
|
||||
<el-option label="Docker镜像" value="docker" />
|
||||
<el-option label="Windows镜像" value="windows" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="审核中" value="pending" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<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>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<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-item label="镜像名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="镜像类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable>
|
||||
<el-option label="Docker镜像" value="docker" />
|
||||
<el-option label="Windows镜像" value="windows" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="审核中" value="pending" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
<!-- 提示信息 -->
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="info-alert"
|
||||
style="margin: 20px 20px 0; width: auto;"
|
||||
>
|
||||
<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="type" label="类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
|
||||
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">
|
||||
<el-icon><view /></el-icon>查看详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'rejected'"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleResubmit(scope.row)"
|
||||
>
|
||||
<el-icon><refresh /></el-icon>重新提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #title>
|
||||
申请的镜像需要经过安全审核,审核通过后可在创建云电脑或容器时使用,审核结果将通过站内信通知。
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
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="name" label="镜像名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="type" label="类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
|
||||
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">
|
||||
<el-icon><view /></el-icon>查看详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'rejected'"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleResubmit(scope.row)"
|
||||
>
|
||||
<el-icon><refresh /></el-icon>重新提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
@@ -106,6 +106,8 @@
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -589,41 +591,58 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -637,12 +656,16 @@ onMounted(() => {
|
||||
/* 环境变量配置样式 */
|
||||
.env-vars-container {
|
||||
margin-bottom: 20px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.env-vars-header {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.env-vars-item {
|
||||
@@ -665,17 +688,23 @@ onMounted(() => {
|
||||
|
||||
.add-env-btn {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* 端口配置样式 */
|
||||
.ports-container {
|
||||
margin-bottom: 20px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ports-header {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.ports-item {
|
||||
@@ -702,6 +731,8 @@ onMounted(() => {
|
||||
|
||||
.add-port-btn {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* 详情样式 */
|
||||
@@ -710,11 +741,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.request-reason {
|
||||
background-color: #f8f8f8;
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
white-space: pre-wrap;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.review-comment {
|
||||
@@ -724,5 +757,37 @@ onMounted(() => {
|
||||
margin-top: 10px;
|
||||
white-space: pre-wrap;
|
||||
border-left: 4px solid #67c23a;
|
||||
color: #606266;
|
||||
}
|
||||
</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>
|
||||
+189
-725
File diff suppressed because it is too large
Load Diff
@@ -1,99 +1,93 @@
|
||||
<template>
|
||||
<div class="announcements-container">
|
||||
<div class="page-header">
|
||||
<h2>官方公告</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>发布公告
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||
<el-option label="已发布" value="published" />
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已下线" value="offline" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="已发布" value="published" />
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已下线" value="offline" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
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 prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="publisher" label="发布人" width="120" />
|
||||
<el-table-column prop="publishTime" label="发布时间" width="180" />
|
||||
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleChangeStatus(scope.row)"
|
||||
v-if="scope.row.status !== 'offline'"
|
||||
>下线</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="scope.row.status === 'offline'"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
>
|
||||
<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="publisher" label="发布人" width="120" align="center" />
|
||||
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
|
||||
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">
|
||||
<el-icon><view /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
||||
<el-icon><edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleChangeStatus(scope.row)"
|
||||
v-if="scope.row.status !== 'offline'"
|
||||
>
|
||||
<el-icon><turn-off /></el-icon>下线
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="scope.row.status === 'offline'"
|
||||
>
|
||||
<el-icon><delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
@@ -102,6 +96,8 @@
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -365,32 +361,58 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -420,4 +442,35 @@ onMounted(() => {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
}
|
||||
</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>
|
||||
+142
-92
@@ -1,58 +1,61 @@
|
||||
<template>
|
||||
<div class="news-container">
|
||||
<div class="page-header">
|
||||
<h2>新闻咨询</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>发布新闻
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="新闻标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新闻分类">
|
||||
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable style="width: 150px">
|
||||
<el-option label="产品动态" value="product" />
|
||||
<el-option label="技术干货" value="technology" />
|
||||
<el-option label="行业资讯" value="industry" />
|
||||
<el-option label="活动公告" value="activity" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="新闻标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="新闻分类">
|
||||
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable>
|
||||
<el-option label="产品动态" value="product" />
|
||||
<el-option label="技术干货" value="technology" />
|
||||
<el-option label="行业资讯" value="industry" />
|
||||
<el-option label="活动公告" value="activity" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 新闻列表卡片 -->
|
||||
<div v-loading="loading" class="news-list">
|
||||
<el-card class="table-card">
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="newsData"
|
||||
border
|
||||
style="width: 100%"
|
||||
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 prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
|
||||
@@ -68,33 +71,27 @@
|
||||
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">
|
||||
<el-icon><view /></el-icon>查看
|
||||
</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>
|
||||
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新闻详情对话框 -->
|
||||
<el-dialog
|
||||
@@ -400,36 +397,58 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.news-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -487,4 +506,35 @@ onMounted(() => {
|
||||
margin-right: 5px;
|
||||
color: #E6A23C;
|
||||
}
|
||||
</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>
|
||||
+170
-117
@@ -1,108 +1,102 @@
|
||||
<template>
|
||||
<div class="policies-container">
|
||||
<div class="page-header">
|
||||
<h2>官方政策</h2>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>发布政策
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="政策标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="政策类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable style="width: 150px">
|
||||
<el-option label="服务条款" value="terms" />
|
||||
<el-option label="定价政策" value="pricing" />
|
||||
<el-option label="隐私政策" value="privacy" />
|
||||
<el-option label="合规政策" value="compliance" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="政策标题">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="政策类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable>
|
||||
<el-option label="服务条款" value="terms" />
|
||||
<el-option label="定价政策" value="pricing" />
|
||||
<el-option label="隐私政策" value="privacy" />
|
||||
<el-option label="合规政策" value="compliance" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
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 prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="type" label="政策类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getPolicyTypeTag(scope.row.type)">
|
||||
{{ getPolicyTypeText(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
|
||||
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
|
||||
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleChangeStatus(scope.row)"
|
||||
v-if="scope.row.status === 'active'"
|
||||
>下线</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="scope.row.status === 'inactive'"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
>
|
||||
<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="type" label="政策类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getPolicyTypeTag(scope.row.type)">
|
||||
{{ getPolicyTypeText(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
|
||||
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
|
||||
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleView(scope.row)">
|
||||
<el-icon><view /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
||||
<el-icon><edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleChangeStatus(scope.row)"
|
||||
v-if="scope.row.status === 'active'"
|
||||
>
|
||||
<el-icon><turn-off /></el-icon>下线
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="scope.row.status === 'inactive'"
|
||||
>
|
||||
<el-icon><delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
@@ -111,6 +105,8 @@
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -435,32 +431,58 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -495,4 +517,35 @@ onMounted(() => {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
}
|
||||
</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>
|
||||
+194
-772
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>
|
||||
@@ -15,6 +15,7 @@
|
||||
<div class="actions">
|
||||
<el-button @click="goBack" :icon="Back">返回</el-button>
|
||||
<el-button type="primary" @click="refreshData" :icon="Refresh">刷新数据</el-button>
|
||||
<el-button type="info" @click="clearAllCache" plain>清除缓存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +100,7 @@
|
||||
<div class="info-content">
|
||||
<div class="info-item">
|
||||
<div class="info-label">链接端口号</div>
|
||||
<div class="info-value">{{ vmInfo.link_port || '10006' }}</div>
|
||||
<div class="info-value">{{ vmInfo.node_port || (portsList.length > 0 ? portsList[0].node_port : '10006') }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">虚拟机用户名</div>
|
||||
@@ -825,7 +826,7 @@ const setCachedData = (instanceId, data) => {
|
||||
console.log(`缓存虚拟机数据: ${instanceId}`, cacheData);
|
||||
};
|
||||
|
||||
const isCacheValid = (cachedData, maxAge = 5 * 60 * 1000) => { // 默认5分钟有效期
|
||||
const isCacheValid = (cachedData, maxAge = 10 * 60 * 1000) => { // 默认10分钟有效期
|
||||
if (!cachedData || !cachedData.timestamp) return false;
|
||||
return (Date.now() - cachedData.timestamp) < maxAge;
|
||||
};
|
||||
@@ -1379,12 +1380,17 @@ const fetchVmInfo = async (instanceId = null, useCache = true) => {
|
||||
const serverRes = await selectServer({server_id:res.data.data.server_id})
|
||||
const planRes = await selectServerPlan({plan_id:res.data.data.plan_id,server_type:"hyperV"})
|
||||
const imageRes= await Mirrorinfo({image_id:res.data.data.image_id,server_type:"hyperV"})
|
||||
|
||||
// 如果端口列表已加载,获取第一个端口号
|
||||
const firstPort = portsList.value.length > 0 ? portsList.value[0].node_port : null;
|
||||
|
||||
const data = {
|
||||
...res.data.data,
|
||||
server_name:serverRes.data.data.name,
|
||||
server_ip:serverRes.data.data.server_ip,
|
||||
plan_name:planRes.data.data.name,
|
||||
image_name:imageRes.data.data.name
|
||||
image_name:imageRes.data.data.name,
|
||||
node_port: res.data.data.node_port || firstPort // 优先使用API返回的,否则用端口列表的第一个
|
||||
}
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
vmInfo.value = data || {};
|
||||
@@ -1554,6 +1560,13 @@ const refreshData = () => {
|
||||
loadAllData(instanceId, false);
|
||||
};
|
||||
|
||||
// 清除所有缓存
|
||||
const clearAllCache = () => {
|
||||
console.log('清除所有虚拟机缓存');
|
||||
dataCache.value.clear();
|
||||
ElMessage.success('已清除所有缓存');
|
||||
};
|
||||
|
||||
// 启动虚拟机
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
|
||||
+185
-197
@@ -446,8 +446,8 @@
|
||||
<!-- <serverChart v-if="TypeData" :Type="TypeData" class="chart-section" /> -->
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="content-wrapper">
|
||||
<el-tabs type="border-card" class="main-tabs">
|
||||
<el-card class="main-container" shadow="never">
|
||||
<el-tabs class="main-tabs">
|
||||
<!-- 实例规格列表 -->
|
||||
<el-tab-pane label="实例规格列表">
|
||||
<div class="tab-header">
|
||||
@@ -464,11 +464,11 @@
|
||||
<el-table
|
||||
v-loading="specLoading"
|
||||
:data="spec_list"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
table-layout="auto"
|
||||
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="name" label="规格名称" min-width="120" show-overflow-tooltip />
|
||||
@@ -510,24 +510,24 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<div class="table-actions">
|
||||
<el-tooltip content="编辑" placement="top" :hide-after="1500">
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
link
|
||||
:icon="Edit"
|
||||
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
|
||||
/>
|
||||
>编辑</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top" :hide-after="1500">
|
||||
<el-button
|
||||
type="danger"
|
||||
circle
|
||||
link
|
||||
:icon="Delete"
|
||||
@click="deleteSpec(scope.row.plan_id)"
|
||||
/>
|
||||
>删除</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -562,9 +562,9 @@
|
||||
<el-table
|
||||
:data="user_servers"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
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="价格" prop="pay" width="100">
|
||||
@@ -598,7 +598,7 @@
|
||||
: scope.row.container_state == 4
|
||||
? 'danger'
|
||||
: 'info'"
|
||||
effect="light"
|
||||
effect="plain"
|
||||
>
|
||||
{{
|
||||
scope.row.container_state == 0
|
||||
@@ -616,11 +616,11 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
:icon="Setting"
|
||||
@click="$router.push('/servers/container?container_id=' + scope.row.container_id)"
|
||||
>
|
||||
@@ -667,9 +667,9 @@
|
||||
<el-table
|
||||
:data="floatList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
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="创建时间" min-width="160">
|
||||
@@ -680,7 +680,7 @@
|
||||
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<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 ? "已绑定" : "未绑定" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -690,10 +690,10 @@
|
||||
<el-tooltip content="删除IP" placement="top" :hide-after="1500">
|
||||
<el-button
|
||||
type="danger"
|
||||
circle
|
||||
link
|
||||
:icon="Delete"
|
||||
@click="delFloating(scope.row.id)"
|
||||
/>
|
||||
>删除</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -702,7 +702,7 @@
|
||||
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑实例规格对话框 -->
|
||||
<el-dialog
|
||||
@@ -837,6 +837,54 @@
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section" v-if="TypeData == 'hyperV'">
|
||||
<div class="section-title">GPU配置</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="GPU资源百分比">
|
||||
<el-input v-model="spec_form.gpu_percentage" placeholder="GPU资源百分比" type="number">
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最小显存">
|
||||
<el-input v-model="spec_form.min_partition_vram" placeholder="最小显存" type="number">
|
||||
<template #append>MB</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大显存">
|
||||
<el-input v-model="spec_form.max_partition_vram" placeholder="最大显存" type="number">
|
||||
<template #append>MB</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最优显存">
|
||||
<el-input v-model="spec_form.optimal_partition_vram" placeholder="最优显存" type="number">
|
||||
<template #append>MB</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="低内存映射IO空间">
|
||||
<el-input v-model="spec_form.low_mmio_space" placeholder="默认128" type="number" :value="spec_form.low_mmio_space || 128">
|
||||
<template #append>MB</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="高内存映射IO空间">
|
||||
<el-input v-model="spec_form.high_mmio_space" placeholder="建议为显存的2倍" type="number">
|
||||
<template #append>MB</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">价格与验证</div>
|
||||
<el-row :gutter="20">
|
||||
@@ -2205,7 +2253,13 @@ const spec_form = reactive({
|
||||
min_iops: "",
|
||||
max_iops: "",
|
||||
description: "",
|
||||
must_real_name: 0
|
||||
must_real_name: 0,
|
||||
gpu_percentage: "",
|
||||
min_partition_vram: "",
|
||||
max_partition_vram: "",
|
||||
optimal_partition_vram: "",
|
||||
low_mmio_space: "",
|
||||
high_mmio_space: ""
|
||||
});
|
||||
|
||||
function show_spec(data = null) {
|
||||
@@ -2227,6 +2281,7 @@ function show_spec(data = null) {
|
||||
spec_form.snapshot_num = '3';
|
||||
spec_form.min_iops = '1000';
|
||||
spec_form.max_iops = '5000';
|
||||
spec_form.low_mmio_space = '128';
|
||||
}
|
||||
} else {
|
||||
// 复制传入的数据
|
||||
@@ -2562,9 +2617,7 @@ import { ElMessageBox } from 'element-plus';
|
||||
|
||||
<style scoped>
|
||||
.server-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 页面标题区域 */
|
||||
@@ -2573,6 +2626,9 @@ import { ElMessageBox } from 'element-plus';
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.page-header .left {
|
||||
@@ -2583,7 +2639,7 @@ import { ElMessageBox } from 'element-plus';
|
||||
|
||||
.page-header .title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
@@ -2592,7 +2648,8 @@ import { ElMessageBox } from 'element-plus';
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -2614,13 +2671,13 @@ import { ElMessageBox } from 'element-plus';
|
||||
/* 返回按钮样式 */
|
||||
.back-btn {
|
||||
font-size: 16px;
|
||||
color: #409EFF;
|
||||
padding: 8px 0;
|
||||
margin-right: 16px;
|
||||
color: #606266;
|
||||
padding: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #66b1ff;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
/* 服务器信息卡片 */
|
||||
@@ -2629,6 +2686,7 @@ import { ElMessageBox } from 'element-plus';
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 服务器详细信息卡片 */
|
||||
@@ -2637,16 +2695,17 @@ import { ElMessageBox } from 'element-plus';
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
@@ -2655,10 +2714,10 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
.card-title {
|
||||
background-color: #f5f7fa;
|
||||
background-color: #fafbfc;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
@@ -2667,7 +2726,7 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
.card-title .el-icon {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
@@ -2690,32 +2749,31 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.info-value.highlight {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
/* 硬件信息样式 - 符合整体设计风格 */
|
||||
/* 硬件信息样式 */
|
||||
.device-count {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.hardware-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
@@ -2725,21 +2783,21 @@ import { ElMessageBox } from 'element-plus';
|
||||
.hardware-devices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
background-color: #fafbfc;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.device-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.device-title {
|
||||
@@ -2752,57 +2810,27 @@ import { ElMessageBox } from 'element-plus';
|
||||
|
||||
.device-title .el-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.device-details {
|
||||
margin-top: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.usage-progress {
|
||||
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 {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.traffic-section:last-child {
|
||||
@@ -2810,62 +2838,38 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.traffic-speed-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-total-grid,
|
||||
.traffic-distribution {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.raw-data {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background-color: #fafbfc;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e1e8ed;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.raw-data .info-value {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.traffic-note {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 旧样式保持兼容 */
|
||||
.usage-high {
|
||||
color: #F56C6C;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.usage-medium {
|
||||
color: #E6A23C;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 错误状态样式 */
|
||||
.error-status {
|
||||
color: #F56C6C;
|
||||
@@ -2874,15 +2878,10 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.chart-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 24px;
|
||||
.main-container {
|
||||
margin: 0 20px 20px 20px;
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
@@ -2890,11 +2889,13 @@ import { ElMessageBox } from 'element-plus';
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
@@ -2914,40 +2915,67 @@ import { ElMessageBox } from 'element-plus';
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.data-table {
|
||||
margin-bottom: 16px;
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.unit {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
color: #F56C6C;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #409EFF;
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -2958,15 +2986,13 @@ import { ElMessageBox } from 'element-plus';
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
border-bottom: 1px dashed #e1e8ed;
|
||||
}
|
||||
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -3069,27 +3095,24 @@ import { ElMessageBox } from 'element-plus';
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header .left {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.server-detail-info {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.info-card.location-info {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -3104,40 +3127,5 @@ import { ElMessageBox } from 'element-plus';
|
||||
width: 100%;
|
||||
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>
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div class="signin-activity-container">
|
||||
<!-- 操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAddReward">
|
||||
<el-icon><Plus /></el-icon>新增签到奖励
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleAddRewardType">
|
||||
<el-icon><Plus /></el-icon>新增奖励类型
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 签到奖励列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>签到奖励配置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rewardList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="day" label="签到天数" width="120" />
|
||||
<el-table-column prop="reward_type" label="奖励类型" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRewardTypeColor(row.reward_type)">
|
||||
{{ getRewardTypeText(row.reward_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reward_value" label="奖励值" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.reward_type === 'balance'">¥{{ row.reward_value }}</span>
|
||||
<span v-else>{{ row.reward_value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 奖励类型列表 -->
|
||||
<el-card class="table-container" shadow="never" style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>奖励类型配置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="typeLoading"
|
||||
:data="typeList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="type_code" label="类型代码" width="150" />
|
||||
<el-table-column prop="type_name" label="类型名称" min-width="200" />
|
||||
<el-table-column prop="description" label="描述" min-width="250" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEditType(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDeleteType(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 签到奖励表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="rewardDialogVisible"
|
||||
title="签到奖励配置"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="rewardFormRef"
|
||||
:model="rewardForm"
|
||||
:rules="rewardRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="签到天数" prop="day">
|
||||
<el-input-number v-model="rewardForm.day" :min="1" placeholder="请输入签到天数" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="奖励类型" prop="reward_type">
|
||||
<el-select v-model="rewardForm.reward_type" placeholder="请选择奖励类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.type_code"
|
||||
:label="item.type_name"
|
||||
:value="item.type_code"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="奖励值" prop="reward_value">
|
||||
<el-input v-model.number="rewardForm.reward_value" placeholder="请输入奖励值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="rewardForm.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="rewardForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="rewardDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitRewardForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 奖励类型表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="typeDialogVisible"
|
||||
title="奖励类型配置"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="typeFormRef"
|
||||
:model="typeForm"
|
||||
:rules="typeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="类型代码" prop="type_code">
|
||||
<el-input v-model="typeForm.type_code" placeholder="请输入类型代码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型名称" prop="type_name">
|
||||
<el-input v-model="typeForm.type_name" placeholder="请输入类型名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="typeForm.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="typeForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="typeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitTypeForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { addSignReward, addSignRewardType } from '@/api/admin/activity'
|
||||
|
||||
// 签到奖励表单
|
||||
const rewardForm = reactive({
|
||||
id: undefined,
|
||||
day: 1,
|
||||
reward_type: '',
|
||||
reward_value: 0,
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
const rewardRules = {
|
||||
day: [
|
||||
{ required: true, message: '请输入签到天数', trigger: 'blur' }
|
||||
],
|
||||
reward_type: [
|
||||
{ required: true, message: '请选择奖励类型', trigger: 'change' }
|
||||
],
|
||||
reward_value: [
|
||||
{ required: true, message: '请输入奖励值', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 奖励类型表单
|
||||
const typeForm = reactive({
|
||||
id: undefined,
|
||||
type_code: '',
|
||||
type_name: '',
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
const typeRules = {
|
||||
type_code: [
|
||||
{ required: true, message: '请输入类型代码', trigger: 'blur' }
|
||||
],
|
||||
type_name: [
|
||||
{ required: true, message: '请输入类型名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const typeLoading = ref(false)
|
||||
const rewardList = ref([])
|
||||
const typeList = ref([])
|
||||
const typeOptions = ref([])
|
||||
const rewardDialogVisible = ref(false)
|
||||
const typeDialogVisible = ref(false)
|
||||
const rewardFormRef = ref(null)
|
||||
const typeFormRef = ref(null)
|
||||
|
||||
// 获取奖励类型文本
|
||||
const getRewardTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'balance': '余额',
|
||||
'points': '积分',
|
||||
'coupon': '优惠券',
|
||||
'voucher': '代金券'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取奖励类型颜色
|
||||
const getRewardTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'balance': 'success',
|
||||
'points': 'primary',
|
||||
'coupon': 'warning',
|
||||
'voucher': 'info'
|
||||
}
|
||||
return colorMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 新增签到奖励
|
||||
const handleAddReward = () => {
|
||||
rewardDialogVisible.value = true
|
||||
Object.assign(rewardForm, {
|
||||
id: undefined,
|
||||
day: 1,
|
||||
reward_type: '',
|
||||
reward_value: 0,
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
rewardFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 新增奖励类型
|
||||
const handleAddRewardType = () => {
|
||||
typeDialogVisible.value = true
|
||||
Object.assign(typeForm, {
|
||||
id: undefined,
|
||||
type_code: '',
|
||||
type_name: '',
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
typeFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑签到奖励
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(rewardForm, {
|
||||
id: row.id,
|
||||
day: row.day,
|
||||
reward_type: row.reward_type,
|
||||
reward_value: row.reward_value,
|
||||
description: row.description,
|
||||
status: row.status
|
||||
})
|
||||
rewardDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑奖励类型
|
||||
const handleEditType = (row) => {
|
||||
Object.assign(typeForm, {
|
||||
id: row.id,
|
||||
type_code: row.type_code,
|
||||
type_name: row.type_name,
|
||||
description: row.description,
|
||||
status: row.status
|
||||
})
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除签到奖励
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除第${row.day}天的签到奖励吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
// 这里应该调用删除API
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 删除奖励类型
|
||||
const handleDeleteType = (row) => {
|
||||
ElMessageBox.confirm(`确认删除奖励类型 ${row.type_name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
// 这里应该调用删除API
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交签到奖励表单
|
||||
const submitRewardForm = () => {
|
||||
rewardFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await addSignReward(rewardForm)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('添加成功')
|
||||
rewardDialogVisible.value = false
|
||||
// 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交奖励类型表单
|
||||
const submitTypeForm = () => {
|
||||
typeFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await addSignRewardType(typeForm)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('添加成功')
|
||||
typeDialogVisible.value = false
|
||||
// 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 模拟数据
|
||||
rewardList.value = [
|
||||
{
|
||||
id: 1,
|
||||
day: 1,
|
||||
reward_type: 'balance',
|
||||
reward_value: 1,
|
||||
description: '连续签到1天奖励1元余额',
|
||||
status: 1,
|
||||
create_time: '2024-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
day: 7,
|
||||
reward_type: 'balance',
|
||||
reward_value: 10,
|
||||
description: '连续签到7天奖励10元余额',
|
||||
status: 1,
|
||||
create_time: '2024-01-01 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
typeList.value = [
|
||||
{
|
||||
id: 1,
|
||||
type_code: 'balance',
|
||||
type_name: '余额',
|
||||
description: '用户账户余额',
|
||||
status: 1,
|
||||
create_time: '2024-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type_code: 'points',
|
||||
type_name: '积分',
|
||||
description: '用户积分',
|
||||
status: 1,
|
||||
create_time: '2024-01-01 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
typeOptions.value = typeList.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.signin-activity-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
+200
-193
@@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<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="stat-card total-card">
|
||||
@@ -44,102 +32,115 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="搜索容器">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="搜索容器">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<el-icon><Delete /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<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">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="siteList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="url" label="访问地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="primary" v-if="row.url">
|
||||
{{ row.url }}
|
||||
</el-link>
|
||||
<span v-else class="text-muted">无访问地址</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
|
||||
{{ getConnectTypeText(row.connect_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getConnectionStatusType(row.connect)"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ getConnectionStatusText(row.connect) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.is_violation ? 'danger' : 'success'"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ row.is_violation ? '违规' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
|
||||
<!-- 站点列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="siteList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="url" label="访问地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="primary" v-if="row.url">
|
||||
{{ row.url }}
|
||||
</el-link>
|
||||
<span v-else class="text-muted">无访问地址</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
|
||||
{{ getConnectTypeText(row.connect_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getConnectionStatusType(row.connect)"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ getConnectionStatusText(row.connect) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.is_violation ? 'danger' : 'success'"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ row.is_violation ? '违规' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" fixed="right" align="center" width="180">
|
||||
<template #default="{ row }">
|
||||
<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 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 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>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -650,42 +651,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.all-sites-container {
|
||||
padding: 20px;
|
||||
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;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
@@ -698,18 +664,18 @@ onMounted(() => {
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
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 {
|
||||
@@ -753,88 +719,129 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.1;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选容器 */
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 站点详情 */
|
||||
.site-detail {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin-bottom: 12px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 对话框底部 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 1200px) {
|
||||
@media screen and (max-width: 992px) {
|
||||
.stats-panel {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-card:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义样式 */
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
|
||||
.stat-card:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+199
-282
@@ -1,158 +1,101 @@
|
||||
<template>
|
||||
<div class="violation-sites-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="left">
|
||||
<h2 class="title">违规站点</h2>
|
||||
<el-tag type="danger" effect="plain" class="count-tag">共 {{ pagination.total }} 个违规站点</el-tag>
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="搜索容器">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<el-icon><Delete /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="搜索容器">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
|
||||
</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-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 违规站点列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="violationList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="url" label="违规地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="danger" v-if="row.url">
|
||||
{{ row.url }}
|
||||
</el-link>
|
||||
<span v-else class="text-muted">无访问地址</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" size="small" v-if="row.violation_keys && row.violation_keys.length > 0">
|
||||
{{ row.violation_keys.join(', ') }}
|
||||
</el-tag>
|
||||
<el-tag type="warning" size="small" v-else>
|
||||
检测到违规
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
|
||||
{{ getConnectTypeText(row.connect_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<!-- 违规站点列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="violationList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="url" label="违规地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.url" target="_blank" type="danger" v-if="row.url">
|
||||
{{ row.url }}
|
||||
</el-link>
|
||||
<span v-else class="text-muted">无访问地址</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" size="small" v-if="row.violation_keys && row.violation_keys.length > 0">
|
||||
{{ row.violation_keys.join(', ') }}
|
||||
</el-tag>
|
||||
<el-tag type="warning" size="small" v-else>
|
||||
检测到违规
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
|
||||
{{ getConnectTypeText(row.connect_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" fixed="right" align="center" width="180">
|
||||
<template #default="{ row }">
|
||||
<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 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 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>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 违规详情对话框 -->
|
||||
@@ -799,7 +742,13 @@ const handleBatchProcess = () => {
|
||||
|
||||
// 导出数据
|
||||
const handleExport = () => {
|
||||
ElMessage.success('违规报告导出功能开发中...')
|
||||
ElMessage.success('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row) => {
|
||||
currentSite.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 重新检查
|
||||
@@ -893,12 +842,6 @@ const handleMarkNormal = (row) => {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row) => {
|
||||
currentSite.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理违规
|
||||
const handleProcess = (row) => {
|
||||
currentSite.value = row
|
||||
@@ -907,49 +850,14 @@ const handleProcess = (row) => {
|
||||
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 = () => {
|
||||
if (!processFormRef.value) return
|
||||
|
||||
processFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('违规处理提交成功')
|
||||
// 模拟提交处理
|
||||
ElMessage.success('处理成功')
|
||||
processDialogVisible.value = false
|
||||
getList()
|
||||
}
|
||||
@@ -964,42 +872,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.violation-sites-container {
|
||||
padding: 20px;
|
||||
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;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
@@ -1012,18 +885,18 @@ onMounted(() => {
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
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 {
|
||||
@@ -1039,8 +912,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.total-card .stat-icon {
|
||||
background-color: rgba(230, 162, 60, 0.1);
|
||||
color: #E6A23C;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.severe-card .stat-icon {
|
||||
@@ -1049,8 +922,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.moderate-card .stat-icon {
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
color: #FFC107;
|
||||
background-color: rgba(230, 162, 60, 0.1);
|
||||
color: #E6A23C;
|
||||
}
|
||||
|
||||
.pending-card .stat-icon {
|
||||
@@ -1067,56 +940,67 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.1;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选容器 */
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.domain-cell {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.blocked-tag {
|
||||
margin-left: 8px;
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.report-count {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 违规详情 */
|
||||
/* 详情样式 */
|
||||
.violation-detail {
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -1127,43 +1011,76 @@ onMounted(() => {
|
||||
|
||||
.detail-section h4 {
|
||||
margin-bottom: 12px;
|
||||
color: #303133;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
border-left: 4px solid #409EFF;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* 对话框底部 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
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 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-card:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义样式 */
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
|
||||
.stat-card:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,665 @@
|
||||
<template>
|
||||
<div class="discount-code-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增优惠码
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchDiscountList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优惠码列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<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
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="discountList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="code" label="优惠码" min-width="150" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
<el-table-column label="优惠类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.percentage ? 'success' : 'primary'">
|
||||
{{ row.percentage ? '百分比折扣' : '固定金额' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠值" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.percentage" class="discount-value">{{ (row.percentage / 100).toFixed(0) }}%</span>
|
||||
<span v-else class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ (row.minAmount / 100).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最大抵扣" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="maxTimes" label="最大使用次数" width="120" />
|
||||
<el-table-column prop="userTimes" label="单用户次数" width="120" />
|
||||
<el-table-column label="可叠加" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="row.canStacking" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续费可用" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
||||
</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="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 优惠码表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
|
||||
width="700px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="discountFormRef"
|
||||
:model="discountForm"
|
||||
:rules="discountRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="优惠码" prop="code">
|
||||
<el-input v-model="discountForm.code" placeholder="请输入优惠码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠码名称" prop="name">
|
||||
<el-input v-model="discountForm.name" placeholder="请输入优惠码名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="discountForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠类型" prop="discount_mode">
|
||||
<el-radio-group v-model="discountForm.discount_mode">
|
||||
<el-radio label="amount">固定金额</el-radio>
|
||||
<el-radio label="percentage">百分比折扣</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额(元)" prop="amount">
|
||||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="discountForm.discount_mode === 'percentage'" label="优惠百分比(%)" prop="percentage">
|
||||
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="请输入百分比(1-100)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最低消费(元)" prop="min_amount">
|
||||
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大抵扣(元)" prop="max_amount">
|
||||
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_times">
|
||||
<el-input-number v-model="discountForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单用户最大次数" prop="user_times">
|
||||
<el-input-number v-model="discountForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期" prop="timeRange">
|
||||
<el-date-picker
|
||||
v-model="discountForm.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:teleported="true"
|
||||
popper-class="discount-date-picker"
|
||||
placement="top-start"
|
||||
:editable="true"
|
||||
:clearable="true"
|
||||
style="width: 100%"
|
||||
@keyup.enter="handleDatePickerEnter"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="续费可用" prop="renew">
|
||||
<el-switch v-model="discountForm.renew" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
<el-form-item label="同类型可叠加" prop="can_stacking">
|
||||
<el-switch v-model="discountForm.can_stacking" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
<el-form-item label="其他类型可叠加" prop="can_combine">
|
||||
<el-switch v-model="discountForm.can_combine" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情查看对话框 -->
|
||||
<DiscountDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
type="code"
|
||||
:detail-data="currentDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Search, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDiscountCodeList,
|
||||
getDiscountCodeDetail,
|
||||
createDiscountCode,
|
||||
updateDiscountCode,
|
||||
deleteDiscountCode
|
||||
} from '@/api/admin/discount'
|
||||
import { timeToTimestamp } from '@/utils/tool'
|
||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
discount_type: 'code', // 固定为code表示优惠码
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 优惠码表单
|
||||
const discountForm = reactive({
|
||||
code_id: undefined,
|
||||
discount_type: 'code', // 固定为code
|
||||
code: '',
|
||||
name: '',
|
||||
note: '',
|
||||
discount_mode: 'amount', // amount 或 percentage
|
||||
amount: 0,
|
||||
percentage: 0,
|
||||
min_amount: 0,
|
||||
max_amount: 0,
|
||||
max_times: 0,
|
||||
user_times: 0,
|
||||
timeRange: [],
|
||||
renew: false,
|
||||
can_stacking: false,
|
||||
can_combine: false
|
||||
})
|
||||
|
||||
const discountRules = {
|
||||
code: [
|
||||
{ required: true, message: '请输入优惠码', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入优惠码名称', trigger: 'blur' }
|
||||
],
|
||||
discount_mode: [
|
||||
{ required: true, message: '请选择优惠类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const discountList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const discountFormRef = ref(null)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref(null)
|
||||
|
||||
// 获取优惠码列表
|
||||
const fetchDiscountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDiscountCodeList(queryParams)
|
||||
console.log('优惠码列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
discountList.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 handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchDiscountList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchDiscountList()
|
||||
}
|
||||
|
||||
// 新增优惠码
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(discountForm, {
|
||||
code_id: undefined,
|
||||
discount_type: 'code',
|
||||
code: '',
|
||||
name: '',
|
||||
note: '',
|
||||
discount_mode: 'amount',
|
||||
amount: 0,
|
||||
percentage: 0,
|
||||
min_amount: 0,
|
||||
max_amount: 0,
|
||||
max_times: 0,
|
||||
user_times: 0,
|
||||
timeRange: [],
|
||||
renew: false,
|
||||
can_stacking: false,
|
||||
can_combine: false
|
||||
})
|
||||
discountFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑优惠码
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 转换日期字符串为日期选择器格式
|
||||
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
|
||||
Object.assign(discountForm, {
|
||||
code_id: row.id,
|
||||
discount_type: 'code',
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
note: row.note || '',
|
||||
discount_mode: row.percentage ? 'percentage' : 'amount',
|
||||
amount: row.amount ? row.amount / 100 : 0,
|
||||
percentage: row.percentage ? row.percentage / 100 : 0,
|
||||
min_amount: row.minAmount ? row.minAmount / 100 : 0,
|
||||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
||||
max_times: row.maxTimes || 0,
|
||||
user_times: row.userTimes || 0,
|
||||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
||||
renew: row.renew || false,
|
||||
can_stacking: row.canStacking || false,
|
||||
can_combine: row.canCombine || false
|
||||
})
|
||||
}
|
||||
|
||||
// 查看优惠码详情
|
||||
const handleView = async (row) => {
|
||||
try {
|
||||
const res = await getDiscountCodeDetail({ code_id: row.id })
|
||||
console.log('优惠码详情:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
currentDetail.value = res.data.data
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠码详情失败:', error)
|
||||
ElMessage.error('获取优惠码详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除优惠码
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除优惠码 ${row.code} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteDiscountCode({ code_id: row.id })
|
||||
console.log('删除响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDiscountList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteDiscountCode({ code_id: row.id })
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(deletePromises)
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||||
} else {
|
||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
}
|
||||
|
||||
fetchDiscountList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除操作异常')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 处理日期选择器回车事件
|
||||
const handleDatePickerEnter = (event) => {
|
||||
// 回车键确认日期选择
|
||||
const datePicker = event.target.closest('.el-date-editor')
|
||||
if (datePicker) {
|
||||
// 触发失焦事件,确认日期选择
|
||||
event.target.blur()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
discountFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
discount_type: 'code',
|
||||
code: discountForm.code,
|
||||
name: discountForm.name,
|
||||
note: discountForm.note,
|
||||
min_amount: Math.round(discountForm.min_amount * 100),
|
||||
max_amount: Math.round(discountForm.max_amount * 100),
|
||||
max_times: discountForm.max_times || 0,
|
||||
user_times: discountForm.user_times || 0,
|
||||
renew: discountForm.renew,
|
||||
can_stacking: discountForm.can_stacking,
|
||||
can_combine: discountForm.can_combine
|
||||
}
|
||||
|
||||
// 根据优惠类型设置amount或percentage
|
||||
if (discountForm.discount_mode === 'percentage') {
|
||||
submitData.percentage = Math.round(discountForm.percentage * 100)
|
||||
submitData.amount = 0
|
||||
} else {
|
||||
submitData.amount = Math.round(discountForm.amount * 100)
|
||||
submitData.percentage = 0
|
||||
}
|
||||
|
||||
// 处理时间(转换为秒级时间戳)
|
||||
if (discountForm.timeRange && discountForm.timeRange.length === 2) {
|
||||
submitData.start_time = timeToTimestamp(discountForm.timeRange[0])
|
||||
submitData.end_time = timeToTimestamp(discountForm.timeRange[1])
|
||||
} else {
|
||||
submitData.start_time = ''
|
||||
submitData.end_time = ''
|
||||
}
|
||||
|
||||
// 如果是编辑,添加code_id
|
||||
if (dialogType.value === 'edit') {
|
||||
submitData.code_id = discountForm.code_id
|
||||
}
|
||||
|
||||
console.log('提交优惠码数据:', submitData)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createDiscountCode(submitData)
|
||||
} else {
|
||||
res = await updateDiscountCode(submitData)
|
||||
}
|
||||
|
||||
console.log('提交响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchDiscountList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchDiscountList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discount-code-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: 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;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.discount-value {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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-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>
|
||||
/* 时间选择器弹出层样式 - 非 scoped */
|
||||
.discount-date-picker {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.discount-date-picker .el-picker-panel {
|
||||
max-width: 90vw;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,872 @@
|
||||
<template>
|
||||
<div class="discount-goods-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金卷" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherListOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
|
||||
<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="fetchGoodsList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品关联列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<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
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="goodsList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
|
||||
<el-table-column label="关联对象ID" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.goodId || row.goodGroupId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
{{ row.good?.name || row.goodGroup?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getGoodsTypeTagByRow(row)">
|
||||
{{ getGoodsTypeNameByRow(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.good?.table || row.goodGroup?.note || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品价格" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.good?.price" class="price">¥{{ (row.good.price / 100).toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑商品关联对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'"
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="代金券" prop="code_id">
|
||||
<el-select
|
||||
v-model="form.code_id"
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="dialogType === 'edit' || !!codeId"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherListOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
|
||||
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
|
||||
<el-radio value="product">商品</el-radio>
|
||||
<el-radio value="product_group">商品组</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择商品" prop="selected_product" v-if="dialogType === 'add' && form.select_type === 'product'">
|
||||
<el-select
|
||||
v-model="form.selected_product"
|
||||
placeholder="请选择商品"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleProductChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (ID: ${item.id})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择商品组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'product_group'">
|
||||
<el-select
|
||||
v-model="form.selected_group"
|
||||
placeholder="请选择商品组"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleProductGroupChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productGroupOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (ID: ${item.id})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 编辑模式显示字段 -->
|
||||
<template v-if="dialogType === 'edit'">
|
||||
<el-form-item label="关联类型">
|
||||
<el-tag :type="form.goods_type === 'product' ? 'primary' : 'warning'">
|
||||
{{ form.goods_type === 'product' ? '商品' : '商品组' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联对象ID">
|
||||
<el-select v-model="form.goods_id" style="width: 100%">
|
||||
<template v-if="form.goods_type === 'product'">
|
||||
<el-option v-for="item in productOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-option v-for="item in productGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDiscountGoodsList,
|
||||
addDiscountGoods,
|
||||
updateDiscountGoods,
|
||||
deleteDiscountGoods,
|
||||
getDiscountCodeList
|
||||
} from '@/api/admin/discount'
|
||||
import {
|
||||
getProductList,
|
||||
getProductGroupList
|
||||
} from '@/api/admin/product'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
code_id: props.codeId || '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchGoodsList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
code_id: undefined,
|
||||
goods_id: undefined,
|
||||
goods_name: '',
|
||||
goods_type: '',
|
||||
select_type: 'product', // 选择类型:product 或 product_group
|
||||
selected_product: undefined, // 选中的商品ID
|
||||
selected_group: undefined // 选中的商品组ID
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
code_id: [
|
||||
{ required: true, message: '请选择代金券', trigger: 'change' }
|
||||
],
|
||||
select_type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'change' }
|
||||
],
|
||||
selected_product: [
|
||||
{ required: true, message: '请选择商品', trigger: 'change' }
|
||||
],
|
||||
selected_group: [
|
||||
{ required: true, message: '请选择商品组', trigger: 'change' }
|
||||
],
|
||||
goods_id: [
|
||||
{ required: true, message: '请输入商品ID', trigger: 'blur' }
|
||||
],
|
||||
goods_name: [
|
||||
{ required: true, message: '请输入商品名称', trigger: 'blur' }
|
||||
],
|
||||
goods_type: [
|
||||
{ required: true, message: '请选择商品类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const goodsList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref(null)
|
||||
const voucherListOptions = ref([]) // 代金券列表选项
|
||||
const productOptions = ref([]) // 商品列表选项
|
||||
const productGroupOptions = ref([]) // 商品组列表选项
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 获取商品类型名称(根据行数据)
|
||||
const getGoodsTypeNameByRow = (row) => {
|
||||
// 判断是否有 goodGroup 对象(选择的是商品组)
|
||||
if (row.goodGroup) {
|
||||
return '商品组'
|
||||
}
|
||||
// 判断是否有 good 对象(选择的是商品)
|
||||
if (row.good) {
|
||||
return '商品'
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 获取商品类型标签(根据行数据)
|
||||
const getGoodsTypeTagByRow = (row) => {
|
||||
// 商品组用橙色
|
||||
if (row.goodGroup) {
|
||||
return 'warning'
|
||||
}
|
||||
// 商品用蓝色
|
||||
if (row.good) {
|
||||
return 'primary'
|
||||
}
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 获取商品类型名称(兼容旧版)
|
||||
const getGoodsTypeName = (type) => {
|
||||
const typeMap = {
|
||||
'product': '商品',
|
||||
'product_group': '商品组',
|
||||
'cloud_server': '云服务器',
|
||||
'cloud_database': '云数据库',
|
||||
'cloud_storage': '云存储',
|
||||
'cdn': 'CDN',
|
||||
'other': '其他'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取商品类型标签(兼容旧版)
|
||||
const getGoodsTypeTag = (type) => {
|
||||
const tagMap = {
|
||||
'product': 'primary',
|
||||
'product_group': 'warning',
|
||||
'cloud_server': 'primary',
|
||||
'cloud_database': 'success',
|
||||
'cloud_storage': 'warning',
|
||||
'cdn': 'info',
|
||||
'other': 'default'
|
||||
}
|
||||
return tagMap[type] || 'default'
|
||||
}
|
||||
|
||||
// 获取代金券列表选项
|
||||
const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
voucherListOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
ElMessage.error('获取代金券列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
const fetchProductList = async () => {
|
||||
try {
|
||||
const res = await getProductList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
})
|
||||
console.log('获取商品列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
productOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
ElMessage.error('获取商品列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品组列表
|
||||
const fetchProductGroupList = async () => {
|
||||
try {
|
||||
const res = await getProductGroupList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
})
|
||||
console.log('获取商品组列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
productGroupOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品组列表失败:', error)
|
||||
ElMessage.error('获取商品组列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 选择类型变化
|
||||
const handleSelectTypeChange = (type) => {
|
||||
form.selected_product = undefined
|
||||
form.selected_group = undefined
|
||||
form.goods_id = undefined
|
||||
form.goods_name = ''
|
||||
form.goods_type = ''
|
||||
}
|
||||
|
||||
// 选择商品变化
|
||||
const handleProductChange = (productId) => {
|
||||
const product = productOptions.value.find(item => item.id === productId)
|
||||
if (product) {
|
||||
form.goods_id = product.id
|
||||
form.goods_name = product.goodsName || product.name || ''
|
||||
form.goods_type = 'product'
|
||||
}
|
||||
}
|
||||
|
||||
// 选择商品组变化
|
||||
const handleProductGroupChange = (groupId) => {
|
||||
const group = productGroupOptions.value.find(item => item.id === groupId)
|
||||
if (group) {
|
||||
form.goods_id = group.id
|
||||
form.goods_name = group.name || ''
|
||||
form.goods_type = 'product_group'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品关联列表
|
||||
const fetchGoodsList = async () => {
|
||||
if (!queryParams.code_id) {
|
||||
ElMessage.warning('请选择代金券进行查询')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDiscountGoodsList(queryParams)
|
||||
console.log('商品关联列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
goodsList.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
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.code_id = ''
|
||||
queryParams.page = 1
|
||||
goodsList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGoodsList()
|
||||
}
|
||||
|
||||
// 新增商品关联
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
|
||||
goods_id: undefined,
|
||||
goods_name: '',
|
||||
goods_type: '',
|
||||
select_type: 'product',
|
||||
selected_product: undefined,
|
||||
selected_group: undefined
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
|
||||
// 加载商品和商品组列表
|
||||
fetchProductList()
|
||||
fetchProductGroupList()
|
||||
}
|
||||
|
||||
// 编辑商品关联
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 判断是商品还是商品组
|
||||
let goodsId, goodsName, goodsType
|
||||
|
||||
if (row.goodGroup) {
|
||||
// 商品组
|
||||
goodsId = row.goodGroupId
|
||||
goodsName = row.goodGroup.name
|
||||
goodsType = 'product_group'
|
||||
} else if (row.good) {
|
||||
// 商品
|
||||
goodsId = row.goodId
|
||||
goodsName = row.good.name
|
||||
goodsType = 'product'
|
||||
}
|
||||
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code_id: row.discountId,
|
||||
goods_id: goodsId,
|
||||
goods_name: goodsName,
|
||||
goods_type: goodsType
|
||||
})
|
||||
|
||||
// 加载商品和商品组列表以便编辑
|
||||
fetchProductList()
|
||||
fetchProductGroupList()
|
||||
}
|
||||
|
||||
// 删除商品关联
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除该商品关联吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteDiscountGoods({
|
||||
discount_good_id: String(row.id),
|
||||
code_id: String(row.discountId)
|
||||
})
|
||||
console.log('删除响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGoodsList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteDiscountGoods({
|
||||
discount_good_id: String(row.id),
|
||||
code_id: String(row.discountId)
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(deletePromises)
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||||
} else {
|
||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
}
|
||||
|
||||
fetchGoodsList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除操作异常')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
// 新增模式下的额外验证
|
||||
if (dialogType.value === 'add') {
|
||||
if (!form.code_id) {
|
||||
ElMessage.warning('请选择代金券')
|
||||
return
|
||||
}
|
||||
if (form.select_type === 'product' && !form.selected_product) {
|
||||
ElMessage.warning('请选择商品')
|
||||
return
|
||||
}
|
||||
if (form.select_type === 'product_group' && !form.selected_group) {
|
||||
ElMessage.warning('请选择商品组')
|
||||
return
|
||||
}
|
||||
if (!form.goods_id || !form.goods_name || !form.goods_type) {
|
||||
ElMessage.warning('请先选择商品或商品组')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
code_id: String(form.code_id)
|
||||
}
|
||||
|
||||
// 根据选择类型决定传 good_id 还是 good_group_id
|
||||
if (dialogType.value === 'add') {
|
||||
if (form.select_type === 'product') {
|
||||
// 选择的是商品,传 good_id
|
||||
submitData.good_ids = String(form.goods_id)
|
||||
} else if (form.select_type === 'product_group') {
|
||||
// 选择的是商品组,传 good_group_id
|
||||
submitData.good_group_ids = String(form.goods_id)
|
||||
}
|
||||
} else {
|
||||
// 编辑模式:传 discount_good_id
|
||||
submitData.discount_good_id = String(form.id)
|
||||
|
||||
// 根据 goods_type 判断传 good_id 还是 good_group_id
|
||||
if (form.goods_type === 'product') {
|
||||
submitData.good_id = String(form.goods_id)
|
||||
} else if (form.goods_type === 'product_group') {
|
||||
submitData.good_group_id = String(form.goods_id)
|
||||
} else {
|
||||
// 其他类型默认使用 good_id
|
||||
submitData.good_id = String(form.goods_id)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('提交商品关联数据:', submitData)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addDiscountGoods(submitData)
|
||||
} else {
|
||||
res = await updateDiscountGoods(submitData)
|
||||
}
|
||||
|
||||
console.log('提交响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchGoodsList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchVoucherListOptions()
|
||||
if (queryParams.code_id) {
|
||||
fetchGoodsList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discount-goods-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,913 @@
|
||||
<template>
|
||||
<div class="discount-users-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金卷" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherListOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
|
||||
<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="fetchUsersList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户关联列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<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
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="usersList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
|
||||
<el-table-column label="用户名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ 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>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getUserTypeTagByRow(row)">
|
||||
{{ getUserTypeNameByRow(row) }}
|
||||
</el-tag>
|
||||
</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="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑用户关联对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'"
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="代金券" prop="code_id">
|
||||
<el-select
|
||||
v-model="form.code_id"
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="dialogType === 'edit'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherListOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
|
||||
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
|
||||
<el-radio value="user">用户</el-radio>
|
||||
<el-radio value="user_group">用户组</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择用户" prop="selected_user" v-if="dialogType === 'add' && form.select_type === 'user'">
|
||||
<div class="user-selector-wrapper">
|
||||
<div class="selected-user-display" v-if="form.selected_user">
|
||||
<el-tag type="primary" closable @close="clearSelectedUser">
|
||||
{{ getSelectedUserName() }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openUserSelector"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
{{ form.selected_user ? '重新选择用户' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择用户组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'user_group'">
|
||||
<el-select
|
||||
v-model="form.selected_group"
|
||||
placeholder="请选择用户组"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleUserGroupChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userGroupOptions"
|
||||
:key="item.Id"
|
||||
:label="`${item.Name} (ID: ${item.Id})`"
|
||||
:value="item.Id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 编辑模式显示字段 -->
|
||||
<template v-if="dialogType === 'edit'">
|
||||
<el-form-item label="关联类型">
|
||||
<el-tag :type="form.select_type === 'user' ? 'primary' : 'warning'">
|
||||
{{ form.select_type === 'user' ? '用户' : '用户组' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id">
|
||||
<div class="user-selector-wrapper">
|
||||
<div class="selected-user-display" v-if="form.user_id">
|
||||
<el-tag type="primary" closable @close="clearSelectedUser">
|
||||
{{ getSelectedUserName() }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="openUserSelector" style="width: 100%;">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ form.user_id ? '重新选择用户' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.select_type === 'user_group'" label="用户组ID">
|
||||
<el-select v-model="form.user_group_id" placeholder="">
|
||||
<el-option v-for="item in userGroupOptions" :key="item.Id" :label="`${item.Name} (ID: ${item.Id})`" :value="item.Id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDiscountUsersList,
|
||||
addDiscountUsers,
|
||||
updateDiscountUsers,
|
||||
deleteDiscountUsers,
|
||||
getDiscountCodeList
|
||||
} from '@/api/admin/discount'
|
||||
import {
|
||||
getUserList,
|
||||
getUserGroupList
|
||||
} from '@/api/admin/user'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
code_id: props.codeId || '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchUsersList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
code_id: undefined,
|
||||
user_id: undefined,
|
||||
username: '',
|
||||
email: '',
|
||||
status: 1,
|
||||
select_type: 'user', // 选择类型:user 或 user_group
|
||||
selected_user: undefined, // 选中的用户ID
|
||||
selected_group: undefined, // 选中的用户组ID
|
||||
user_group_id: undefined // 用户组ID(用于提交)
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
code_id: [
|
||||
{ required: true, message: '请选择代金券', trigger: 'change' }
|
||||
],
|
||||
select_type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'change' }
|
||||
],
|
||||
selected_user: [
|
||||
{ required: true, message: '请选择用户', trigger: 'change' }
|
||||
],
|
||||
selected_group: [
|
||||
{ required: true, message: '请选择用户组', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const usersList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref(null)
|
||||
const voucherListOptions = ref([]) // 代金券列表选项
|
||||
const userOptions = ref([]) // 用户列表选项
|
||||
const userGroupOptions = ref([]) // 用户组列表选项
|
||||
|
||||
// 用户选择弹窗相关
|
||||
const userSelectorVisible = ref(false)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 获取用户类型名称(根据行数据)
|
||||
const getUserTypeNameByRow = (row) => {
|
||||
|
||||
//通过看是否有user对象参数判断是否为用户还是用户组类型
|
||||
if(row.user){
|
||||
return '用户'
|
||||
}else{
|
||||
return '用户组'
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 获取用户类型标签(根据行数据)
|
||||
const getUserTypeTagByRow = (row) => {
|
||||
|
||||
|
||||
if(row.user){
|
||||
return 'primary'
|
||||
}else{
|
||||
return 'warning'
|
||||
}
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 获取代金券列表选项
|
||||
const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
voucherListOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
ElMessage.error('获取代金券列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
console.log('获取用户列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
userOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户组列表
|
||||
const fetchUserGroupList = async () => {
|
||||
try {
|
||||
const res = await getUserGroupList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
key: ''
|
||||
})
|
||||
console.log('获取用户组列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
userGroupOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户组列表失败:', error)
|
||||
ElMessage.error('获取用户组列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开用户选择器
|
||||
const openUserSelector = () => {
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
form.selected_user = user.UserId
|
||||
form.user_id = user.UserId
|
||||
// 将选中的用户添加到 userOptions 中(如果不存在)
|
||||
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
userOptions.value.push(user)
|
||||
}
|
||||
userSelectorVisible.value = false
|
||||
ElMessage.success('用户选择成功')
|
||||
}
|
||||
|
||||
// 清除选中的用户
|
||||
const clearSelectedUser = () => {
|
||||
form.selected_user = undefined
|
||||
form.user_id = undefined
|
||||
}
|
||||
|
||||
|
||||
// 获取选中用户的显示名称
|
||||
const getSelectedUserName = () => {
|
||||
|
||||
let user;
|
||||
user = userOptions.value.find(u => u.UserId === form.selected_user)
|
||||
console.log("是否有用户的user_id信息",form.user_id)
|
||||
//需要进行判断是否是编辑的用户通过user_id进行刷选到所需要的用户对象信息
|
||||
if(form.user_id){
|
||||
console.log("用户表信息:",userOptions.value)
|
||||
user = userOptions.value.find(u => u.UserId === form.user_id)
|
||||
}
|
||||
|
||||
console.log("获取用户的名称",user)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : '未知用户'
|
||||
}
|
||||
|
||||
// 选择类型变化
|
||||
const handleSelectTypeChange = (type) => {
|
||||
form.selected_user = undefined
|
||||
form.selected_group = undefined
|
||||
form.user_id = undefined
|
||||
form.user_group_id = undefined
|
||||
}
|
||||
|
||||
// 选择用户变化
|
||||
const handleUserChange = (userId) => {
|
||||
const user = userOptions.value.find(item => item.UserId === userId)
|
||||
if (user) {
|
||||
form.user_id = user.UserId
|
||||
}
|
||||
}
|
||||
|
||||
// 选择用户组变化
|
||||
const handleUserGroupChange = (groupId) => {
|
||||
const group = userGroupOptions.value.find(item => item.Id === groupId)
|
||||
if (group) {
|
||||
form.user_group_id = group.Id
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户关联列表
|
||||
const fetchUsersList = async () => {
|
||||
if (!queryParams.code_id) {
|
||||
ElMessage.warning('请选择代金券进行查询')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDiscountUsersList(queryParams)
|
||||
console.log('用户关联列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
usersList.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
|
||||
fetchUsersList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.code_id = ''
|
||||
queryParams.page = 1
|
||||
usersList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchUsersList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchUsersList()
|
||||
}
|
||||
|
||||
// 新增用户关联
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
|
||||
user_id: undefined,
|
||||
username: '',
|
||||
email: '',
|
||||
status: 1,
|
||||
select_type: 'user',
|
||||
selected_user: undefined,
|
||||
selected_group: undefined,
|
||||
user_group_id: undefined
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
|
||||
fetchUserGroupList()
|
||||
}
|
||||
|
||||
// 编辑用户关联
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 判断是用户还是用户组
|
||||
let userId, userGroupId, selectType
|
||||
console.log("获取编辑的当前信息:",row)
|
||||
if (row.userId && row.userId !== 0) {
|
||||
// 用户
|
||||
userId = row.userId
|
||||
userGroupId = undefined
|
||||
selectType = 'user'
|
||||
} else if (row.userGroupId && row.userGroupId !== 0) {
|
||||
// 用户组
|
||||
userId = undefined
|
||||
userGroupId = row.userGroupId
|
||||
selectType = 'user_group'
|
||||
}
|
||||
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code_id: row.discountId,
|
||||
user_id: userId,
|
||||
user_group_id: userGroupId,
|
||||
select_type: selectType,
|
||||
username: '',
|
||||
email: '',
|
||||
status: 1
|
||||
})
|
||||
//点击编辑需要初始化加载用户列表
|
||||
fetchUserList()
|
||||
fetchUserGroupList()
|
||||
}
|
||||
|
||||
// 删除用户关联
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除该用户关联吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteDiscountUsers({
|
||||
discount_user_id: String(row.id),
|
||||
code_id: String(row.discountId)
|
||||
})
|
||||
console.log('删除响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchUsersList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteDiscountUsers({
|
||||
discount_user_id: String(row.id),
|
||||
code_id: String(row.discountId)
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(deletePromises)
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||||
} else {
|
||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
}
|
||||
|
||||
fetchUsersList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除操作异常')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
// 新增模式下的额外验证
|
||||
if (dialogType.value === 'add') {
|
||||
if (!form.code_id) {
|
||||
ElMessage.warning('请选择代金券')
|
||||
return
|
||||
}
|
||||
if (form.select_type === 'user' && !form.selected_user) {
|
||||
ElMessage.warning('请选择用户')
|
||||
return
|
||||
}
|
||||
if (form.select_type === 'user_group' && !form.selected_group) {
|
||||
ElMessage.warning('请选择用户组')
|
||||
return
|
||||
}
|
||||
if (!form.user_id && !form.user_group_id) {
|
||||
ElMessage.warning('请先选择用户或用户组')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
code_id: String(form.code_id)
|
||||
}
|
||||
|
||||
// 根据选择类型决定传 user_id 还是 user_group_id
|
||||
if (dialogType.value === 'add') {
|
||||
if (form.select_type === 'user') {
|
||||
// 选择的是用户,传 user_id
|
||||
submitData.user_id = String(form.user_id)
|
||||
} else if (form.select_type === 'user_group') {
|
||||
// 选择的是用户组,传 user_group_id
|
||||
submitData.user_group_id = String(form.user_group_id)
|
||||
}
|
||||
} else {
|
||||
// 编辑模式:根据类型传递对应的ID
|
||||
if (form.select_type === 'user') {
|
||||
submitData.user_id = String(form.user_id)
|
||||
} else if (form.select_type === 'user_group') {
|
||||
submitData.user_group_id = String(form.user_group_id)
|
||||
}
|
||||
// 编辑需要传 discount_user_id
|
||||
submitData.discount_user_id = String(form.id)
|
||||
}
|
||||
|
||||
console.log('提交用户关联数据:', submitData)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addDiscountUsers(submitData)
|
||||
} else {
|
||||
res = await updateDiscountUsers(submitData)
|
||||
}
|
||||
|
||||
console.log('提交响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchUsersList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchVoucherListOptions()
|
||||
if (queryParams.code_id) {
|
||||
fetchUsersList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discount-users-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 用户选择器样式 */
|
||||
.user-selector-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-user-display {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-selector-dialog .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: #ecf5ff !important;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.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,944 @@
|
||||
<template>
|
||||
<div class="user-voucher-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
|
||||
<el-form-item label="代金券" v-if="!codeId">
|
||||
<el-select
|
||||
v-model="queryParams.code_id"
|
||||
placeholder="请选择代金券"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 280px"
|
||||
@change="handleVoucherSelect"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherListOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
|
||||
<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="fetchUserVoucherList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户代金券列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userVoucherList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.Id || row.id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户ID" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.UserId || row.userId }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券ID" width="100" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discountId }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券名称" min-width="150" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discount?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面额" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已使用/最大次数" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expireAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加用户代金券对话框 -->
|
||||
<el-dialog
|
||||
v-model="addDialogVisible"
|
||||
title="为用户分发优惠券/优惠码"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="优惠类型" prop="discount_type">
|
||||
<el-radio-group v-model="addForm.discount_type" @change="handleDiscountTypeChange">
|
||||
<el-radio value="coupon">代金券</el-radio>
|
||||
<!-- <el-radio value="code">优惠码</el-radio> -->
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="代金券" prop="voucher_id">
|
||||
<el-select
|
||||
v-model="addForm.voucher_id"
|
||||
placeholder="请选择代金券"
|
||||
:disabled="addForm.discount_type === 'code' || !!codeId"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleVoucherChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in voucherOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item label="优惠码" prop="code_id">
|
||||
<el-select
|
||||
v-model="addForm.code_id"
|
||||
placeholder="请选择优惠码"
|
||||
:disabled="addForm.discount_type === 'coupon'"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleCodeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in codeOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.code})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-form-item label="分发对象" prop="target_type">
|
||||
<el-radio-group v-model="addForm.target_type" @change="handleTargetTypeChange">
|
||||
<el-radio value="user">指定用户</el-radio>
|
||||
<el-radio value="group">用户组</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div class="user-selector-wrapper">
|
||||
<div class="selected-user-display" v-if="addForm.user_id">
|
||||
<el-tag type="primary" closable @close="clearSelectedUser">
|
||||
{{ getSelectedUserName() }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openUserSelector"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
{{ addForm.user_id ? '重新选择用户' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户组" prop="group_id">
|
||||
<el-select
|
||||
v-model="addForm.group_id"
|
||||
placeholder="请选择用户组"
|
||||
:disabled="addForm.target_type === 'user'"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleGroupChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in groupOptions"
|
||||
:key="item.Id"
|
||||
:label="item.Name"
|
||||
:value="item.Id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAdd" :loading="submitLoading">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑用户代金券对话框 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
title="编辑用户代金券"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="用户ID" prop="user_id">
|
||||
<el-input-number v-model="editForm.user_id" :min="1" disabled style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="代金券ID" prop="discount_id">
|
||||
<el-input-number v-model="editForm.discount_id" :min="1" placeholder="请输入代金券ID" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已使用次数" prop="use_times">
|
||||
<el-input-number v-model="editForm.use_times" :min="0" placeholder="请输入已使用次数" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_use_times">
|
||||
<el-input-number v-model="editForm.max_use_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_at">
|
||||
<el-date-picker
|
||||
v-model="editForm.expire_at"
|
||||
type="datetime"
|
||||
placeholder="选择过期时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:teleported="true"
|
||||
placement="top-start"
|
||||
:editable="true"
|
||||
:clearable="true"
|
||||
style="width: 100%"
|
||||
@keyup.enter="handleDatePickerEnter"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserVoucherList,
|
||||
addUserVoucher,
|
||||
updateUserVoucher,
|
||||
deleteUserVoucher,
|
||||
getDiscountCodeList,
|
||||
getVoucherHolderList,
|
||||
allocateVoucher
|
||||
} from '@/api/admin/discount'
|
||||
import { getUserList, getUserGroupList } from '@/api/admin/user'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
code_id: props.codeId || undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
})
|
||||
|
||||
// 添加表单
|
||||
const addForm = reactive({
|
||||
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
|
||||
voucher_id: undefined, // 代金券ID
|
||||
code_id: undefined, // 优惠码ID
|
||||
target_type: 'user', // 分发对象:user-指定用户, group-用户组
|
||||
user_id: undefined, // 用户ID
|
||||
group_id: undefined // 用户组ID
|
||||
})
|
||||
|
||||
const addRules = {
|
||||
discount_type: [
|
||||
{ required: true, message: '请选择优惠类型', trigger: 'change' }
|
||||
],
|
||||
target_type: [
|
||||
{ required: true, message: '请选择分发对象', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 下拉选项数据
|
||||
const voucherListOptions = ref([]) // 用于搜索的代金券列表
|
||||
const voucherOptions = ref([]) // 代金券选项
|
||||
const codeOptions = ref([]) // 优惠码选项
|
||||
const userOptions = ref([]) // 用户选项
|
||||
const groupOptions = ref([]) // 用户组选项
|
||||
const userSearchLoading = ref(false) // 用户搜索加载状态
|
||||
const submitLoading = ref(false) // 提交加载状态
|
||||
const dataList = ref([]) // 优惠列表
|
||||
const userSelectorVisible = ref(false)
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
id: undefined,
|
||||
user_id: undefined,
|
||||
discount_id: undefined,
|
||||
use_times: 0,
|
||||
max_use_times: 0,
|
||||
expire_at: ''
|
||||
})
|
||||
|
||||
const editRules = {
|
||||
discount_id: [
|
||||
{ required: true, message: '请输入代金券ID', trigger: 'blur' }
|
||||
],
|
||||
use_times: [
|
||||
{ required: true, message: '请输入已使用次数', trigger: 'blur' }
|
||||
],
|
||||
max_use_times: [
|
||||
{ required: true, message: '请输入最大使用次数', trigger: 'blur' }
|
||||
],
|
||||
expire_at: [
|
||||
{ required: true, message: '请选择过期时间', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const userVoucherList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const addDialogVisible = ref(false)
|
||||
const editDialogVisible = ref(false)
|
||||
const addFormRef = ref(null)
|
||||
const editFormRef = ref(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
// 检查日期是否有效且不是默认的1970年
|
||||
if (isNaN(date.getTime()) || date.getFullYear() <= 1970) {
|
||||
return '-'
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理日期选择器回车事件
|
||||
const handleDatePickerEnter = (event) => {
|
||||
const datePicker = event.target.closest('.el-date-editor')
|
||||
if (datePicker) {
|
||||
event.target.blur()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代金券持有者列表
|
||||
const fetchUserVoucherList = async () => {
|
||||
if (!queryParams.code_id) {
|
||||
ElMessage.warning('请先选择代金券')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
code_id: queryParams.code_id,
|
||||
page: queryParams.page,
|
||||
count: queryParams.count
|
||||
}
|
||||
const res = await getVoucherHolderList(params)
|
||||
console.log('代金券持有者列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
userVoucherList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券持有者列表失败:', error)
|
||||
ElMessage.error('获取代金券持有者列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.code_id = undefined
|
||||
queryParams.page = 1
|
||||
userVoucherList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 处理代金券选择
|
||||
const handleVoucherSelect = (value) => {
|
||||
if (value) {
|
||||
queryParams.page = 1
|
||||
fetchUserVoucherList()
|
||||
} else {
|
||||
userVoucherList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
// 获取代金券列表(用于搜索下拉框)
|
||||
const fetchVoucherListOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 1000,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
voucherListOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
ElMessage.error('获取代金券列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
//获取优惠列表
|
||||
const fetchDiscountList = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
discount_type: 'coupon'
|
||||
})
|
||||
console.log('获取代金券列表:', res.data)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
voucherOptions.value = res.data.data?.data || []
|
||||
dataList.value.push(...res.data.data?.data || [])
|
||||
}
|
||||
const res2 = await getDiscountCodeList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
discount_type: 'code'
|
||||
})
|
||||
console.log('获取优惠码列表:', res2.data)
|
||||
if (res2.data.code === 200) {
|
||||
codeOptions.value = res2.data.data?.data || []
|
||||
dataList.value.push(...res2.data.data?.data || [])
|
||||
}
|
||||
console.log('获取优惠列表最终:', dataList.value)
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchVoucherOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 100
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
voucherOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠码列表
|
||||
const fetchCodeOptions = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'code',
|
||||
page: 1,
|
||||
count: 100
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
codeOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠码列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 100,
|
||||
key: ''
|
||||
})
|
||||
console.log('获取用户列表:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
userOptions.value = res.data.data?.data || []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
finally {
|
||||
userSearchLoading.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 获取用户组列表
|
||||
const fetchGroupOptions = async () => {
|
||||
try {
|
||||
const res = await getUserGroupList({
|
||||
page: 1,
|
||||
count: 100
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
groupOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理优惠类型变化(代金券/优惠码互斥)
|
||||
const handleDiscountTypeChange = (val) => {
|
||||
if (val === 'coupon') {
|
||||
addForm.code_id = undefined
|
||||
} else if (val === 'code') {
|
||||
addForm.voucher_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 处理代金券选择变化
|
||||
const handleVoucherChange = (val) => {
|
||||
if (val) {
|
||||
addForm.code_id = undefined
|
||||
addForm.discount_type = 'coupon'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理优惠码选择变化
|
||||
const handleCodeChange = (val) => {
|
||||
if (val) {
|
||||
addForm.voucher_id = undefined
|
||||
addForm.discount_type = 'code'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理分发对象类型变化(用户/用户组互斥)
|
||||
const handleTargetTypeChange = (val) => {
|
||||
if (val === 'user') {
|
||||
addForm.group_id = undefined
|
||||
} else if (val === 'group') {
|
||||
addForm.user_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户选择变化
|
||||
const handleUserChange = (val) => {
|
||||
if (val) {
|
||||
addForm.group_id = undefined
|
||||
addForm.target_type = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户组选择变化
|
||||
const handleGroupChange = (val) => {
|
||||
if (val) {
|
||||
addForm.user_id = undefined
|
||||
addForm.target_type = 'group'
|
||||
}
|
||||
}
|
||||
|
||||
// 打开用户选择器
|
||||
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 () => {
|
||||
addDialogVisible.value = true
|
||||
|
||||
// 重置表单
|
||||
Object.assign(addForm, {
|
||||
discount_type: 'coupon',
|
||||
voucher_id: props.codeId || undefined,
|
||||
code_id: undefined,
|
||||
target_type: 'user',
|
||||
user_id: undefined,
|
||||
group_id: undefined
|
||||
})
|
||||
addFormRef.value?.resetFields()
|
||||
|
||||
// 加载下拉选项数据
|
||||
await Promise.all([
|
||||
fetchVoucherOptions(),
|
||||
// fetchCodeOptions(),
|
||||
fetchGroupOptions(),
|
||||
fetchUserList()
|
||||
])
|
||||
}
|
||||
|
||||
// 编辑用户代金券
|
||||
const handleEdit = (row) => {
|
||||
editDialogVisible.value = true
|
||||
|
||||
// 处理过期时间 - 支持ISO字符串和时间戳
|
||||
let expireAt = ''
|
||||
if (row.expireAt) {
|
||||
try {
|
||||
// 如果是ISO字符串格式
|
||||
if (typeof row.expireAt === 'string') {
|
||||
const date = new Date(row.expireAt)
|
||||
if (!isNaN(date.getTime()) && date.getFullYear() > 1970) {
|
||||
expireAt = date.toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
}
|
||||
// 如果是时间戳
|
||||
else if (typeof row.expireAt === 'number' && row.expireAt > 0) {
|
||||
expireAt = new Date(row.expireAt * 1000).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('时间转换失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(editForm, {
|
||||
id: row.Id || row.id,
|
||||
user_id: row.UserId || row.userId,
|
||||
discount_id: row.discountId,
|
||||
use_times: row.useTimes || 0,
|
||||
max_use_times: row.maxUseTimes || 0,
|
||||
expire_at: expireAt
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户代金券
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除该用户代金券吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteUserVoucher({
|
||||
user_id: String(row.UserId || row.userId),
|
||||
id: String(row.Id || row.id)
|
||||
})
|
||||
console.log('删除响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteUserVoucher({
|
||||
user_id: String(row.UserId || row.userId),
|
||||
id: String(row.Id || row.id)
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(deletePromises)
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||||
} else {
|
||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
}
|
||||
|
||||
fetchUserVoucherList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除操作异常')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交添加表单
|
||||
const submitAdd = () => {
|
||||
addFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
// 验证是否选择了优惠券/优惠码
|
||||
const discountId = addForm.discount_type === 'coupon' ? addForm.voucher_id : addForm.code_id
|
||||
if (!discountId) {
|
||||
ElMessage.warning('请选择代金券或优惠码')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证是否选择了用户或用户组
|
||||
const targetId = addForm.target_type === 'user' ? addForm.user_id : addForm.group_id
|
||||
if (!targetId) {
|
||||
ElMessage.warning('请选择用户或用户组')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData = {
|
||||
code_id: String(discountId)
|
||||
}
|
||||
|
||||
// 根据分发对象添加不同参数
|
||||
if (addForm.target_type === 'user') {
|
||||
submitData.user_id = String(addForm.user_id)
|
||||
} else {
|
||||
submitData.user_group_id = String(addForm.group_id)
|
||||
}
|
||||
|
||||
console.log('分发优惠券/优惠码数据:', submitData)
|
||||
const res = await allocateVoucher(submitData)
|
||||
console.log('分发响应:', res.data)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('分发成功')
|
||||
addDialogVisible.value = false
|
||||
// 如果是为当前查询的用户分发,则刷新列表
|
||||
//if (addForm.target_type === 'user' && queryParams.user_id) {
|
||||
fetchUserVoucherList()
|
||||
//}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分发失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '分发失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交编辑表单
|
||||
const submitEdit = () => {
|
||||
editFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
user_id: Number(editForm.user_id),
|
||||
id: Number(editForm.id),
|
||||
discount_id: Number(editForm.discount_id),
|
||||
use_times: Number(editForm.use_times),
|
||||
max_use_times: Number(editForm.max_use_times),
|
||||
expire_at: Math.floor(new Date(editForm.expire_at).getTime() / 1000)
|
||||
}
|
||||
|
||||
console.log('更新用户代金券数据:', submitData)
|
||||
const res = await updateUserVoucher(submitData)
|
||||
console.log('更新响应:', res.data)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('更新成功')
|
||||
editDialogVisible.value = false
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '更新失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 加载代金券列表供选择
|
||||
fetchVoucherListOptions()
|
||||
fetchDiscountList()
|
||||
if (queryParams.code_id) {
|
||||
fetchUserVoucherList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-voucher-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;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div class="voucher-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增代金券
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchVoucherList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 代金券列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="voucherList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="代金券名称" min-width="200" />
|
||||
<el-table-column label="面额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ (row.minAmount / 100).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最大抵扣" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>无限制</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="maxTimes" label="最大使用次数" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.maxTimes || '无限制' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userTimes" label="单用户次数" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.userTimes || '无限制' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期(天)" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration ? (row.duration / 86400).toFixed(0) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续费可用" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
|
||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</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="700px"
|
||||
>
|
||||
<el-form
|
||||
ref="voucherFormRef"
|
||||
:model="voucherForm"
|
||||
:rules="voucherRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="代金券名称" prop="name">
|
||||
<el-input v-model="voucherForm.name" placeholder="请输入代金券名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="voucherForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="面额(元)" prop="amount">
|
||||
<el-input-number v-model="voucherForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最低消费(元)" prop="min_amount">
|
||||
<el-input-number v-model="voucherForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大抵扣(元)" prop="max_amount">
|
||||
<el-input-number v-model="voucherForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_times">
|
||||
<el-input-number v-model="voucherForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单用户最大次数" prop="user_times">
|
||||
<el-input-number v-model="voucherForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期(天)" prop="duration_days">
|
||||
<el-input-number v-model="voucherForm.duration_days" :min="1" placeholder="代金券有效天数" style="width: 100%" />
|
||||
<div class="form-tip">代金券领取后的有效持续时间</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="发放时间范围" prop="timeRange">
|
||||
<el-date-picker
|
||||
v-model="voucherForm.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:teleported="true"
|
||||
popper-class="voucher-date-picker"
|
||||
placement="top-start"
|
||||
:editable="true"
|
||||
:clearable="true"
|
||||
style="width: 100%"
|
||||
@keyup.enter="handleDatePickerEnter"
|
||||
/>
|
||||
<div class="form-tip">代金券可以发放给用户的时间范围</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="续费可用" prop="renew">
|
||||
<el-switch v-model="voucherForm.renew" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
<el-form-item label="同类型可叠加" prop="can_stacking">
|
||||
<el-switch v-model="voucherForm.can_stacking" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
<el-form-item label="其他类型可叠加" prop="can_combine">
|
||||
<el-switch v-model="voucherForm.can_combine" active-text="是" inactive-text="否" />
|
||||
</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>
|
||||
|
||||
<!-- 详情查看对话框 -->
|
||||
<DiscountDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
type="coupon"
|
||||
:detail-data="currentDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDiscountCodeList,
|
||||
getDiscountCodeDetail,
|
||||
createDiscountCode,
|
||||
updateDiscountCode,
|
||||
deleteDiscountCode
|
||||
} from '@/api/admin/discount'
|
||||
import { timeToTimestamp } from '@/utils/tool'
|
||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
discount_type: 'coupon', // 固定为coupon表示代金券
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 代金券表单
|
||||
const voucherForm = reactive({
|
||||
code_id: undefined,
|
||||
discount_type: 'coupon', // 固定为coupon
|
||||
name: '',
|
||||
note: '',
|
||||
amount: 0,
|
||||
min_amount: 0,
|
||||
max_amount: 0,
|
||||
max_times: 0,
|
||||
user_times: 0,
|
||||
duration_days: 30, // 默认30天
|
||||
timeRange: [],
|
||||
renew: false,
|
||||
can_stacking: false,
|
||||
can_combine: false
|
||||
})
|
||||
|
||||
const voucherRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入代金券名称', trigger: 'blur' }
|
||||
],
|
||||
amount: [
|
||||
{ required: true, message: '请输入面额', trigger: 'blur' }
|
||||
],
|
||||
duration_days: [
|
||||
{ required: true, message: '请输入有效期天数', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const voucherList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const voucherFormRef = ref(null)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref(null)
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchVoucherList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDiscountCodeList(queryParams)
|
||||
console.log('代金券列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
voucherList.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 handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 新增代金券
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(voucherForm, {
|
||||
code_id: undefined,
|
||||
discount_type: 'coupon',
|
||||
name: '',
|
||||
note: '',
|
||||
amount: 0,
|
||||
min_amount: 0,
|
||||
max_amount: 0,
|
||||
max_times: 0,
|
||||
user_times: 0,
|
||||
duration_days: 30,
|
||||
timeRange: [],
|
||||
renew: false,
|
||||
can_stacking: false,
|
||||
can_combine: false
|
||||
})
|
||||
voucherFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑代金券
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 转换日期字符串为日期选择器格式
|
||||
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
||||
|
||||
Object.assign(voucherForm, {
|
||||
code_id: row.id,
|
||||
discount_type: 'coupon',
|
||||
name: row.name,
|
||||
note: row.note || '',
|
||||
amount: row.amount ? row.amount / 100 : 0,
|
||||
min_amount: row.minAmount ? row.minAmount / 100 : 0,
|
||||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
||||
max_times: row.maxTimes || 0,
|
||||
user_times: row.userTimes || 0,
|
||||
duration_days: row.duration ? row.duration / 86400 : 30, // 秒转天
|
||||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
||||
renew: row.renew || false,
|
||||
can_stacking: row.canStacking || false,
|
||||
can_combine: row.canCombine || false
|
||||
})
|
||||
}
|
||||
|
||||
// 管理代金券
|
||||
const handleManage = (row) => {
|
||||
router.push(`/marketing/voucher/${row.id}/manage`)
|
||||
}
|
||||
|
||||
// 查看代金券详情
|
||||
const handleView = async (row) => {
|
||||
try {
|
||||
const res = await getDiscountCodeDetail({ code_id: row.id })
|
||||
console.log('代金券详情:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
currentDetail.value = res.data.data
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券详情失败:', error)
|
||||
ElMessage.error('获取代金券详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除代金券
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除代金券 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteDiscountCode({ code_id: row.id })
|
||||
console.log('删除响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchVoucherList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteDiscountCode({ code_id: row.id })
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(deletePromises)
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||||
} else {
|
||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
}
|
||||
|
||||
fetchVoucherList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除操作异常')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 处理日期选择器回车事件
|
||||
const handleDatePickerEnter = (event) => {
|
||||
// 回车键确认日期选择
|
||||
const datePicker = event.target.closest('.el-date-editor')
|
||||
if (datePicker) {
|
||||
// 触发失焦事件,确认日期选择
|
||||
event.target.blur()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交代金券表单
|
||||
const submitForm = () => {
|
||||
voucherFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
discount_type: 'coupon',
|
||||
name: voucherForm.name,
|
||||
note: voucherForm.note,
|
||||
amount: Math.round(voucherForm.amount * 100),
|
||||
percentage: 0, // 代金券固定为0
|
||||
min_amount: Math.round(voucherForm.min_amount * 100),
|
||||
max_amount: Math.round(voucherForm.max_amount * 100),
|
||||
max_times: voucherForm.max_times || 0,
|
||||
user_times: voucherForm.user_times || 0,
|
||||
duration: voucherForm.duration_days * 86400, // 天转秒
|
||||
renew: voucherForm.renew,
|
||||
can_stacking: voucherForm.can_stacking,
|
||||
can_combine: voucherForm.can_combine
|
||||
}
|
||||
|
||||
// 处理时间(转换为秒级时间戳)
|
||||
if (voucherForm.timeRange && voucherForm.timeRange.length === 2) {
|
||||
submitData.start_time = timeToTimestamp(voucherForm.timeRange[0])
|
||||
submitData.end_time = timeToTimestamp(voucherForm.timeRange[1])
|
||||
} else {
|
||||
submitData.start_time = ''
|
||||
submitData.end_time = ''
|
||||
}
|
||||
|
||||
// 如果是编辑,添加code_id
|
||||
if (dialogType.value === 'edit') {
|
||||
submitData.code_id = voucherForm.code_id
|
||||
}
|
||||
|
||||
console.log('提交代金券数据:', submitData)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createDiscountCode(submitData)
|
||||
} else {
|
||||
res = await updateDiscountCode(submitData)
|
||||
}
|
||||
|
||||
console.log('提交响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchVoucherList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchVoucherList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voucher-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 时间选择器弹出层样式 - 非 scoped */
|
||||
.voucher-date-picker {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.voucher-date-picker .el-picker-panel {
|
||||
max-width: 90vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<div class="voucher-history-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
|
||||
<el-form-item label="用户">
|
||||
<div class="user_selector-inline">
|
||||
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
|
||||
{{ getQueryUserName() }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openQueryUserSelector" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联信息ID">
|
||||
<el-input v-model="queryParams.id" placeholder="请输入关联信息ID" clearable style="width: 180px" />
|
||||
</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="success" @click="fetchHistoryList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 使用记录列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="historyList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="id" label="记录ID" width="80" fixed="left" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
|
||||
<el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
|
||||
<el-table-column label="优惠金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span>¥{{ row.order_amount ? (row.order_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_id" label="订单ID" width="150" />
|
||||
<el-table-column label="使用状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.used_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleView(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="detailDialogVisible"
|
||||
title="使用记录详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="记录ID">{{ currentDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ currentDetail.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ currentDetail.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ currentDetail.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="代金券ID">{{ currentDetail.discount_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="代金券名称">{{ currentDetail.discount_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="优惠金额">
|
||||
<span class="amount">¥{{ currentDetail.discount_amount ? (currentDetail.discount_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">
|
||||
<span>¥{{ currentDetail.order_amount ? (currentDetail.order_amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单ID" :span="2">{{ currentDetail.order_id || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="使用状态">
|
||||
<el-tag :type="getStatusType(currentDetail.status)">
|
||||
{{ getStatusText(currentDetail.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用时间">{{ formatDate(currentDetail.used_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentDetail.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ formatDate(currentDetail.updated_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ currentDetail.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="detailDialogVisible = false">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="总使用次数" :value="statistics.totalCount">
|
||||
<template #suffix>次</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="总优惠金额" :value="(statistics.totalAmount / 100).toFixed(2)">
|
||||
<template #prefix>¥</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="成功使用" :value="statistics.successCount">
|
||||
<template #suffix>次</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="失败/取消" :value="statistics.failedCount">
|
||||
<template #suffix>次</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, Download } from '@element-plus/icons-vue'
|
||||
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
|
||||
import { getUserList } from '@/api/admin/user'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
user_id: undefined,
|
||||
code_id: props.codeId || undefined,
|
||||
id: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
fetchHistoryList()
|
||||
}
|
||||
})
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const historyList = ref([])
|
||||
const total = ref(0)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref({})
|
||||
// const userOptions = ref([])
|
||||
const discountOptions = ref([])
|
||||
const selectorType = ref('query')
|
||||
const userSelectorVisible = ref(false)
|
||||
const UserOptions = ref([])
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
0: '未使用',
|
||||
1: '已使用',
|
||||
2: '使用失败',
|
||||
3: '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
0: 'info',
|
||||
1: 'success',
|
||||
2: 'danger',
|
||||
3: 'warning'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const statistics = computed(() => {
|
||||
const stats = {
|
||||
totalCount: historyList.value.length,
|
||||
totalAmount: 0,
|
||||
successCount: 0,
|
||||
failedCount: 0
|
||||
}
|
||||
|
||||
historyList.value.forEach(item => {
|
||||
if (item.status === 1) {
|
||||
stats.successCount++
|
||||
stats.totalAmount += item.discount_amount || 0
|
||||
} else if (item.status === 2 || item.status === 3) {
|
||||
stats.failedCount++
|
||||
}
|
||||
})
|
||||
|
||||
return stats
|
||||
})
|
||||
// 获取查询用户名称
|
||||
const getQueryUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
|
||||
}
|
||||
|
||||
// 获取使用记录列表
|
||||
const fetchHistoryList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
// 清除空参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await getUserVoucherHistory(params)
|
||||
console.log('使用记录数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
historyList.value = res.data.data?.list || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取使用记录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取使用记录失败:', error)
|
||||
ElMessage.error('获取使用记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 清除查询用户
|
||||
const clearQueryUser = () => {
|
||||
queryParams.user_id = undefined
|
||||
}
|
||||
// 重置用户搜索
|
||||
const resetUserSearch = () => {
|
||||
userSearchParams.key = ''
|
||||
userSearchParams.page = 1
|
||||
// fetchUserSelectorList()
|
||||
}
|
||||
|
||||
|
||||
// 打开查询用户选择器
|
||||
const openQueryUserSelector = () => {
|
||||
selectorType.value = 'query'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
// 打开编辑用户选择器
|
||||
const openEditUserSelector = () => {
|
||||
selectorType.value = 'edit'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
// 查询表单选择
|
||||
queryParams.user_id = user.UserId
|
||||
} else {
|
||||
// 编辑表单选择
|
||||
editForm.user_id = user.UserId
|
||||
}
|
||||
|
||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
UserOptions.value.push(user)
|
||||
}
|
||||
|
||||
userSelectorVisible.value = false
|
||||
ElMessage.success('用户选择成功')
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchHistoryList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.user_id = undefined
|
||||
queryParams.code_id = undefined
|
||||
queryParams.id = ''
|
||||
queryParams.page = 1
|
||||
fetchHistoryList()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchHistoryList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchHistoryList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = () => {
|
||||
if (historyList.value.length === 0) {
|
||||
ElMessage.warning('暂无数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
key: ''
|
||||
})
|
||||
UserOptions.value = res.data.data?.data || []
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchDiscountList = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 1000
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
discountOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
fetchDiscountList()
|
||||
// 默认加载第一页数据
|
||||
// fetchHistoryList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voucher-history-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;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__number) {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<div class="voucher-holders-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="用户">
|
||||
<div class="user-selector-inline">
|
||||
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px">
|
||||
{{ getQueryUserName() }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openQueryUserSelector" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>添加代金券
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchHoldersList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 拥有者列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="holdersList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="Id" label="ID" width="80" />
|
||||
<el-table-column prop="UserId" label="用户ID" min-width="100" />
|
||||
<el-table-column label="代金券ID" min-width="110" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discountId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.discount?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="代金券编码" min-width="150" v-if="!codeId">
|
||||
<template #default="{ row }">
|
||||
{{ row.discount?.code || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面额" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="useTimes" label="已使用" min-width="100" />
|
||||
<el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row)" size="small">
|
||||
{{ getStatusText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expireAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<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="detailDialogVisible"
|
||||
title="拥有者详情"
|
||||
width="700px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="记录ID">{{ currentDetail.Id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ currentDetail.UserId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="代金券ID">{{ currentDetail.discountId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="代金券编码">{{ currentDetail.discount?.code || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="代金券名称" :span="2">{{ currentDetail.discount?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="面额">
|
||||
<span class="amount">¥{{ currentDetail.discount?.amount ? (currentDetail.discount.amount / 100).toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最低消费">
|
||||
¥{{ currentDetail.discount?.minAmount ? (currentDetail.discount.minAmount / 100).toFixed(2) : '0.00' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(currentDetail)">
|
||||
{{ getStatusText(currentDetail) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ currentDetail.discount?.type === 'coupon' ? '代金券' : '优惠码' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用次数">{{ currentDetail.useTimes || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大使用次数">{{ currentDetail.maxUseTimes || '无限制' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentDetail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间" :span="2">{{ formatDate(currentDetail.expireAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ currentDetail.discount?.note || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="detailDialogVisible = false">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加/编辑代金券对话框 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
:title="dialogType === 'add' ? '添加代金券' : '编辑代金券'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="用户" prop="user_id" v-if="dialogType === 'add'">
|
||||
<div class="user-selector-wrapper">
|
||||
<div class="selected-user-display" v-if="editForm.user_id">
|
||||
<el-tag type="primary" closable @close="clearEditUser">
|
||||
{{ getEditUserName() }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openEditUserSelector"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
{{ editForm.user_id ? '重新选择用户' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="代金券" prop="discount_id">
|
||||
<el-select v-model="editForm.discount_id" placeholder="请选择代金券" filterable style="width: 100%" >
|
||||
<el-option v-for="item in discountOptions" :key="item.id" :label="`${item.name} (¥${(item.amount/100).toFixed(2)})`" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="已使用次数" prop="use_times" v-if="dialogType === 'edit'">
|
||||
<el-input-number v-model="editForm.use_times" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大使用次数" prop="max_use_times" v-if="dialogType === 'edit'">
|
||||
<el-input-number v-model="editForm.max_use_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_at" v-if="dialogType === 'edit'">
|
||||
<el-date-picker
|
||||
v-model="editForm.expire_at"
|
||||
type="datetime"
|
||||
placeholder="选择过期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:teleported="true"
|
||||
placement="top-start"
|
||||
:editable="true"
|
||||
:clearable="true"
|
||||
value-format="X"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEditForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelector
|
||||
v-model:visible="userSelectorVisible"
|
||||
@select="confirmUserSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserVoucherList,
|
||||
allocateVoucher,
|
||||
updateUserVoucher,
|
||||
deleteUserVoucher,
|
||||
getDiscountCodeList
|
||||
} from '@/api/admin/discount'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
codeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
user_id: undefined,
|
||||
code_id: props.codeId || undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
watch(() => props.codeId, (newVal) => {
|
||||
if (newVal) {
|
||||
queryParams.code_id = newVal
|
||||
// 如果有 code_id,尝试刷新列表(取决于 API 是否支持仅按 code_id 查询)
|
||||
// 如果 API 必须要求 user_id,则这里可能不需要立即刷新,或者提示用户选择用户
|
||||
if (queryParams.user_id) {
|
||||
fetchHoldersList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const holdersList = ref([])
|
||||
const total = ref(0)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref({})
|
||||
const UserOptions = ref([])
|
||||
const editDialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const editFormRef = ref(null)
|
||||
const discountOptions = ref([])
|
||||
|
||||
// 用户选择弹窗相关
|
||||
const userSelectorVisible = ref(false)
|
||||
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
user_id: undefined,
|
||||
discount_id: undefined,
|
||||
id: undefined,
|
||||
use_times: 0,
|
||||
max_use_times: 0,
|
||||
expire_at: undefined
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const editRules = {
|
||||
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||
discount_id: [{ required: true, message: '请选择代金券', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr || dateStr === '1970-01-01T08:01:40+08:00' || dateStr === '0001-01-01T00:00:00Z') return '-'
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
const year = date.getFullYear()
|
||||
// 过滤掉1970年的日期(通常是零值)
|
||||
if (year === 1970) return '-'
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (row) => {
|
||||
const now = Date.now()
|
||||
const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0
|
||||
|
||||
// 检查是否已用完
|
||||
if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) {
|
||||
return '已用完'
|
||||
}
|
||||
// 检查是否已过期(排除1970年的零值日期)
|
||||
if (expireAt > 0 && expireAt < now && new Date(row.expireAt).getFullYear() !== 1970) {
|
||||
return '已过期'
|
||||
}
|
||||
return '可使用'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (row) => {
|
||||
const now = Date.now()
|
||||
const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0
|
||||
|
||||
if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) {
|
||||
return 'info'
|
||||
}
|
||||
if (expireAt > 0 && expireAt < now && new Date(row.expireAt).getFullYear() !== 1970) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 获取拥有者列表
|
||||
const fetchHoldersList = async () => {
|
||||
if (!queryParams.user_id) {
|
||||
ElMessage.warning('请选择用户ID进行查询')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
// 清除空参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await getUserVoucherList(params)
|
||||
console.log('拥有者列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
holdersList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
console.log('解析后的列表数据:', holdersList.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取拥有者列表失败:', error)
|
||||
ElMessage.error('获取拥有者列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchHoldersList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.code_id = ''
|
||||
queryParams.username = ''
|
||||
queryParams.page = 1
|
||||
holdersList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchHoldersList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchHoldersList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = () => {
|
||||
ElMessage.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 打开查询用户选择器
|
||||
const openQueryUserSelector = () => {
|
||||
selectorType.value = 'query'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑用户选择器
|
||||
const openEditUserSelector = () => {
|
||||
selectorType.value = 'edit'
|
||||
userSelectorVisible.value = true
|
||||
}
|
||||
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = (user) => {
|
||||
if (!user) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
// 查询表单选择
|
||||
queryParams.user_id = user.UserId
|
||||
} else {
|
||||
// 编辑表单选择
|
||||
editForm.user_id = user.UserId
|
||||
}
|
||||
|
||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
|
||||
UserOptions.value.push(user)
|
||||
}
|
||||
|
||||
userSelectorVisible.value = false
|
||||
ElMessage.success('用户选择成功')
|
||||
}
|
||||
|
||||
// 清除查询用户
|
||||
const clearQueryUser = () => {
|
||||
queryParams.user_id = undefined
|
||||
}
|
||||
|
||||
// 清除编辑用户
|
||||
const clearEditUser = () => {
|
||||
editForm.user_id = undefined
|
||||
}
|
||||
|
||||
// 获取查询用户名称
|
||||
const getQueryUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
|
||||
}
|
||||
|
||||
// 获取编辑用户名称
|
||||
const getEditUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === editForm.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${editForm.user_id}`
|
||||
}
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchDiscountList = async () => {
|
||||
try {
|
||||
const res = await getDiscountCodeList({
|
||||
discount_type: 'coupon',
|
||||
page: 1,
|
||||
count: 1000
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
discountOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加代金券
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
editDialogVisible.value = true
|
||||
Object.assign(editForm, {
|
||||
user_id: queryParams.user_id,
|
||||
discount_id: undefined,
|
||||
id: undefined,
|
||||
use_times: 0,
|
||||
max_use_times: 0,
|
||||
expire_at: undefined
|
||||
})
|
||||
editFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
editDialogVisible.value = true
|
||||
|
||||
// 处理过期时间
|
||||
let expireTime = undefined
|
||||
if (row.expireAt && row.expireAt !== '1970-01-01T08:01:40+08:00' && row.expireAt !== '0001-01-01T00:00:00Z') {
|
||||
const date = new Date(row.expireAt)
|
||||
if (!isNaN(date.getTime()) && date.getFullYear() !== 1970) {
|
||||
expireTime = Math.floor(date.getTime() / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(editForm, {
|
||||
user_id: row.UserId,
|
||||
discount_id: row.discountId,
|
||||
id: row.Id,
|
||||
use_times: row.useTimes || 0,
|
||||
max_use_times: row.maxUseTimes || 0,
|
||||
expire_at: expireTime
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
const discountName = row.discount?.name || '该代金券'
|
||||
ElMessageBox.confirm(`确认删除用户ID ${row.UserId} 的代金券 ${discountName} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteUserVoucher({
|
||||
user_id: row.UserId,
|
||||
id: row.Id
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchHoldersList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitEditForm = () => {
|
||||
editFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
// 使用 allocateVoucher 接口为用户添加代金券
|
||||
res = await allocateVoucher({
|
||||
user_id: editForm.user_id,
|
||||
code_id: editForm.discount_id
|
||||
})
|
||||
} else {
|
||||
res = await updateUserVoucher({
|
||||
user_id: editForm.user_id,
|
||||
id: editForm.id,
|
||||
discount_id: editForm.discount_id,
|
||||
use_times: editForm.use_times,
|
||||
max_use_times: editForm.max_use_times,
|
||||
expire_at: editForm.expire_at
|
||||
})
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '修改成功')
|
||||
editDialogVisible.value = false
|
||||
fetchHoldersList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchDiscountList()
|
||||
if (queryParams.user_id) {
|
||||
fetchHoldersList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voucher-holders-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;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 用户选择器样式 */
|
||||
.user-selector-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-selector-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-user-display {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-selector-dialog .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: #ecf5ff !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,654 @@
|
||||
<template>
|
||||
<div class="order-list-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增订单
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchOrderList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading" class="skeleton-container">
|
||||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
||||
<div class="skeleton-cell skeleton-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
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="orderList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="订单ID" width="100" />
|
||||
<el-table-column prop="name" label="订单名称" min-width="180" />
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column prop="commodityId" label="商品ID" width="100" />
|
||||
<el-table-column label="表名" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.table || '未知' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ (row.price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续费价格" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="renew-price">¥{{ (row.renewPrice / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="80">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.payNum }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.state)">
|
||||
{{ getStatusText(row.state) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付方式" width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.payType || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" width="170">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.expireTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.CreatedAt) }}</span>
|
||||
</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="handleView(row)">查看</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="订单详情"
|
||||
width="800px"
|
||||
append-to-body
|
||||
>
|
||||
<el-descriptions :column="2" border v-if="orderDetail">
|
||||
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单名称">{{ orderDetail.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ orderDetail.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品ID">{{ orderDetail.commodityId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="表名">{{ orderDetail.table }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ orderDetail.payNum }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">¥{{ (orderDetail.price / 100).toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="续费价格">¥{{ (orderDetail.renewPrice / 100).toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<el-tag :type="getStatusType(orderDetail.state)">
|
||||
{{ getStatusText(orderDetail.state) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付方式">{{ orderDetail.payType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">{{ formatDate(orderDetail.expireTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.UpdatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参数信息">{{ orderDetail.args || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ orderDetail.note || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
|
||||
width="700px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="orderFormRef"
|
||||
:model="orderForm"
|
||||
:rules="orderRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="订单名称" prop="name">
|
||||
<el-input v-model="orderForm.name" placeholder="请输入订单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属表" prop="table">
|
||||
<el-input v-model="orderForm.table" placeholder="请输入所属表" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID" prop="user_id">
|
||||
<el-input-number v-model="orderForm.user_id" :min="1" placeholder="请输入用户ID" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品ID" prop="commodity_id">
|
||||
<el-input-number v-model="orderForm.commodity_id" :min="0" placeholder="请输入商品ID" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="购买数量" prop="pay_num">
|
||||
<el-input-number v-model="orderForm.pay_num" :min="1" placeholder="请输入数量" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格(分)" prop="price">
|
||||
<el-input-number v-model="orderForm.price" :min="0" placeholder="请输入价格(分)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="续费价格(分)" prop="renew_price">
|
||||
<el-input-number v-model="orderForm.renew_price" :min="0" placeholder="请输入续费价格(分)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_time">
|
||||
<el-input-number v-model="orderForm.expire_time" :min="0" placeholder="请输入过期时间(时间戳)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠码ID" prop="discount_code_id">
|
||||
<el-input-number v-model="orderForm.discount_code_id" :min="0" placeholder="请输入优惠码ID" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="代金券ID" prop="coupon_id">
|
||||
<el-input-number v-model="orderForm.coupon_id" :min="0" placeholder="请输入代金券ID (必填)" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态" prop="state">
|
||||
<el-radio-group v-model="orderForm.state">
|
||||
<el-radio :label="0">待支付</el-radio>
|
||||
<el-radio :label="1">已支付</el-radio>
|
||||
<el-radio :label="2">已失效</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付方式" prop="pay_type">
|
||||
<el-input v-model="orderForm.pay_type" placeholder="请输入支付类型" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单参数" prop="args">
|
||||
<el-input v-model="orderForm.args" placeholder="请输入订单参数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="orderForm.note" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Search, Download, Refresh } from '@element-plus/icons-vue'
|
||||
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } from '@/api/admin/order'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 订单表单
|
||||
const orderForm = reactive({
|
||||
order_id: undefined,
|
||||
name: '',
|
||||
table: '',
|
||||
user_id: undefined,
|
||||
commodity_id: 0,
|
||||
pay_num: 1,
|
||||
price: 0,
|
||||
renew_price: 0,
|
||||
expire_time: 0,
|
||||
discount_code_id: 0,
|
||||
coupon_id: 0,
|
||||
state: 0,
|
||||
pay_type: '',
|
||||
args: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
const orderRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入订单名称', trigger: 'blur' }
|
||||
],
|
||||
table: [
|
||||
{ required: true, message: '请输入所属表', trigger: 'blur' }
|
||||
],
|
||||
user_id: [
|
||||
{ required: true, message: '请输入用户ID', trigger: 'blur' },
|
||||
{ type: 'number', message: '用户ID必须是数字', trigger: 'blur' }
|
||||
],
|
||||
coupon_id: [
|
||||
{ required: true, message: '请输入代金券ID', trigger: 'blur' },
|
||||
{ type: 'number', message: '代金券ID必须是数字', trigger: 'blur' }
|
||||
],
|
||||
pay_num: [
|
||||
{ required: true, message: '请输入购买数量', trigger: 'blur' }
|
||||
],
|
||||
price: [
|
||||
{ required: true, message: '请输入价格', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const orderList = ref([])
|
||||
const orderDetail = ref(null)
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const orderFormRef = ref(null)
|
||||
|
||||
// 获取订单列表
|
||||
const fetchOrderList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getOrderList(queryParams)
|
||||
console.log('订单列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
orderList.value = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error)
|
||||
ElMessage.error('获取订单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 获取订单状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
0: 'warning', // 待支付
|
||||
1: 'success', // 已支付
|
||||
2: 'info', // 已失效
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取订单状态文本
|
||||
// state 0:未支付 1:已支付 2:已失效
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
0: '待支付',
|
||||
1: '已支付',
|
||||
2: '已失效'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchOrderList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.order_no = ''
|
||||
queryParams.user_id = ''
|
||||
queryParams.status = ''
|
||||
queryParams.dateRange = []
|
||||
queryParams.page = 1
|
||||
fetchOrderList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchOrderList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchOrderList()
|
||||
}
|
||||
|
||||
// 新增订单
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(orderForm, {
|
||||
order_id: undefined,
|
||||
name: '',
|
||||
table: '',
|
||||
user_id: undefined,
|
||||
commodity_id: 0,
|
||||
pay_num: 1,
|
||||
price: 0,
|
||||
renew_price: 0,
|
||||
expire_time: 0,
|
||||
discount_code_id: 0,
|
||||
coupon_id: 0,
|
||||
state: 0,
|
||||
pay_type: '',
|
||||
args: '',
|
||||
note: ''
|
||||
})
|
||||
orderFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const handleView = async (row) => {
|
||||
try {
|
||||
const res = await getOrderDetail({ order_id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
orderDetail.value = res.data.data
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error)
|
||||
ElMessage.error('获取订单详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑订单
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
Object.assign(orderForm, {
|
||||
order_id: row.id,
|
||||
name: row.name,
|
||||
table: row.table,
|
||||
user_id: row.userId,
|
||||
commodity_id: row.commodityId,
|
||||
pay_num: row.payNum,
|
||||
price: row.price,
|
||||
renew_price: row.renewPrice,
|
||||
expire_time: row.expireTime ? new Date(row.expireTime).getTime() / 1000 : 0,
|
||||
discount_code_id: 0, // 从详情接口获取
|
||||
coupon_id: 0, // 从详情接口获取
|
||||
state: row.state,
|
||||
pay_type: row.payType || '',
|
||||
args: row.args || '',
|
||||
note: row.note || ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除订单 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteOrder({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchOrderList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('批量删除成功')
|
||||
fetchOrderList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
orderFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
name: orderForm.name,
|
||||
table: orderForm.table,
|
||||
user_id: Number(orderForm.user_id),
|
||||
commodity_id: Number(orderForm.commodity_id),
|
||||
pay_num: Number(orderForm.pay_num),
|
||||
price: Number(orderForm.price),
|
||||
renew_price: Number(orderForm.renew_price),
|
||||
expire_time: Number(orderForm.expire_time),
|
||||
discount_code_id: Number(orderForm.discount_code_id),
|
||||
coupon_id: Number(orderForm.coupon_id),
|
||||
state: Number(orderForm.state),
|
||||
pay_type: orderForm.pay_type || '',
|
||||
args: orderForm.args || '',
|
||||
note: orderForm.note || ''
|
||||
}
|
||||
|
||||
// 如果是编辑,添加order_id
|
||||
if (dialogType.value === 'edit') {
|
||||
submitData.order_id = Number(orderForm.order_id)
|
||||
}
|
||||
|
||||
console.log('提交订单数据:', submitData)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createOrder(submitData)
|
||||
} else {
|
||||
res = await updateOrder(submitData)
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchOrderList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchOrderList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-list-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: 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;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.renew-price {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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: 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>
|
||||
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div class="product-group-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增商品分组
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchGroupList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品分组列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading" class="skeleton-container">
|
||||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
||||
<div class="skeleton-cell skeleton-id"></div>
|
||||
<div class="skeleton-cell skeleton-name"></div>
|
||||
<div class="skeleton-cell skeleton-note"></div>
|
||||
<div class="skeleton-cell skeleton-status"></div>
|
||||
<div class="skeleton-cell skeleton-action"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="分组ID" width="100" />
|
||||
<el-table-column prop="name" label="分组名称" min-width="200" />
|
||||
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.disable"
|
||||
:active-value="false"
|
||||
:inactive-value="true"
|
||||
@change="(val) => handleStatusChange(row, val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品分组表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增商品分组' : '编辑商品分组'"
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="groupFormRef"
|
||||
:model="groupForm"
|
||||
:rules="groupRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分组名称" prop="name">
|
||||
<el-input v-model="groupForm.name" placeholder="请输入分组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="groupForm.note" type="textarea" :rows="4" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="disable">
|
||||
<el-radio-group v-model="groupForm.disable">
|
||||
<el-radio :label="false">启用</el-radio>
|
||||
<el-radio :label="true">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProductGroupList,
|
||||
createProductGroup,
|
||||
updateProductGroup,
|
||||
deleteProductGroup,
|
||||
hideProductGroup,
|
||||
startProductGroup
|
||||
} from '@/api/admin/product'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 商品分组表单
|
||||
const groupForm = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: '',
|
||||
disable: false
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入分组名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const groupFormRef = ref(null)
|
||||
|
||||
// 获取商品分组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductGroupList(queryParams)
|
||||
if (res.data.code === 200) {
|
||||
groupList.value = res.data.data.data || []
|
||||
total.value = res.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取商品分组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 新增商品分组
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(groupForm, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: '',
|
||||
disable: false
|
||||
})
|
||||
groupFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑商品分组
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
Object.assign(groupForm, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
note: row.note,
|
||||
disable: row.disable
|
||||
})
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (row, disable) => {
|
||||
try {
|
||||
let res
|
||||
if (disable === false) {
|
||||
// 启用
|
||||
res = await startProductGroup({ id: row.id })
|
||||
} else {
|
||||
// 禁用
|
||||
res = await hideProductGroup({ id: row.id })
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('状态修改成功')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('状态修改失败')
|
||||
row.disable = !disable // 恢复原状态
|
||||
}
|
||||
}
|
||||
|
||||
// 删除商品分组
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除商品分组 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteProductGroup({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
groupFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createProductGroup(groupForm)
|
||||
} else {
|
||||
res = await updateProductGroup(groupForm)
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-group-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: 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;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skeleton-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.skeleton-cell {
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-id { width: 100px; }
|
||||
.skeleton-name { width: 200px; }
|
||||
.skeleton-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>
|
||||
File diff suppressed because it is too large
Load Diff
+146
-127
@@ -1,77 +1,76 @@
|
||||
<template>
|
||||
<div class="global-setting-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="left">
|
||||
<h2 class="title">全局设置</h2>
|
||||
<el-tag type="info" effect="plain" class="info-tag">系统全局配置管理</el-tag>
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="设置名称">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限">
|
||||
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable style="width: 120px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="公有" value="0" />
|
||||
<el-option label="私有" value="1" />
|
||||
</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-icon><Delete /></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 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">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="设置名称">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限">
|
||||
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="公有" value="0" />
|
||||
<el-option label="私有" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
|
||||
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card> -->
|
||||
|
||||
<!-- 设置列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="settingsList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<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="value" label="Value值" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="权限" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAuthorityType(row.authority)" size="small">
|
||||
{{ getAuthorityText(row.authority) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<!-- 设置列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="settingsList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<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="value" label="Value值" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="权限" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAuthorityType(row.authority)" size="small">
|
||||
{{ getAuthorityText(row.authority) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<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 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>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑设置对话框 -->
|
||||
@@ -256,13 +255,17 @@ const getAuthorityText = (authority) => {
|
||||
return authority === 0 ? '公有' : '私有'
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.name = ''
|
||||
queryParams.authority = ''
|
||||
getList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
@@ -387,65 +390,83 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.global-setting-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 120px);
|
||||
background-color: #f5f7fa;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.info-tag {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 筛选容器 */
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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 {
|
||||
@@ -455,20 +476,18 @@ onMounted(() => {
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.page-header {
|
||||
.filter-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
.search-form {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
.action-bar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,62 +1,73 @@
|
||||
<template>
|
||||
<div class="domain-whitelist-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入域名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>新增域名
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="queryParams.domain" placeholder="请输入域名" clearable />
|
||||
</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="getList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 域名列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="domainList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="Id" label="ID" width="80" />
|
||||
<el-table-column prop="Domain" label="域名" min-width="200" >
|
||||
<template #default="{ row }">
|
||||
<el-link :href="`http://${row.Domain}`" target="_blank" type="primary">{{ row.Domain }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="CreatedAt" label="创建时间" width="180" :formatter="parseCreatedAt" />
|
||||
<el-table-column prop="UpdatedAt" label="更新时间" width="180" :formatter="parseUpdatedAt" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
<!-- 域名列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="domainList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="domain" label="域名" min-width="200" >
|
||||
<template #default="{ row }">
|
||||
<el-link :href="`http://${row.domain}`" target="_blank" type="primary">{{ row.domain }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="CreatedAt" label="创建时间" width="180" :formatter="parseCreatedAt" />
|
||||
<el-table-column prop="UpdatedAt" label="更新时间" width="180" :formatter="parseUpdatedAt" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 域名表单对话框 -->
|
||||
@@ -89,7 +100,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
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'
|
||||
|
||||
// 查询参数
|
||||
@@ -223,9 +234,14 @@ const handleDelete = (row) => {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDomain(row.Id)
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
const res = await deleteDomain({domain_id: row.id})
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除域名失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
@@ -239,10 +255,11 @@ const handleBatchDelete = () => {
|
||||
ElMessage.warning('请选择要删除的域名')
|
||||
return
|
||||
}
|
||||
|
||||
const ids = selectedRows.value.map(item => item.Id)
|
||||
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
|
||||
const domains = selectedRows.value.map(item => item.domain).join('、')
|
||||
|
||||
console.log('id数据:',ids)
|
||||
ElMessageBox.confirm(`确认删除以下域名吗?\n${domains}`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -257,9 +274,7 @@ const handleBatchDelete = () => {
|
||||
console.error('批量删除域名失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
await batchDeleteDomain(ids)
|
||||
ElMessage.success('批量删除成功')
|
||||
getList()
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量删除域名失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
@@ -275,35 +290,93 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.domain-whitelist-container {
|
||||
padding: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 10px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
</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>
|
||||
+148
-165
@@ -1,89 +1,98 @@
|
||||
<template>
|
||||
<div class="operation-log">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="操作人">
|
||||
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable>
|
||||
<el-option label="登录" value="login" />
|
||||
<el-option label="新增" value="create" />
|
||||
<el-option label="修改" value="update" />
|
||||
<el-option label="删除" value="delete" />
|
||||
</el-select>
|
||||
</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-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="success">
|
||||
<el-icon><upload /></el-icon>导入
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleExport">
|
||||
<el-icon><download /></el-icon>导出
|
||||
</el-button>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="操作人">
|
||||
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable style="width: 160px">
|
||||
<el-option label="登录" value="login" />
|
||||
<el-option label="新增" value="create" />
|
||||
<el-option label="修改" value="update" />
|
||||
<el-option label="删除" value="delete" />
|
||||
</el-select>
|
||||
</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"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</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="success">
|
||||
<el-icon><Upload /></el-icon>导入
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="logList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="操作人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="operator-info">
|
||||
<el-avatar :size="32" :src="row.avatar"></el-avatar>
|
||||
<div class="operator-detail">
|
||||
<div class="username">{{ row.operator }}</div>
|
||||
<div class="ip">{{ row.ip }}</div>
|
||||
<!-- 日志列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="logList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="操作人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="operator-info">
|
||||
<el-avatar :size="32" :src="row.avatar"></el-avatar>
|
||||
<div class="operator-detail">
|
||||
<div class="username">{{ row.operator }}</div>
|
||||
<div class="ip">{{ row.ip }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="操作类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTag(row.type)">{{ getTypeText(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="操作描述" min-width="200" />
|
||||
<el-table-column prop="createTime" label="操作时间" width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="操作类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTag(row.type)">{{ getTypeText(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="操作描述" min-width="200" />
|
||||
<el-table-column prop="createTime" label="操作时间" width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
@@ -117,7 +126,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
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({
|
||||
@@ -247,57 +256,87 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-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 {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #f8f9fb !important;
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
height: 50px;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
: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) {
|
||||
background: #f8fafc;
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover > td) {
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr) {
|
||||
transition: all 0.3s ease;
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.operator-info {
|
||||
@@ -322,37 +361,6 @@ onMounted(() => {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -367,29 +375,4 @@ onMounted(() => {
|
||||
white-space: pre-wrap;
|
||||
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>
|
||||
@@ -0,0 +1,840 @@
|
||||
<template>
|
||||
<div class="permission-admin-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 150px" @change="handleOwnerTypeChange">
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="组" value="group" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
|
||||
<div class="user_selector-inline">
|
||||
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
|
||||
{{ getQueryUserName() }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openQueryUserSelector" size="default">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'">
|
||||
<el-select v-model="queryParams.admin_group_id" placeholder="请选择管理员组" clearable filterable style="width: 200px">
|
||||
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</el-select>
|
||||
</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="fetchAdminPermissionList">
|
||||
<el-icon><Refresh/></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员权限列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="adminPermissionList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="拥有者类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ownerType === 'user' ? 'primary' : 'success'">
|
||||
{{ row.ownerType === 'user' ? '用户' : '组' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="拥有者" width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.ownerType === 'user'">用户ID: {{ row.userId }}</span>
|
||||
<span v-else>管理员组ID: {{ row.groupId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="permissionId" label="路径权限ID" width="120" />
|
||||
<el-table-column label="权限路径" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.permission?.path || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="权限名称" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.permission?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="weight" label="权重" width="100" />
|
||||
<el-table-column label="权限类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPermissionTypeTag(row.permissionType)">
|
||||
{{ getPermissionTypeText(row.permissionType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expireAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="userSelectorVisible"
|
||||
title="选择用户"
|
||||
width="800px"
|
||||
class="user-selector-dialog"
|
||||
>
|
||||
<!-- 搜索栏 -->
|
||||
<div class="selector-search">
|
||||
<el-input
|
||||
v-model="userSearchParams.key"
|
||||
placeholder="搜索用户名或ID"
|
||||
clearable
|
||||
@keyup.enter="searchUsers"
|
||||
style="width: 300px; margin-right: 12px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="searchUsers">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetUserSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table
|
||||
v-loading="userSelectorLoading"
|
||||
:data="userSelectorList"
|
||||
highlight-current-row
|
||||
@current-change="handleUserSelectChange"
|
||||
style="width: 100%; margin-top: 16px"
|
||||
:height="400"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="UserId" label="用户ID" width="100" />
|
||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
||||
<el-table-column prop="Email" label="邮箱" min-width="180" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.Status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="userSearchParams.page"
|
||||
v-model:page-size="userSearchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="userSelectorTotal"
|
||||
@size-change="handleUserSelectorSizeChange"
|
||||
@current-change="handleUserSelectorPageChange"
|
||||
background
|
||||
class="selector-pagination"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="userSelectorVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
|
||||
确定选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 分配权限对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="分配管理员权限"
|
||||
width="700px"
|
||||
>
|
||||
<el-form
|
||||
ref="permissionFormRef"
|
||||
:model="permissionForm"
|
||||
:rules="permissionRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="权限绑定类型" prop="owner_type">
|
||||
<el-select v-model="permissionForm.owner_type" placeholder="请选择权限绑定类型" style="width: 100%" @change="handleFormOwnerTypeChange" :disabled="permissionForm.id">
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="组" value="group" />
|
||||
</el-select>
|
||||
<div class="form-tip">如果是 user 则填写 user_id,如果是 group 则填写 admin_group_id</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" prop="user_id" v-if="permissionForm.owner_type === 'user'" >
|
||||
<div class="user_selector-inline">
|
||||
<el-tag v-if="permissionForm.user_id" type="primary" closable @close="clearFormUser" style="margin-right: 8px;">
|
||||
{{ getFormUserName() }}
|
||||
</el-tag>
|
||||
<el-button type="primary" plain @click="openFormUserSelector" size="default" :disabled="permissionForm.user_id">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ permissionForm.user_id ? '重新选择' : '选择用户' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员组" prop="admin_group_id" v-if="permissionForm.owner_type === 'group'">
|
||||
<el-select v-model="permissionForm.admin_group_id" placeholder="请选择管理员组" filterable style="width: 100%">
|
||||
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="路径权限" prop="permission_id">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-select
|
||||
v-model="permissionForm.permission_id"
|
||||
placeholder="请选择路径权限"
|
||||
filterable
|
||||
style="flex: 1"
|
||||
:loading="permissionLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in permissionOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
<el-tag v-if="item.method" :type="getMethodTag(item.method)" size="small" style="margin-right: 8px;">{{ item.method }}</el-tag>
|
||||
{{ item.path }}
|
||||
</span>
|
||||
<span style="color: #999; font-size: 12px; margin-left: 12px;">{{ item.note || item.name || `ID: ${item.id}` }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button @click="fetchPermissionList" :loading="permissionLoading" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
<div class="form-tip">共 {{ permissionOptions.length }} 个路径权限可选</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="权重" prop="weight">
|
||||
<el-input-number v-model="permissionForm.weight" placeholder="请输入权重" :min="0" style="width: 100%" />
|
||||
<div class="form-tip">权重默认 10</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限类型" prop="permission_type">
|
||||
<el-select v-model="permissionForm.permission_type" placeholder="请选择权限类型" style="width: 100%">
|
||||
<el-option label="无权限 (none)" :value="0" />
|
||||
<el-option label="禁止 (prohibit)" :value="1" />
|
||||
<el-option label="读取 (read)" :value="2" />
|
||||
<el-option label="写入 (write)" :value="3" />
|
||||
<el-option label="全部 (all)" :value="4" />
|
||||
</el-select>
|
||||
<div class="form-tip">0 none / 1 prohibit / 2 read / 3 write / 4 all</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_at">
|
||||
<el-date-picker
|
||||
v-model="permissionForm.expire_at"
|
||||
type="datetime"
|
||||
placeholder="请选择过期时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">权限过期时间 不填写则不会过期</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, User } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getPermissionListByAdmin,
|
||||
addPermissionAdmin,
|
||||
updatePermissionAdmin,
|
||||
deletePermissionAdmin,
|
||||
getPermissionList
|
||||
} from '@/api/admin/Permission'
|
||||
import { getUserList } from '@/api/admin/user'
|
||||
import { getAdminGroupList } from '@/api/admin/group'
|
||||
import { formatDate ,timeToTimestamp} from '@/utils/tool'
|
||||
|
||||
|
||||
const selectorType = ref('query')
|
||||
const userSelectorVisible = ref(false)
|
||||
const userSelectorList = ref([])
|
||||
const userSelectorTotal = ref(0)
|
||||
const userSearchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
const selectedUserTemp = ref(null)
|
||||
const userSelectorLoading = ref(false)
|
||||
const UserOptions = ref([])
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
owner_type: '',
|
||||
user_id: undefined,
|
||||
admin_group_id: undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
// 清除查询用户
|
||||
const clearQueryUser = () => {
|
||||
queryParams.user_id = undefined
|
||||
}
|
||||
// 获取查询用户名称
|
||||
const getQueryUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
|
||||
}
|
||||
// 表单:清除用户
|
||||
const clearFormUser = () => {
|
||||
permissionForm.user_id = undefined
|
||||
}
|
||||
// 表单:获取显示名称
|
||||
const getFormUserName = () => {
|
||||
const user = UserOptions.value.find(u => u.UserId === permissionForm.user_id)
|
||||
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${permissionForm.user_id}`
|
||||
}
|
||||
// 确认用户选择
|
||||
const confirmUserSelection = () => {
|
||||
if (!selectedUserTemp.value) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectorType.value === 'query') {
|
||||
// 查询表单选择,仅影响查询条件
|
||||
queryParams.user_id = selectedUserTemp.value.UserId
|
||||
} else if (selectorType.value === 'form') {
|
||||
// 表单选择,仅影响表单
|
||||
permissionForm.user_id = selectedUserTemp.value.UserId
|
||||
}
|
||||
|
||||
// 将选中的用户添加到 UserOptions 中(如果不存在)
|
||||
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
|
||||
UserOptions.value.push(selectedUserTemp.value)
|
||||
}
|
||||
|
||||
userSelectorVisible.value = false
|
||||
ElMessage.success('用户选择成功')
|
||||
}
|
||||
// 打开查询用户选择器
|
||||
const openQueryUserSelector = () => {
|
||||
selectorType.value = 'query'
|
||||
userSelectorVisible.value = true
|
||||
selectedUserTemp.value = null
|
||||
userSearchParams.key = ''
|
||||
userSearchParams.page = 1
|
||||
fetchUserSelectorList()
|
||||
}
|
||||
// 打开表单用户选择器
|
||||
const openFormUserSelector = () => {
|
||||
selectorType.value = 'form'
|
||||
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 permissionForm = reactive({
|
||||
id: undefined,
|
||||
user_id: undefined,
|
||||
admin_group_id: undefined,
|
||||
owner_type: '',
|
||||
permission_id: undefined,
|
||||
weight: 10,
|
||||
permission_type: 4,
|
||||
expire_at: ''
|
||||
})
|
||||
|
||||
const permissionRules = {
|
||||
owner_type: [
|
||||
{ required: true, message: '请选择权限的绑定类型', trigger: 'change' }
|
||||
],
|
||||
permission_id: [
|
||||
{ required: true, message: '请输入权限ID', trigger: 'blur' }
|
||||
],
|
||||
permission_type: [
|
||||
{ required: true, message: '请选择权限类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const adminPermissionList = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const permissionFormRef = ref(null)
|
||||
// const userOptions = ref([])
|
||||
// const UserOptions = ref([])
|
||||
const adminGroupOptions = ref([])
|
||||
const permissionOptions = ref([])
|
||||
const permissionLoading = ref(false)
|
||||
|
||||
// 获取方法标签颜色
|
||||
const getMethodTag = (method) => {
|
||||
const tagMap = {
|
||||
'GET': 'success',
|
||||
'POST': 'primary',
|
||||
'PUT': 'warning',
|
||||
'DELETE': 'danger',
|
||||
'PATCH': 'info'
|
||||
}
|
||||
return tagMap[method?.toUpperCase()] || 'info'
|
||||
}
|
||||
|
||||
// 获取权限类型标签颜色
|
||||
const getPermissionTypeTag = (type) => {
|
||||
const tagMap = {
|
||||
0: 'info', // none
|
||||
1: 'danger', // prohibit
|
||||
2: 'warning', // read
|
||||
3: 'primary', // write
|
||||
4: 'success' // all
|
||||
}
|
||||
return tagMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取权限类型文本
|
||||
const getPermissionTypeText = (type) => {
|
||||
const textMap = {
|
||||
0: '无权限',
|
||||
1: '禁止',
|
||||
2: '读取',
|
||||
3: '写入',
|
||||
4: '全部'
|
||||
}
|
||||
return textMap[type] || '未知'
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取管理员权限列表
|
||||
const fetchAdminPermissionList = async () => {
|
||||
if (!queryParams.owner_type) {
|
||||
ElMessage.warning('请先选择类型')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
// 清除空参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await getPermissionListByAdmin(params)
|
||||
console.log('管理员权限列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
adminPermissionList.value = res.data.data || []
|
||||
total.value = res.data.data.length || 0
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取管理员权限列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取管理员权限列表失败:', error)
|
||||
ElMessage.error('获取管理员权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchAdminPermissionList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.user_id = undefined
|
||||
queryParams.admin_group_id = undefined
|
||||
queryParams.owner_type = ''
|
||||
queryParams.page = 1
|
||||
adminPermissionList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 类型变化时清空关联字段
|
||||
const handleOwnerTypeChange = () => {
|
||||
if (queryParams.owner_type === 'user') {
|
||||
queryParams.admin_group_id = undefined
|
||||
} else if (queryParams.owner_type === 'group') {
|
||||
queryParams.user_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 表单类型变化时清空关联字段
|
||||
const handleFormOwnerTypeChange = () => {
|
||||
if (permissionForm.owner_type === 'user') {
|
||||
permissionForm.admin_group_id = undefined
|
||||
} else if (permissionForm.owner_type === 'group') {
|
||||
permissionForm.user_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchAdminPermissionList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchAdminPermissionList()
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
const handleAdd = () => {
|
||||
dialogVisible.value = true
|
||||
Object.assign(permissionForm, {
|
||||
id: undefined,
|
||||
user_id: undefined,
|
||||
admin_group_id: undefined,
|
||||
owner_type: '',
|
||||
permission_id: undefined,
|
||||
weight: 10,
|
||||
permission_type: 4,
|
||||
expire_at: ''
|
||||
})
|
||||
permissionFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑权限
|
||||
const handleEdit = (row) => {
|
||||
// 处理过期时间
|
||||
let expireTime = ''
|
||||
if (row.expireAt && row.expireAt !== '0001-01-01T00:00:00Z' && row.expireAt !== null) {
|
||||
const date = new Date(row.expireAt)
|
||||
if (!isNaN(date.getTime())) {
|
||||
expireTime = date.toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(permissionForm, {
|
||||
id: row.id,
|
||||
user_id: row.userId,
|
||||
admin_group_id: row.groupId,
|
||||
owner_type: row.ownerType,
|
||||
permission_id: row.permissionId,
|
||||
weight: row.weight,
|
||||
permission_type: row.permissionType,
|
||||
expire_at: expireTime
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除权限
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除该权限吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deletePermissionAdmin({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchAdminPermissionList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
permissionFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
owner_type: permissionForm.owner_type,
|
||||
permission_id: Number(permissionForm.permission_id),
|
||||
weight: Number(permissionForm.weight),
|
||||
permission_type: Number(permissionForm.permission_type)
|
||||
}
|
||||
|
||||
// 根据 owner_type 添加对应的 ID
|
||||
if (permissionForm.owner_type === 'user') {
|
||||
submitData.user_id = Number(permissionForm.user_id)
|
||||
} else if (permissionForm.owner_type === 'group') {
|
||||
submitData.admin_group_id = Number(permissionForm.admin_group_id)
|
||||
}
|
||||
|
||||
// 将过期时间转换为时间戳(秒)
|
||||
if (permissionForm.expire_at) {
|
||||
submitData.expire_at = timeToTimestamp(permissionForm.expire_at)
|
||||
}
|
||||
|
||||
// 如果是编辑,添加ID
|
||||
if (permissionForm.id) {
|
||||
submitData.id = permissionForm.id
|
||||
}
|
||||
|
||||
console.log('提交管理员权限数据:', submitData)
|
||||
|
||||
let res
|
||||
if (permissionForm.id) {
|
||||
res = await updatePermissionAdmin(submitData)
|
||||
} else {
|
||||
res = await addPermissionAdmin(submitData)
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(permissionForm.id ? '修改成功' : '分配成功')
|
||||
dialogVisible.value = false
|
||||
fetchAdminPermissionList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: 1,
|
||||
count: 10000,
|
||||
key: ''
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
UserOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取管理员组列表
|
||||
const fetchAdminGroupList = async () => {
|
||||
try {
|
||||
const res = await getAdminGroupList({
|
||||
page: 1,
|
||||
count: 1000
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
adminGroupOptions.value = res.data.data?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取管理员组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取路径权限列表
|
||||
const fetchPermissionList = async () => {
|
||||
permissionLoading.value = true
|
||||
try {
|
||||
const res = await getPermissionList({
|
||||
page: 1,
|
||||
count: 10000
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
permissionOptions.value = res.data.data?.list || []
|
||||
console.log('路径权限列表加载成功,共', permissionOptions.value.length, '条')
|
||||
if (dialogVisible.value) {
|
||||
ElMessage.success(`已加载 ${permissionOptions.value.length} 个路径权限`)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取路径权限列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取路径权限列表失败:', error)
|
||||
ElMessage.error('获取路径权限列表失败')
|
||||
} finally {
|
||||
permissionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
fetchAdminGroupList()
|
||||
fetchPermissionList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-admin-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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>
|
||||
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="permission-route-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词搜索" clearable style="width: 250px" />
|
||||
</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="fetchPermissionList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路由权限列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="permissionList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="权限名称" min-width="200" />
|
||||
<el-table-column prop="path" label="路由路径" min-width="300" />
|
||||
<el-table-column prop="note" label="说明" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 路由权限表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增路由权限' : '编辑路由权限'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="permissionFormRef"
|
||||
:model="permissionForm"
|
||||
:rules="permissionRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="权限名称" prop="name">
|
||||
<el-input v-model="permissionForm.name" placeholder="请输入权限名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由路径" prop="path">
|
||||
<el-input v-model="permissionForm.path" placeholder="请输入路由路径,如: /api/v1/admin/..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明" prop="note">
|
||||
<el-input v-model="permissionForm.note" type="textarea" :rows="3" placeholder="请输入说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getPermissionList,
|
||||
addPermissionInfo,
|
||||
updatePermissionInfo,
|
||||
deletePermissionInfo
|
||||
} from '@/api/admin/Permission'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 路由权限表单
|
||||
const permissionForm = reactive({
|
||||
id: undefined,
|
||||
path: '',
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
const permissionRules = {
|
||||
path: [
|
||||
{ required: true, message: '请输入路由路径', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入权限名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const permissionList = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const permissionFormRef = ref(null)
|
||||
|
||||
// 获取路由权限列表
|
||||
const fetchPermissionList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPermissionList(queryParams)
|
||||
console.log('路由权限列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
permissionList.value = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取路由权限列表失败:', error)
|
||||
ElMessage.error('获取路由权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchPermissionList()
|
||||
}
|
||||
|
||||
// 新增路由权限
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(permissionForm, {
|
||||
id: undefined,
|
||||
path: '',
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
permissionFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑路由权限
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
Object.assign(permissionForm, {
|
||||
id: row.id,
|
||||
path: row.path,
|
||||
name: row.name,
|
||||
note: row.note || ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除路由权限
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除路由权限 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deletePermissionInfo({ id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchPermissionList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
permissionFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
console.log('提交路由权限数据:', permissionForm)
|
||||
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addPermissionInfo(permissionForm)
|
||||
} else {
|
||||
res = await updatePermissionInfo(permissionForm)
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchPermissionList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchPermissionList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-route-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,600 @@
|
||||
<template>
|
||||
<div class="setting-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="配置组">
|
||||
<el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery">
|
||||
<el-option
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词筛选">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增配置
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="settingList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="150" />
|
||||
<el-table-column prop="value" label="值" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'bool'">{{ row.value ? '是' : '否' }}</span>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeColor(row.type)">
|
||||
{{ row.type || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="settingGroupID" label="配置组" width="150" />
|
||||
<el-table-column label="是否开放" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.open"
|
||||
@change="handleToggleOpen(row)"
|
||||
:disabled="toggleLoading === row.id"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 配置表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="settingFormRef"
|
||||
:model="settingForm"
|
||||
:rules="settingRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="配置组" prop="settingGroupID">
|
||||
<el-select v-model="settingForm.settingGroupID" placeholder="请选择配置组" style="width: 100%">
|
||||
<el-option
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="settingForm.name" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="settingForm.type" placeholder="请选择类型" style="width: 100%" @change="handleTypeChange">
|
||||
<el-option label="字符串 (string)" value="string" />
|
||||
<el-option label="整数 (int)" value="int" />
|
||||
<el-option label="浮点数 (float)" value="float" />
|
||||
<el-option label="布尔值 (bool)" value="bool" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="value">
|
||||
<el-input
|
||||
v-if="settingForm.type === 'string'"
|
||||
v-model="settingForm.value"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="settingForm.type === 'int'"
|
||||
v-model="settingForm.value"
|
||||
:controls="false"
|
||||
placeholder="请输入整数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="settingForm.type === 'float'"
|
||||
v-model="settingForm.value"
|
||||
:controls="false"
|
||||
:precision="2"
|
||||
placeholder="请输入浮点数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="settingForm.type === 'bool'"
|
||||
v-model="settingForm.value"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="settingForm.value"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开放访问">
|
||||
<el-switch v-model="settingForm.open" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||||
开启后允许公开访问
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input
|
||||
v-model="settingForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSettingList,
|
||||
getSettingInfo,
|
||||
createSetting,
|
||||
updateSetting,
|
||||
setSettingOpen,
|
||||
deleteSetting
|
||||
} from '@/api/admin/setting'
|
||||
import { getSettingGroupList } from '@/api/admin/setting'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
group_id: undefined,
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 配置表单
|
||||
const settingForm = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'string',
|
||||
settingGroupID: undefined,
|
||||
open: false,
|
||||
note: ''
|
||||
})
|
||||
|
||||
const settingRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置名称', trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置值', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||
],
|
||||
settingGroupID: [
|
||||
{ required: true, message: '请选择配置组', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const settingList = ref([])
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增配置')
|
||||
const settingFormRef = ref(null)
|
||||
const toggleLoading = ref(null)
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'string': 'primary',
|
||||
'int': 'success',
|
||||
'float': 'warning',
|
||||
'bool': 'info'
|
||||
}
|
||||
return colorMap[type] || ''
|
||||
}
|
||||
|
||||
// 获取配置组列表
|
||||
const fetchGroupList = async () => {
|
||||
try {
|
||||
const res = await getSettingGroupList({ page: 1, count: 1000 })
|
||||
if (res.data.code === 200) {
|
||||
groupList.value = res.data.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置列表
|
||||
const fetchSettingList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
if (!params.group_id) {
|
||||
delete params.group_id
|
||||
}
|
||||
const res = await getSettingList(params)
|
||||
console.log('配置列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
settingList.value = res.data.data.data || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置列表失败:', error)
|
||||
ElMessage.error('获取配置列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.group_id = undefined
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchSettingList()
|
||||
}
|
||||
|
||||
// 类型变化
|
||||
const handleTypeChange = (type) => {
|
||||
// 根据类型重置值
|
||||
if (type === 'bool') {
|
||||
settingForm.value = false
|
||||
} else if (type === 'int' || type === 'float') {
|
||||
settingForm.value = 0
|
||||
} else {
|
||||
settingForm.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 新增配置
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增配置'
|
||||
Object.assign(settingForm, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'string',
|
||||
setting_group_id: undefined,
|
||||
open: false,
|
||||
note: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑配置'
|
||||
try {
|
||||
const res = await getSettingInfo({ id: row.id })
|
||||
console.log('配置详情数据:', res)
|
||||
if (res.data.code === 200) {
|
||||
const data = res.data.data
|
||||
Object.assign(settingForm, {
|
||||
id: data.id,
|
||||
name: data.name || '',
|
||||
value: data.value,
|
||||
type: data.type || 'string',
|
||||
settingGroupID: data.settingGroupID,
|
||||
open: data.open || false,
|
||||
note: data.note || ''
|
||||
})
|
||||
console.log('配置详情数据:', settingForm)
|
||||
// 根据类型转换值
|
||||
if (data.type === 'bool') {
|
||||
settingForm.value = data.value === true || data.value === 'true' || data.value === 1
|
||||
} else if (data.type === 'int') {
|
||||
settingForm.value = parseInt(data.value) || 0
|
||||
} else if (data.type === 'float') {
|
||||
settingForm.value = parseFloat(data.value) || 0
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置详情失败:', error)
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换开放状态
|
||||
const handleToggleOpen = async (row) => {
|
||||
toggleLoading.value = row.id
|
||||
try {
|
||||
const res = await setSettingOpen({
|
||||
id: row.id,
|
||||
open: row.open
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
// 恢复原状态
|
||||
row.open = !row.open
|
||||
ElMessage.error(res.data.message || '修改失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复原状态
|
||||
row.open = !row.open
|
||||
console.error('修改失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '修改失败')
|
||||
} finally {
|
||||
toggleLoading.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除配置 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteSetting({ id: row.id })
|
||||
console.log('删除配置响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchSettingList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteSetting({ id: row.id })
|
||||
)
|
||||
await Promise.all(deletePromises)
|
||||
ElMessage.success('批量删除成功')
|
||||
fetchSettingList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
settingFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
name: settingForm.name,
|
||||
value: String(settingForm.value),
|
||||
type: settingForm.type,
|
||||
setting_group_id: settingForm.settingGroupID,
|
||||
open: settingForm.open,
|
||||
note: settingForm.note
|
||||
}
|
||||
if (settingForm.id) {
|
||||
submitData.id = settingForm.id
|
||||
}
|
||||
console.log('提交配置数据:', submitData)
|
||||
const res = settingForm.id
|
||||
? await updateSetting(submitData)
|
||||
: await createSetting(submitData)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(settingForm.id ? '修改成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchSettingList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '提交失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
fetchSettingList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div class="setting-group-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="关键词筛选">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增配置组
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置组列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="200" />
|
||||
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.UpdatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 配置组表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="groupFormRef"
|
||||
:model="groupForm"
|
||||
:rules="groupRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="groupForm.name" placeholder="请输入配置组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input
|
||||
v-model="groupForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSettingGroupList,
|
||||
getSettingGroupInfo,
|
||||
createSettingGroup,
|
||||
updateSettingGroup,
|
||||
deleteSettingGroup
|
||||
} from '@/api/admin/setting'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 配置组表单
|
||||
const groupForm = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置组名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增配置组')
|
||||
const groupFormRef = ref(null)
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取配置组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSettingGroupList(queryParams)
|
||||
console.log('配置组列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
groupList.value = res.data.data.data || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置组列表失败:', error)
|
||||
ElMessage.error('获取配置组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 新增配置组
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增配置组'
|
||||
Object.assign(groupForm, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑配置组
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑配置组'
|
||||
try {
|
||||
const res = await getSettingGroupInfo({ setting_group_id: row.id })
|
||||
console.log('配置组详情数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
Object.assign(groupForm, {
|
||||
id: res.data.data.id,
|
||||
name: res.data.data.name || '',
|
||||
note: res.data.data.note || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置组详情失败:', error)
|
||||
ElMessage.error('获取配置组详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置组
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除配置组 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteSettingGroup({ setting_group_id: row.id })
|
||||
console.log('删除配置组响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const deletePromises = selectedRows.value.map(row =>
|
||||
deleteSettingGroup({ setting_group_id: row.id })
|
||||
)
|
||||
await Promise.all(deletePromises)
|
||||
ElMessage.success('批量删除成功')
|
||||
fetchGroupList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
groupFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
name: groupForm.name,
|
||||
note: groupForm.note
|
||||
}
|
||||
if (groupForm.id) {
|
||||
submitData.id = groupForm.id
|
||||
}
|
||||
console.log('提交配置组数据:', submitData)
|
||||
const res = groupForm.id
|
||||
? await updateSettingGroup(submitData)
|
||||
: await createSettingGroup(submitData)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(groupForm.id ? '修改成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '提交失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-group-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,835 @@
|
||||
<template>
|
||||
<div class="system-file-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="关键词筛选">
|
||||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="筛选用户">
|
||||
<el-input-number v-model="queryParams.user_id" placeholder="请输入用户ID" :controls="false" clearable style="width: 150px" />
|
||||
</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="handleUpload">
|
||||
<el-icon><Upload /></el-icon>上传文件
|
||||
</el-button>
|
||||
|
||||
<el-button type="success" @click="fetchFileList">
|
||||
<el-icon><Refresh/></el-icon>刷新
|
||||
</el-button>
|
||||
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="fileList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="realName" label="真实文件名" min-width="200" />
|
||||
<el-table-column prop="saveName" label="保存名称" min-width="150" />
|
||||
<el-table-column prop="savePath" label="保存路径" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="size" label="文件大小" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="文件类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getFileTypeColor(row.type)">
|
||||
{{ row.type || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column label="是否公开" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.openDow ? 'success' : 'info'">
|
||||
{{ row.openDow ? '公开' : '私有' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||
<el-button type="success" link @click="handleDownload(row)">下载</el-button>
|
||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 文件详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="文件详情"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="fileDetail" class="file-detail-container">
|
||||
<!-- 文件预览区域 -->
|
||||
<div class="file-preview-section">
|
||||
<div class="preview-label">文件预览</div>
|
||||
<div class="preview-content">
|
||||
<el-image
|
||||
v-if="isImageFile(fileDetail.type) && fileDetail.url"
|
||||
:src="fileDetail.url"
|
||||
fit="contain"
|
||||
style="max-width: 100%; max-height: 400px; border-radius: 8px;"
|
||||
:preview-src-list="[fileDetail.url]"
|
||||
:initial-index="0"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon size="40"><Picture /></el-icon>
|
||||
<div>图片加载失败</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="file-icon-large">
|
||||
<el-icon size="80"><Document /></el-icon>
|
||||
<div class="file-type-text">{{ fileDetail.type || '未知类型' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<el-descriptions :column="2" border class="file-info-descriptions">
|
||||
<el-descriptions-item label="文件ID" label-align="right">{{ fileDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID" label-align="right">{{ fileDetail.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实文件名" label-align="right" :span="2">{{ fileDetail.realName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="保存名称" label-align="right">{{ fileDetail.saveName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型" label-align="right">
|
||||
<el-tag :type="getFileTypeColor(fileDetail.type)">{{ fileDetail.type || '未知' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小" label-align="right">{{ formatFileSize(fileDetail.size) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否公开" label-align="right">
|
||||
<el-tag :type="fileDetail.openDow ? 'success' : 'info'">
|
||||
{{ fileDetail.openDow ? '公开访问' : '私有' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="保存路径" label-align="right" :span="2">
|
||||
<span class="file-path">{{ fileDetail.savePath }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件URL" label-align="right" :span="2">
|
||||
<el-link :href="fileDetail.url" target="_blank" type="primary" v-if="fileDetail.url">
|
||||
点击查看文件
|
||||
</el-link>
|
||||
<span v-else style="color: #909399;">无URL</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" label-align="right">{{ formatDate(fileDetail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" label-align="right">{{ formatDate(fileDetail.UpdatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" label-align="right" :span="2">{{ fileDetail.content || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文件编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
title="编辑文件信息"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="fileFormRef"
|
||||
:model="fileForm"
|
||||
:rules="fileRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="文件ID">
|
||||
<el-input v-model="fileForm.file_id" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID" prop="user_id">
|
||||
<el-input-number v-model="fileForm.user_id" placeholder="请输入用户ID" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否允许公开">
|
||||
<el-switch v-model="fileForm.open_dow" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||||
开启后允许公开访问
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEditForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文件上传对话框 -->
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
title="上传文件"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="uploadFormRef"
|
||||
:model="uploadForm"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="上传类型" prop="update_type">
|
||||
<el-select v-model="uploadForm.update_type" placeholder="请选择上传类型" style="width: 100%">
|
||||
<el-option label="工单文件" value="work_order" />
|
||||
<el-option label="封面" value="cover" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开放下载">
|
||||
<el-switch v-model="uploadForm.open_down" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||||
开启后允许公开下载
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传文件">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:http-request="handleCustomUpload"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:on-remove="handleRemoveFile"
|
||||
:on-change="handleFileChange"
|
||||
:before-upload="beforeUpload"
|
||||
:file-list="uploadFileList"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
multiple
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 jpg/png/gif/pdf/doc/docx 文件,且不超过 10MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleCloseUpload">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitUpload">确定上传</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
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'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
user_id: undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 文件表单
|
||||
const fileForm = reactive({
|
||||
file_id: undefined,
|
||||
user_id: undefined,
|
||||
open_dow: false
|
||||
})
|
||||
|
||||
const fileRules = {
|
||||
user_id: [
|
||||
{ required: true, message: '请输入用户ID', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const fileList = ref([])
|
||||
const fileDetail = ref(null)
|
||||
const total = ref(0)
|
||||
const selectedRows = ref([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const editDialogVisible = ref(false)
|
||||
const uploadDialogVisible = ref(false)
|
||||
const fileFormRef = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const uploadFormRef = ref(null)
|
||||
|
||||
// 上传表单
|
||||
const uploadForm = reactive({
|
||||
update_type: 'work_order',
|
||||
open_down: false
|
||||
})
|
||||
|
||||
// 上传文件列表
|
||||
const uploadFileList = ref([])
|
||||
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (type) => {
|
||||
const imageTypes = ['cover', 'image', 'avatar', 'photo', 'picture']
|
||||
return imageTypes.includes(type?.toLowerCase())
|
||||
}
|
||||
|
||||
// 获取文件类型颜色
|
||||
const getFileTypeColor = (type) => {
|
||||
if (isImageFile(type)) return 'success'
|
||||
const colorMap = {
|
||||
'document': 'primary',
|
||||
'video': 'warning',
|
||||
'audio': 'info',
|
||||
'file': ''
|
||||
}
|
||||
return colorMap[type?.toLowerCase()] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (size < 1024) return size + ' B'
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
|
||||
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
|
||||
}
|
||||
|
||||
// 获取文件列表
|
||||
const fetchFileList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFileList(queryParams)
|
||||
console.log('文件列表数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
fileList.value = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
ElMessage.error('获取文件列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.user_id = undefined
|
||||
queryParams.page = 1
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 选择项变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 查看文件详情
|
||||
const handleView = async (row) => {
|
||||
try {
|
||||
const res = await getFileDetail({ file_id: row.id })
|
||||
console.log('文件详情数据:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
fileDetail.value = res.data.data.data
|
||||
fileDetail.value.url = res.data.data.url
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件详情失败:', error)
|
||||
ElMessage.error('获取文件详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = async (row) => {
|
||||
try {
|
||||
// 先获取文件详情以获取完整URL
|
||||
const res = await getFileDetail({ file_id: row.id })
|
||||
if (res.data.code === 200 && res.data.data.url) {
|
||||
const link = document.createElement('a')
|
||||
link.href = res.data.data.url
|
||||
link.download = row.realName
|
||||
link.target = '_blank'
|
||||
link.click()
|
||||
ElMessage.success('开始下载文件')
|
||||
} else {
|
||||
ElMessage.error('获取文件下载链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error)
|
||||
ElMessage.error('下载文件失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑文件
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(fileForm, {
|
||||
file_id: row.id,
|
||||
user_id: row.userId || undefined,
|
||||
open_dow: row.openDow || false
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除文件 ${row.realName} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteFile({ file_id: row.id })
|
||||
console.log('删除文件响应:', res.data)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchFileList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
console.log("批量选择的值:",selectedRows.value)
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
try{
|
||||
|
||||
const deleteMap = selectedRows.value.map(f => deleteFile({file_id:f.id}))
|
||||
//等待所有删除完毕
|
||||
await Promise.all(deleteMap)
|
||||
ElMessage.success('批量删除成功')
|
||||
//刷新文件列表
|
||||
fetchFileList()
|
||||
}catch(error){
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||
}
|
||||
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = () => {
|
||||
uploadForm.update_type = 'work_order'
|
||||
uploadForm.open_down = false
|
||||
uploadFileList.value = []
|
||||
uploadDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭上传对话框
|
||||
const handleCloseUpload = () => {
|
||||
uploadDialogVisible.value = false
|
||||
uploadFileList.value = []
|
||||
}
|
||||
|
||||
// 文件列表变化
|
||||
const handleFileChange = (file, fileList) => {
|
||||
console.log('文件列表变化:', file, fileList)
|
||||
uploadFileList.value = fileList
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const handleRemoveFile = (file, fileList) => {
|
||||
console.log('移除文件:', file, fileList)
|
||||
uploadFileList.value = fileList
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
const handleSubmitUpload = () => {
|
||||
if (uploadFileList.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一个文件')
|
||||
return
|
||||
}
|
||||
// 触发所有待上传文件的上传
|
||||
const filesToUpload = uploadFileList.value.filter(file =>
|
||||
file.status !== 'success' && file.status !== 'uploading'
|
||||
)
|
||||
if (filesToUpload.length === 0) {
|
||||
ElMessage.info('所有文件已上传完成')
|
||||
return
|
||||
}
|
||||
// 逐个提交文件
|
||||
uploadRef.value?.submit()
|
||||
|
||||
}
|
||||
|
||||
// 上传前检查(只做提示,不阻止文件添加到列表)
|
||||
const beforeUpload = (file) => {
|
||||
const isValidType = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type)
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
console.log('beforeUpload', file)
|
||||
|
||||
if (!isValidType) {
|
||||
ElMessage.warning(`文件 ${file.name} 格式不符合要求(仅支持 JPG/PNG/GIF/PDF/DOC/DOCX)`)
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.warning(`文件 ${file.name} 大小超过 10MB`)
|
||||
}
|
||||
// 允许文件添加到列表,在上传时再进行验证
|
||||
return true
|
||||
}
|
||||
|
||||
// 自定义上传方法
|
||||
const handleCustomUpload = async (options) => {
|
||||
const { file, onSuccess, onError } = options
|
||||
console.log('开始上传文件:', file)
|
||||
|
||||
// 在上传前进行验证
|
||||
const isValidType = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type)
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isValidType) {
|
||||
const error = new Error(`文件 ${file.name} 格式不符合要求(仅支持 JPG/PNG/GIF/PDF/DOC/DOCX)`)
|
||||
// 标记为校验类错误,on-error 中不再弹 error 提示
|
||||
error.isValidation = true
|
||||
onError(error, file)
|
||||
return
|
||||
}
|
||||
if (!isLt10M) {
|
||||
const error = new Error(`文件 ${file.name} 大小超过 10MB`)
|
||||
error.isValidation = true
|
||||
onError(error, file)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
|
||||
// 根据 API 文档,字段名应该是 files(复数)
|
||||
formData.append('files', file)
|
||||
|
||||
// 添加文件名列表(虽然 API 文档说是数组,但实际传递时直接传字符串)
|
||||
formData.append('file_names', file.name)
|
||||
|
||||
// 添加上传类型
|
||||
if (uploadForm.update_type) {
|
||||
formData.append('update_type', uploadForm.update_type)
|
||||
}
|
||||
|
||||
// 添加是否开放下载
|
||||
formData.append('open_down', uploadForm.open_down ? '1' : '0')
|
||||
|
||||
console.log('上传参数:', {
|
||||
files: file.name,
|
||||
file_names: [file.name],
|
||||
update_type: uploadForm.update_type,
|
||||
open_down: uploadForm.open_down
|
||||
})
|
||||
|
||||
const res = await uploadFile(formData)
|
||||
console.log('上传响应:', res)
|
||||
|
||||
// 根据返回码严格区分成功和失败
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
onSuccess(res.data.data, file)
|
||||
} else {
|
||||
const errorMsg = res?.data?.message || res?.data?.msg || '上传失败'
|
||||
const error = new Error(errorMsg)
|
||||
onError(error, file)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error)
|
||||
const err = new Error(error?.response?.data?.message || error?.message || '上传失败')
|
||||
onError(err, file)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const handleUploadSuccess = (response, file, fileList) => {
|
||||
console.log('上传成功文件:', file)
|
||||
console.log('上传成功文件列表:',fileList)
|
||||
|
||||
|
||||
// 成功回调只会在 code === 200 时触发
|
||||
// ElMessage.success(`文件 ${file.name} 上传成功`)
|
||||
// 更新文件列表状态
|
||||
uploadFileList.value = fileList
|
||||
// 如果所有文件都上传成功,关闭对话框并刷新列表
|
||||
const allSuccess = fileList.every(f => f.status === 'success')
|
||||
const uploadList = fileList.some(f => f.status === 'uploading')
|
||||
if (allSuccess && !uploadList && fileList.length > 0) {
|
||||
ElMessage.success(`已成功上传${fileList.length}个文件`)
|
||||
setTimeout(() => {
|
||||
uploadDialogVisible.value = false
|
||||
uploadFileList.value = []
|
||||
fetchFileList()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = (error, file, fileList) => {
|
||||
console.error('上传失败:', error, file, fileList)
|
||||
// 对校验类错误仅在 beforeUpload 中提示过一次 warning,这里不再重复报错
|
||||
if (error?.isValidation) return
|
||||
ElMessage.error(error?.message || '上传失败,请检查网络连接或联系管理员')
|
||||
}
|
||||
|
||||
// 提交编辑表单
|
||||
const submitEditForm = () => {
|
||||
fileFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
file_id: fileForm.file_id,
|
||||
user_id: Number(fileForm.user_id),
|
||||
open_dow: fileForm.open_dow
|
||||
}
|
||||
console.log('提交文件信息数据:', submitData)
|
||||
const res = await updateFile(submitData)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('修改成功')
|
||||
editDialogVisible.value = false
|
||||
fetchFileList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '修改失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchFileList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-file-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-detail-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.file-preview-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.file-icon-large {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-type-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-info-descriptions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
: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>
|
||||
+173
-190
@@ -1,103 +1,112 @@
|
||||
<template>
|
||||
<div class="users-container">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="filter-container" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</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-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><plus /></el-icon>新增用户
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
<el-button type="success">
|
||||
<el-icon><upload /></el-icon>导入
|
||||
</el-button>
|
||||
<el-button>
|
||||
<el-icon><download /></el-icon>导出
|
||||
</el-button>
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</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"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</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">
|
||||
<el-icon><Upload /></el-icon>导入
|
||||
</el-button>
|
||||
<el-button type="primary" plain>
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-card class="table-container" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="用户信息" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="40" :src="row.avatar"></el-avatar>
|
||||
<div class="user-detail">
|
||||
<div class="username">{{ row.username }}</div>
|
||||
<div class="email">{{ row.email }}</div>
|
||||
<!-- 用户列表 -->
|
||||
<div class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="用户信息" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="40" :src="row.avatar"></el-avatar>
|
||||
<div class="user-detail">
|
||||
<div class="username">{{ row.username }}</div>
|
||||
<div class="email">{{ row.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="role" label="角色" />
|
||||
<el-table-column prop="phone" label="手机号码" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="(val) => handleStatusChange(row, val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="primary" link @click="handleRoleAssign(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.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="role" label="角色" />
|
||||
<el-table-column prop="phone" label="手机号码" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="(val) => handleStatusChange(row, val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="primary" link @click="handleRoleAssign(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.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户表单对话框 -->
|
||||
@@ -168,7 +177,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
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({
|
||||
@@ -439,57 +448,56 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-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 {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
: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;
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@@ -514,65 +522,40 @@ onMounted(() => {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@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;
|
||||
}
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
</style>
|
||||
|
||||
: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>
|
||||
+318
-132
@@ -33,7 +33,7 @@
|
||||
@click="selectTicket(ticket)"
|
||||
>
|
||||
<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 class="ticket-content">
|
||||
<div class="ticket-top">
|
||||
@@ -96,8 +96,8 @@
|
||||
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
|
||||
>
|
||||
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
|
||||
<el-avatar :size="36" :src="getUserAvatar(message.userId)">
|
||||
{{ currentTicket.username.charAt(0) }}
|
||||
<el-avatar :size="36" :src="message.avatar">
|
||||
{{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
@@ -117,7 +117,7 @@
|
||||
<div class="message-time">{{ formatMessageTime(message.time) }}</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Loading } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getTickerList,
|
||||
getTickerList,
|
||||
getTicketDetail,
|
||||
replyTicket,
|
||||
closeTicket,
|
||||
getUserAvatar,
|
||||
getFileImage,
|
||||
parseFilesToImages
|
||||
parseFilesToImages,
|
||||
getTicketCount
|
||||
} from '@/api/ticket'
|
||||
import notificationSound from '@/assets/7.wav'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
// 路由相关
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 管理员ID列表(客服ID)
|
||||
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID
|
||||
// 用户 store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 头像
|
||||
const adminAvatar = ref('')
|
||||
@@ -271,6 +274,11 @@ const stats = reactive({
|
||||
isLoadingStats: false
|
||||
})
|
||||
|
||||
// 上一次的待处理数量,用于判断是否有新工单
|
||||
const previousPendingCount = ref(0)
|
||||
// 音频对象
|
||||
const audio = new Audio(notificationSound)
|
||||
|
||||
// 快捷回复选项
|
||||
const quickReplies = ref([
|
||||
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
|
||||
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
|
||||
const mappedTickets = tickets.map(item => ({
|
||||
id: item.work_id,
|
||||
title: item.name,
|
||||
username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息
|
||||
userId: item.user_id,
|
||||
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),
|
||||
@@ -336,9 +345,24 @@ const fetchTicketList = async (append = false) => {
|
||||
}))
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -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 () => {
|
||||
stats.isLoadingStats = true
|
||||
try {
|
||||
// 并行获取各个状态的工单数量
|
||||
await Promise.all([
|
||||
fetchStatusStat(''), // 获取全部工单数量
|
||||
fetchStatusStat('pending'), // 待处理
|
||||
fetchStatusStat('processing'), // 处理中
|
||||
fetchStatusStat('replied'), // 已回复
|
||||
fetchStatusStat('completed') // 已完成
|
||||
])
|
||||
const res = await getTicketCount()
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
|
||||
// 检查是否有新工单(待处理数量增加)
|
||||
if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
|
||||
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) {
|
||||
console.error('获取工单统计数据出错:', error)
|
||||
} finally {
|
||||
@@ -398,6 +413,12 @@ const fetchAllStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新统计数据(用于定时刷新)
|
||||
const fetchCurrentStatusStat = async () => {
|
||||
await fetchAllStats()
|
||||
}
|
||||
|
||||
|
||||
// 加载更多工单
|
||||
const loadMoreTickets = () => {
|
||||
if (!hasMore.value || isLoading.value) return
|
||||
@@ -436,22 +457,22 @@ const filteredTickets = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
// 按最后回复时间排序(优先显示有新消息的)
|
||||
// 按work_id从大到小排序(优先显示待处理)
|
||||
return result.sort((a, b) => {
|
||||
// 优先显示待处理
|
||||
if (a.status === 'pending' && b.status !== 'pending') return -1
|
||||
if (a.status !== 'pending' && b.status === 'pending') return 1
|
||||
|
||||
// 然后按最后回复时间
|
||||
const timeA = new Date(a.lastReplyTime || a.createTime)
|
||||
const timeB = new Date(b.lastReplyTime || b.createTime)
|
||||
return timeB - timeA
|
||||
// 然后按work_id从大到小排序
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
// 判断是否是客服
|
||||
// 判断是否是当前登录的管理员
|
||||
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) {
|
||||
// 使用Promise.all一次性处理所有消息和图片
|
||||
const messagesPromises = detail.Content.map(async (msg) => {
|
||||
const isAdminMsg = isAdmin(msg.UserId)
|
||||
const images = await parseFilesToImages(msg.Flies)
|
||||
if (detail.content && detail.content.length > 0) {
|
||||
// 处理所有消息
|
||||
const messages = detail.content.map((msg) => {
|
||||
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||
// 从 flies 数组中提取图片 URL
|
||||
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||
|
||||
return {
|
||||
id: msg.Id,
|
||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
||||
id: msg.id,
|
||||
content: msg.content !== 'empty' ? msg.content : null,
|
||||
images: images,
|
||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
||||
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.UserId
|
||||
userId: msg.user?.userId,
|
||||
avatar: msg.user?.coverUrl || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 等待所有消息处理完成
|
||||
const messages = await Promise.all(messagesPromises)
|
||||
currentMessages.value = messages
|
||||
}
|
||||
} else {
|
||||
@@ -562,23 +583,29 @@ const sendMessage = async () => {
|
||||
const fileIds = []
|
||||
|
||||
try {
|
||||
// 添加一个临时的"正在发送"消息
|
||||
// 保存输入内容
|
||||
const inputMsg = messageInput.value.trim()
|
||||
const inputImages = [...selectedImages.value]
|
||||
|
||||
// 清空输入和已选图片
|
||||
messageInput.value = ''
|
||||
selectedImages.value = []
|
||||
|
||||
// 立即添加消息到界面(不显示 loading)
|
||||
const tempMsg = {
|
||||
content: messageInput.value.trim() || null,
|
||||
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null,
|
||||
id: Date.now(), // 临时 ID
|
||||
content: inputMsg || null,
|
||||
images: inputImages.length > 0 ? inputImages : [],
|
||||
time: new Date().toLocaleString(),
|
||||
isAdmin: true,
|
||||
isLoading: true,
|
||||
isSystem: false,
|
||||
userId: userStore.userInfo?.user_id,
|
||||
avatar: userStore.userInfo?.cover_url || '',
|
||||
isTempMessage: true
|
||||
}
|
||||
|
||||
currentMessages.value.push(tempMsg)
|
||||
|
||||
// 清空输入和已选图片
|
||||
const inputMsg = messageInput.value
|
||||
messageInput.value = ''
|
||||
selectedImages.value = []
|
||||
|
||||
// 滚动到底部
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
@@ -606,6 +633,7 @@ const sendMessage = async () => {
|
||||
|
||||
// 恢复输入内容
|
||||
messageInput.value = inputMsg
|
||||
selectedImages.value = inputImages
|
||||
|
||||
ElMessage.error(res.message || '发送失败')
|
||||
}
|
||||
@@ -685,7 +713,8 @@ const filterByStatus = (status) => {
|
||||
activeStatus.value = status
|
||||
currentPage.value = 1 // 切换状态后重置页码
|
||||
hasMore.value = true // 重置加载更多标志
|
||||
fetchTicketList() // 重新获取数据
|
||||
ticketList.value = [] // 清空列表,不缓存
|
||||
fetchTicketList(false) // 从头重新获取数据,不追加
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
@@ -701,42 +730,149 @@ const updateTicketStats = () => {
|
||||
|
||||
// 格式化消息时间
|
||||
const formatMessageTime = (timeStr) => {
|
||||
const date = new Date(timeStr)
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
if (!timeStr) return ''
|
||||
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
// 格式化为 HH:MM
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
} catch (e) {
|
||||
console.error('时间格式化失败:', e)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化列表项时间
|
||||
const formatTime = (timeStr) => {
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 今天内的消息只显示时间
|
||||
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
if (!timeStr) return ''; // 空值兜底
|
||||
|
||||
// 步骤1:解析中文时间字符串(核心适配点)
|
||||
let date;
|
||||
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();
|
||||
const dateTime = date.getTime();
|
||||
const nowTime = now.getTime();
|
||||
const diff = nowTime - dateTime;
|
||||
|
||||
// 步骤2:判断“今天”(年/月/日完全一致)
|
||||
const isToday = date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate();
|
||||
|
||||
// 一周内的显示星期几
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return weekdays[date.getDay()]
|
||||
if (isToday) {
|
||||
// 格式化今天的时间(24小时制,补零)
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${hour}:${minute}`;
|
||||
}
|
||||
|
||||
// 其他显示日期
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
// 步骤3:判断“一周内”
|
||||
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 date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return '今天'
|
||||
console.log("原始时间字符串:", timeStr);
|
||||
if (!timeStr) return ''; // 空值兜底
|
||||
|
||||
let date;
|
||||
// 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) => {
|
||||
@@ -775,8 +911,8 @@ const startAutoRefresh = () => {
|
||||
// 静默刷新工单列表,保持当前页码
|
||||
refreshTicketList()
|
||||
|
||||
// 刷新工单统计
|
||||
fetchAllStats()
|
||||
// 只刷新当前分类的统计数据,减少请求数量
|
||||
fetchCurrentStatusStat()
|
||||
}, refreshInterval)
|
||||
}
|
||||
|
||||
@@ -800,7 +936,8 @@ const refreshTicketList = async () => {
|
||||
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) {
|
||||
const tickets = res.data.data || []
|
||||
@@ -809,16 +946,36 @@ const refreshTicketList = async () => {
|
||||
const mappedTickets = tickets.map(item => ({
|
||||
id: item.work_id,
|
||||
title: item.name,
|
||||
username: `用户${item.user_id}`,
|
||||
userId: item.user_id,
|
||||
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),
|
||||
content: item.name
|
||||
}))
|
||||
|
||||
// 更新列表
|
||||
ticketList.value = mappedTickets
|
||||
// 合并到现有列表,去重并按work_id从大到小排序
|
||||
// 使用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
|
||||
}
|
||||
} 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状态)
|
||||
const refreshTicketMessages = async (workId) => {
|
||||
try {
|
||||
@@ -838,37 +1028,33 @@ const refreshTicketMessages = async (workId) => {
|
||||
if (currentTicket.value) {
|
||||
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
|
||||
if (currentTicket.value.status !== 'pending') {
|
||||
currentTicket.value.status = convertStatusToString(detail.Status)
|
||||
currentTicket.value.status = convertStatusToString(detail.status)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理消息列表
|
||||
if (detail.Content && detail.Content.length > 0) {
|
||||
// 检查是否有新消息
|
||||
const lastMsgId = currentMessages.value.length > 0 ?
|
||||
currentMessages.value[currentMessages.value.length - 1].id : 0;
|
||||
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId);
|
||||
|
||||
if (hasNewMessage) {
|
||||
// 有新消息时才更新
|
||||
const messagesPromises = detail.Content.map(async (msg) => {
|
||||
const isAdminMsg = isAdmin(msg.UserId)
|
||||
const images = await parseFilesToImages(msg.Flies)
|
||||
|
||||
return {
|
||||
id: msg.Id,
|
||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
||||
images: images,
|
||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.UserId
|
||||
}
|
||||
})
|
||||
if (detail.content && detail.content.length > 0) {
|
||||
// 构建新消息列表
|
||||
const newMessages = detail.content.map((msg) => {
|
||||
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||
// 从 flies 数组中提取图片 URL
|
||||
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||
|
||||
// 等待所有消息处理完成
|
||||
const messages = await Promise.all(messagesPromises)
|
||||
currentMessages.value = messages
|
||||
return {
|
||||
id: msg.id,
|
||||
content: msg.content !== 'empty' ? msg.content : null,
|
||||
images: images,
|
||||
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.user?.userId,
|
||||
avatar: msg.user?.coverUrl || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化)
|
||||
if (!areMessagesEqual(currentMessages.value, newMessages)) {
|
||||
currentMessages.value = newMessages
|
||||
|
||||
// 如果有新消息,滚动到底部
|
||||
nextTick(() => {
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
<template>
|
||||
<div class="admin-group-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<div class="search-form">
|
||||
<el-input
|
||||
v-model="queryParams.key"
|
||||
placeholder="搜索关键词"
|
||||
clearable
|
||||
@clear="fetchGroupList"
|
||||
@keyup.enter="fetchGroupList"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增管理员组
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchGroupList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员组列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading" class="skeleton-container">
|
||||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
||||
<div class="skeleton-cell skeleton-id"></div>
|
||||
<div class="skeleton-cell skeleton-name"></div>
|
||||
<div class="skeleton-cell skeleton-note"></div>
|
||||
<div class="skeleton-cell skeleton-time"></div>
|
||||
<div class="skeleton-cell skeleton-action"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="id" label="组ID" width="100" />
|
||||
<el-table-column prop="name" label="组名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<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 label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 管理员组表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增管理员组' : '编辑管理员组'"
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="groupFormRef"
|
||||
:model="groupForm"
|
||||
:rules="groupRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="组名称" prop="name">
|
||||
<el-input v-model="groupForm.name" placeholder="请输入组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="groupForm.note" type="textarea" :rows="4" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 成员列表对话框 -->
|
||||
<el-dialog
|
||||
v-model="memberDialogVisible"
|
||||
title="管理员组成员"
|
||||
width="900px"
|
||||
append-to-body
|
||||
>
|
||||
<div class="member-search">
|
||||
<el-input
|
||||
v-model="memberParams.key"
|
||||
placeholder="搜索成员"
|
||||
clearable
|
||||
@clear="fetchMemberList"
|
||||
@keyup.enter="fetchMemberList"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="memberLoading"
|
||||
:data="memberList"
|
||||
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="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="Phone" label="手机号" width="130" />
|
||||
<el-table-column label="性别" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.Sex ? '男' : '女' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Age" label="年龄" width="80" />
|
||||
<el-table-column prop="UserGroupId" label="用户组" width="120" />
|
||||
<el-table-column label="加入时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="memberParams.page"
|
||||
v-model:page-size="memberParams.count"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="memberTotal"
|
||||
@size-change="handleMemberSizeChange"
|
||||
@current-change="handleMemberCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, User, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getAdminGroupList,
|
||||
getAdminGroupMemberList,
|
||||
addAdminGroup,
|
||||
updateAdminGroupInfo,
|
||||
deleteAdminGroup
|
||||
} from '@/api/admin/group'
|
||||
import { formatTime } from '@/utils/tool'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 成员查询参数
|
||||
const memberParams = reactive({
|
||||
key: '',
|
||||
group_id: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 管理员组表单
|
||||
const groupForm = reactive({
|
||||
group_id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入组名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const memberLoading = ref(false)
|
||||
const groupList = ref([])
|
||||
const memberList = ref([])
|
||||
const total = ref(0)
|
||||
const memberTotal = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const memberDialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const groupFormRef = ref(null)
|
||||
|
||||
// 获取管理员组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAdminGroupList(queryParams)
|
||||
if (res.data.code === 200) {
|
||||
let responseData = res.data.data.data || []
|
||||
responseData.forEach(item => {
|
||||
item.CreatedAt = formatTime(item.CreatedAt)
|
||||
})
|
||||
groupList.value = responseData
|
||||
total.value = res.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取管理员组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取成员列表
|
||||
const fetchMemberList = async () => {
|
||||
memberLoading.value = true
|
||||
try {
|
||||
const res = await getAdminGroupMemberList(memberParams)
|
||||
if (res.data.code === 200) {
|
||||
let responseData = res.data.data.data || []
|
||||
// 数据已经包含所有需要的字段,不需要额外处理
|
||||
memberList.value = responseData
|
||||
memberTotal.value = res.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取成员列表失败')
|
||||
} finally {
|
||||
memberLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleMemberSizeChange = (size) => {
|
||||
memberParams.count = size
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
const handleMemberCurrentChange = (page) => {
|
||||
memberParams.page = page
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
// 新增管理员组
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(groupForm, {
|
||||
group_id: undefined,
|
||||
name: '',
|
||||
note: ''
|
||||
})
|
||||
groupFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑管理员组
|
||||
const handleEdit = (row) => {
|
||||
console.log("编辑管理员组",row)
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
Object.assign(groupForm, {
|
||||
group_id: row.id,
|
||||
name: row.name,
|
||||
note: row.note
|
||||
})
|
||||
}
|
||||
|
||||
// 查看成员
|
||||
const handleViewMembers = (row) => {
|
||||
memberParams.group_id = row.id
|
||||
memberParams.key = ''
|
||||
memberParams.page = 1
|
||||
memberDialogVisible.value = true
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
// 删除管理员组
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除管理员组 ${row.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteAdminGroup({ group_id: row.id })
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = () => {
|
||||
groupFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await addAdminGroup(groupForm)
|
||||
} else {
|
||||
res = await updateAdminGroupInfo(groupForm)
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchGroupList()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-group-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
<template>
|
||||
<div class="user-group-container">
|
||||
<!-- 主容器 -->
|
||||
<el-card class="main-container" shadow="never">
|
||||
<!-- 操作栏 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-content">
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增用户组
|
||||
</el-button>
|
||||
<el-button type="success" @click="fetchGroupList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户组列表 -->
|
||||
<div class="table-section">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading" class="skeleton-container">
|
||||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
||||
<div class="skeleton-cell skeleton-id"></div>
|
||||
<div class="skeleton-cell skeleton-name"></div>
|
||||
<div class="skeleton-cell skeleton-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
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column label="组ID" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.group_id || row.GroupId || row.id || row.Id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="组名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="权限" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" effect="plain">{{ row.auth || row.Auth || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="升级金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.floor_price || row.FloorPrice" class="price-text">¥{{ row.floor_price || row.FloorPrice }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下一级组ID" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.higher_level_id || row.HigherLevelId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
|
||||
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成员数量" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small" effect="plain">
|
||||
{{ row.member_count || row.MemberCount || 0 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户组表单对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增用户组' : '编辑用户组'"
|
||||
width="650px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="groupFormRef"
|
||||
:model="groupForm"
|
||||
:rules="groupRules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item v-if="dialogType === 'edit'" label="组ID">
|
||||
<el-input v-model="groupForm.group_id" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="组名称" prop="name">
|
||||
<el-input v-model="groupForm.name" placeholder="请输入组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限" prop="auth">
|
||||
<el-input v-model="groupForm.auth" type="textarea" :rows="4" placeholder="请输入权限配置(JSON格式)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="下一级用户组ID">
|
||||
<el-input-number
|
||||
v-model="groupForm.higher_level_id"
|
||||
:min="0"
|
||||
placeholder="请输入下一级用户组ID"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="升级所需消费金额">
|
||||
<el-input-number
|
||||
v-model="groupForm.floor_price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入升级所需消费金额"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #prepend>¥</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否为固定用户组">
|
||||
<el-switch
|
||||
v-model="groupForm.fixed"
|
||||
active-text="固定(不可升级)"
|
||||
inactive-text="可升级"
|
||||
/>
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 4px;">
|
||||
固定用户组不能通过消费金额自动升级
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 成员列表对话框 -->
|
||||
<el-dialog
|
||||
v-model="memberDialogVisible"
|
||||
title="用户组成员"
|
||||
width="800px"
|
||||
append-to-body
|
||||
>
|
||||
<el-table
|
||||
v-loading="memberLoading"
|
||||
:data="memberList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column label="用户ID" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.user_id || row.UserId || row.id || row.Id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<span class="username">{{ row.username || row.Username || row.UserName || row.name || row.Name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="邮箱" min-width="200">
|
||||
<template #default="{ row }">
|
||||
{{ row.email || row.Email || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="加入时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.join_time || row.JoinTime || row.CreatedAt || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="memberParams.page"
|
||||
v-model:page-size="memberParams.count"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="memberTotal"
|
||||
@size-change="handleMemberSizeChange"
|
||||
@current-change="handleMemberCurrentChange"
|
||||
background
|
||||
class="pagination"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加成员对话框 -->
|
||||
<el-dialog
|
||||
v-model="addMemberDialogVisible"
|
||||
title="添加用户组成员"
|
||||
width="500px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="memberFormRef"
|
||||
:model="memberForm"
|
||||
:rules="memberRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="用户ID" prop="user_ids">
|
||||
<el-input v-model="memberForm.user_ids" placeholder="请输入用户ID,多个用逗号分隔" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="addMemberDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAddMember">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Edit, User, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserGroupList,
|
||||
getUserGroupMemberList,
|
||||
createUserGroup,
|
||||
updateUserGroupInfo,
|
||||
deleteUserGroup,
|
||||
addUserGroupMember
|
||||
} from '@/api/admin/user'
|
||||
import { formatTime } from '@/utils/tool'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 成员查询参数
|
||||
const memberParams = reactive({
|
||||
group_id: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 用户组表单
|
||||
const groupForm = reactive({
|
||||
group_id: undefined,
|
||||
name: '',
|
||||
auth: '',
|
||||
higher_level_id: undefined,
|
||||
floor_price: undefined,
|
||||
fixed: false
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入组名称', trigger: 'blur' }
|
||||
],
|
||||
auth: [
|
||||
{ required: true, message: '请输入权限', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 成员表单
|
||||
const memberForm = reactive({
|
||||
group_id: '',
|
||||
user_ids: ''
|
||||
})
|
||||
|
||||
const memberRules = {
|
||||
user_ids: [
|
||||
{ required: true, message: '请输入用户ID', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
const loading = ref(false)
|
||||
const memberLoading = ref(false)
|
||||
const groupList = ref([])
|
||||
const memberList = ref([])
|
||||
const total = ref(0)
|
||||
const memberTotal = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const memberDialogVisible = ref(false)
|
||||
const addMemberDialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const groupFormRef = ref(null)
|
||||
const memberFormRef = ref(null)
|
||||
|
||||
// 获取用户组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserGroupList(queryParams)
|
||||
const code = res.data?.code || res.code
|
||||
if (code === 200) {
|
||||
let responseData = res.data?.data || res.data
|
||||
responseData.data.forEach(item => {
|
||||
item.CreatedAt = formatTime(item.CreatedAt)
|
||||
})
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
groupList.value = responseData
|
||||
total.value = responseData.length
|
||||
} else if (responseData.list) {
|
||||
groupList.value = responseData.list || []
|
||||
total.value = responseData.total || responseData.all_count || 0
|
||||
} else if (responseData.data && Array.isArray(responseData.data)) {
|
||||
groupList.value = responseData.data
|
||||
total.value = responseData.all_count || responseData.data.length
|
||||
} else {
|
||||
groupList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '获取用户组列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取成员列表
|
||||
const fetchMemberList = async () => {
|
||||
memberLoading.value = true
|
||||
try {
|
||||
const res = await getUserGroupMemberList(memberParams)
|
||||
const code = res.data?.code || res.code
|
||||
if (code === 200) {
|
||||
const responseData = res.data?.data || res.data
|
||||
if (Array.isArray(responseData)) {
|
||||
memberList.value = responseData
|
||||
memberTotal.value = responseData.length
|
||||
} else if (responseData.list) {
|
||||
memberList.value = responseData.list || []
|
||||
memberTotal.value = responseData.total || responseData.all_count || 0
|
||||
} else if (responseData.data && Array.isArray(responseData.data)) {
|
||||
memberList.value = responseData.data
|
||||
memberTotal.value = responseData.all_count || responseData.data.length
|
||||
} else {
|
||||
memberList.value = []
|
||||
memberTotal.value = 0
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '获取成员列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取成员列表失败')
|
||||
} finally {
|
||||
memberLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.count = size
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handleMemberSizeChange = (size) => {
|
||||
memberParams.count = size
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
const handleMemberCurrentChange = (page) => {
|
||||
memberParams.page = page
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
// 新增用户组
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
Object.assign(groupForm, {
|
||||
group_id: undefined,
|
||||
name: '',
|
||||
auth: '',
|
||||
higher_level_id: undefined,
|
||||
floor_price: undefined,
|
||||
fixed: false
|
||||
})
|
||||
groupFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 编辑用户组
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
|
||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||
const groupName = row.group_name || row.name || row.Name
|
||||
const groupAuth = row.auth || row.Auth || ''
|
||||
const higherLevelId = row.higher_level_id || row.HigherLevelId
|
||||
const floorPrice = row.floor_price || row.FloorPrice
|
||||
const fixed = row.fixed || row.Fixed || false
|
||||
|
||||
Object.assign(groupForm, {
|
||||
group_id: groupId,
|
||||
name: groupName,
|
||||
auth: typeof groupAuth === 'object' ? JSON.stringify(groupAuth, null, 2) : groupAuth,
|
||||
higher_level_id: higherLevelId,
|
||||
floor_price: floorPrice,
|
||||
fixed: fixed
|
||||
})
|
||||
}
|
||||
|
||||
// 查看成员
|
||||
const handleViewMembers = (row) => {
|
||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||
memberParams.group_id = groupId
|
||||
memberParams.page = 1
|
||||
memberDialogVisible.value = true
|
||||
fetchMemberList()
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
const handleAddMember = (row) => {
|
||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||
memberForm.group_id = groupId
|
||||
memberForm.user_ids = ''
|
||||
addMemberDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除用户组
|
||||
const handleDelete = (row) => {
|
||||
const groupId = row.group_id || row.GroupId || row.id || row.Id
|
||||
const groupName = row.group_name || row.name || row.Name
|
||||
|
||||
ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteUserGroup({ group_id: groupId })
|
||||
const code = res.data?.code || res.code
|
||||
if (code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchGroupList()
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交用户组表单
|
||||
const submitForm = () => {
|
||||
groupFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
let res
|
||||
const submitData = {
|
||||
name: groupForm.name,
|
||||
auth: groupForm.auth
|
||||
}
|
||||
|
||||
if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) {
|
||||
submitData.higher_level_id = groupForm.higher_level_id
|
||||
}
|
||||
if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) {
|
||||
submitData.floor_price = groupForm.floor_price
|
||||
}
|
||||
submitData.fixed = groupForm.fixed
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createUserGroup(submitData)
|
||||
} else {
|
||||
submitData.group_id = groupForm.group_id
|
||||
res = await updateUserGroupInfo(submitData)
|
||||
}
|
||||
|
||||
const code = res.data?.code || res.code
|
||||
if (code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
fetchGroupList()
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交添加成员
|
||||
const submitAddMember = () => {
|
||||
memberFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await addUserGroupMember(memberForm)
|
||||
const code = res.data?.code || res.code
|
||||
if (code === 200) {
|
||||
ElMessage.success('添加成功')
|
||||
addMemberDialogVisible.value = false
|
||||
fetchGroupList()
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '添加失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-group-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
border: 1px solid #e1e8ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
justify-content: 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 {
|
||||
margin-top: 20px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
background: #fafbfc;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover > td) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skeleton-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.skeleton-cell {
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-id { width: 100px; }
|
||||
.skeleton-name { width: 200px; }
|
||||
.skeleton-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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,687 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "默认模块",
|
||||
"description": "",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"tags": [],
|
||||
"paths": {
|
||||
"/api/v1/admin/server/setting/group/list": {
|
||||
"get": {
|
||||
"summary": "获取配置分组列表",
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"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",
|
||||
"in": "query",
|
||||
"description": "获取条数 默认 10",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "group_id",
|
||||
"in": "query",
|
||||
"description": "组id(与组名称二选一)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "group_name",
|
||||
"in": "query",
|
||||
"description": "组名称(与组id二选一)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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/info": {
|
||||
"get": {
|
||||
"summary": "获取配置信息",
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"description": "配置id (与name二选一)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "配置名称 (与id二选一)",
|
||||
"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/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": {
|
||||
"schemas": {},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"servers": [],
|
||||
"security": []
|
||||
}
|
||||
Reference in New Issue
Block a user