Compare commits
108 Commits
fcfde5191e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7394afb83f | |||
| c43d1978a8 | |||
| 475c62aefc | |||
| c0daa6ed11 | |||
| 2e073c2b87 | |||
| 13248468d3 | |||
| ab7a8d5cfa | |||
| 64d40cbbbf | |||
| d72a4f804e | |||
| 8b2251ef97 | |||
| 2916c04ba5 | |||
| c7245cec67 | |||
| 985412c3bc | |||
| f53f63e679 | |||
| cae1f847e4 | |||
| 5428f01cdf | |||
| 7652b290b0 | |||
| cf188bb94a | |||
| b3ed406f84 | |||
| 2f06aa9f5f | |||
| f0e89695f4 | |||
| c07e09c151 | |||
| 71d3605f4f | |||
| b7e806cc80 | |||
| 1a4587f893 | |||
| 40a5e486a6 | |||
| 3357566b02 | |||
| 25d782b050 | |||
| 9edb59d16e | |||
| cf19956b88 | |||
| cd16ec17ae | |||
| f4dbf17ce9 | |||
| 25975c8b29 | |||
| d650bfeb61 | |||
| 3e751d4c42 | |||
| 193db5735f | |||
| 09a83f4985 | |||
| 20790cf029 | |||
| 86f3835e51 | |||
| a2a7644a9f | |||
| 3ca956d9f0 | |||
| 5d16589e54 | |||
| 255bd9e832 | |||
| 2e82ff8a34 | |||
| c100c37a32 | |||
| cdd8f86b92 | |||
| fe29a8b3d0 | |||
| 2f38932878 | |||
| fdc9db9a9c | |||
| 4d45cf535e | |||
| 9d8f23262b | |||
| e96e9c4a7e | |||
| 5a31de64b3 | |||
| b4260fedb8 | |||
| 793a96a44f | |||
| 043be60f4f | |||
| 127d54eaa6 | |||
| ead7c5bba5 | |||
| 5b5e0f62ec | |||
| 9105503850 | |||
| 20260e221c | |||
| a270f58500 | |||
| 7992ee9902 | |||
| 084aeebf13 | |||
| 1e79005440 | |||
| a6d4d70221 | |||
| e3e70114fb | |||
| 0b57581799 | |||
| 36271b8bd0 | |||
| cae89dd5ad | |||
| d3479fb0bb | |||
| 98cb0e1c8e | |||
| 779359cec5 | |||
| 60f141a0a9 | |||
| fe1a118132 | |||
| 2ce2c1a31f | |||
| 1655d86f6b | |||
| fcebebd216 | |||
| 5a93f4f8a8 | |||
| 4d10deef86 | |||
| cf7ac515f6 | |||
| 4ef208a662 | |||
| f6dcec75d7 | |||
| 4cc684eca6 | |||
| 00ea1845a7 | |||
| 0c6166b3c7 | |||
| 978b18d5d5 | |||
| 54f78e15fe | |||
| ab2df50c0d | |||
| 6859753470 | |||
| 32bb4502e7 | |||
| 4a13048718 | |||
| b56359e572 | |||
| 41d6492daf | |||
| 14fcac3a24 | |||
| 0fc582bc8c | |||
| 0fe4ece1a9 | |||
| a09631551b | |||
| 777022632c | |||
| 5ea4f2cfe3 | |||
| f7c3be1d30 | |||
| 2a91cbb193 | |||
| 067e0539ba | |||
| 11cb40c86a | |||
| fca272f8fa | |||
| d0c706f645 | |||
| e8fa0d7c2c | |||
| a97f6a7202 |
@@ -17,7 +17,12 @@ store封装到src/store目录下。
|
||||
|
||||
注册侧边栏在/config/menus.js文件中。
|
||||
|
||||
新添加要求:
|
||||
在遇到用户id需要填写和修改的弹窗将其修改为可预览样式
|
||||
关于填写表单为推荐人id的需要使用组件AvatarSelector展示,如果是文件id或者是封面id 的也需要预览展示需要向头像列表组件一样,可以弄个文件组件/api/v1/admin/file/list这个是文件列表接口
|
||||
|
||||
规则:
|
||||
1.只要涉及弹窗添加和修改xxxid类型的就需要生成一个弹窗组件并使用到页面中
|
||||
|
||||
## 1. 基础布局规范
|
||||
```css
|
||||
|
||||
@@ -37,10 +37,10 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ninBo
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||
with:
|
||||
name: vue3-build
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ninBo
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||
with:
|
||||
name: vue3-build
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# 管理员后台pc端
|
||||
|
||||
# 007UI 后台管理系统
|
||||
|
||||
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/logo.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="007UI - 高端蓝色扁平化后台管理系统模板" />
|
||||
<meta name="keywords" content="管理系统,后台,模板,Vue3,ElementPlus" />
|
||||
|
||||
Generated
+984
-931
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
+394
-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,421 @@ 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;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.el-card[shadow="hover"]:hover {
|
||||
border-color: #c0c4cc !important;
|
||||
box-shadow: 0 2px 12px rgba(44, 62, 80, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 表格扁平化 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Descriptions 描述列表增强 */
|
||||
.el-descriptions {
|
||||
--el-descriptions-item-bordered-label-background: #fafbfc;
|
||||
}
|
||||
.el-descriptions__label {
|
||||
color: #606266 !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.el-descriptions__content {
|
||||
color: #1d2129 !important;
|
||||
}
|
||||
|
||||
/* Loading 遮罩增强 */
|
||||
.el-loading-mask {
|
||||
background-color: rgba(255, 255, 255, 0.85) !important;
|
||||
}
|
||||
.el-loading-spinner .circular {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.el-loading-spinner .el-loading-text {
|
||||
color: #606266 !important;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Message Box 增强 */
|
||||
.el-message-box {
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15) !important;
|
||||
}
|
||||
.el-message-box__header {
|
||||
padding: 16px 20px 12px !important;
|
||||
}
|
||||
.el-message-box__title {
|
||||
font-weight: 600 !important;
|
||||
color: #1d2129 !important;
|
||||
}
|
||||
.el-message-box__btns .el-button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Alert 增强 */
|
||||
.el-alert {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Tabs 增强 */
|
||||
.el-tabs__item {
|
||||
transition: color 0.2s ease !important;
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Switch 开关增强 */
|
||||
.el-switch {
|
||||
--el-switch-on-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 全局链接按钮悬浮下划线 */
|
||||
.el-button.is-link:hover,
|
||||
.el-button--primary.is-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -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,69 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拼团活动相关接口
|
||||
/**获取拼团队伍列表 */
|
||||
export const getGroupBuyList = () => {
|
||||
return http2.get('/api/v1/users/activity/group_buy/list')
|
||||
}
|
||||
|
||||
/**获取拼团队伍详情 */
|
||||
export const getGroupBuyDetail = (groupBuyId) => {
|
||||
return http2.get('/api/v1/users/activity/group_buy/detail', {
|
||||
params: { group_buy_id: groupBuyId }
|
||||
})
|
||||
}
|
||||
|
||||
/**为队伍添加随机伪人 */
|
||||
export const addRandomUser = (groupBuyId) => {
|
||||
const formData = new FormData()
|
||||
formData.append('group_buy_id', groupBuyId)
|
||||
return http2.post('/api/v1/admin/activity/group_buy/add_random_user', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**创建随机伪人队伍 */
|
||||
export const addRandomGroup = (data) => {
|
||||
const formData = new FormData()
|
||||
formData.append('name', data.name)
|
||||
formData.append('group_buy_type_id', data.group_buy_type_id)
|
||||
return http2.post('/api/v1/admin/activity/group_buy/add_random_group', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**导出成功队伍信息 */
|
||||
export const exportIdcInfo = () => {
|
||||
return http2.get('/api/v1/admin/activity/group_buy/export_idc_info')
|
||||
}
|
||||
|
||||
/**为指定队伍下发订单 */
|
||||
export const setOrder = (groupBuyId) => {
|
||||
const formData = new FormData()
|
||||
formData.append('group_buy_id', groupBuyId)
|
||||
return http2.post('/api/v1/admin/activity/group_buy/set_order', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,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,768 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务管理 API
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取 KVM 主控服务列表 */
|
||||
export const getKvmServiceList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/list', { params })
|
||||
}
|
||||
|
||||
/** 获取 KVM 主控服务详情 */
|
||||
export const getKvmServiceDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建 KVM 主控服务 */
|
||||
export const createKvmService = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改 KVM 主控服务 */
|
||||
export const updateKvmService = (id, data) => {
|
||||
return http2.post(`/api/v1/admin/server/host_service/update?id=${id}`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 KVM 主控服务 */
|
||||
export const deleteKvmService = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 宿主机组映射管理 API
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取本地主机组列表 */
|
||||
export const getHostGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/host_group/list', { params })
|
||||
}
|
||||
|
||||
/** 从远程同步主机组到本地 */
|
||||
export const syncHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定主机组到商品组或商品 */
|
||||
export const bindHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/bind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改本地主机组信息 */
|
||||
export const updateHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据主机组树自动生成 GoodGroup/Goods/Args */
|
||||
export const generateGoodsByHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/host_group/generate_goods', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除本地主机组 */
|
||||
export const deleteHostGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/host_group/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 远程宿主机组管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取远程主机组列表 */
|
||||
export const getRemoteHostGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/list', { params })
|
||||
}
|
||||
|
||||
/** 获取远程主机组详情 */
|
||||
export const getRemoteHostGroupDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取远程主机组树形结构 */
|
||||
export const getRemoteHostGroupTree = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/tree', { params })
|
||||
}
|
||||
|
||||
/** 获取主机组最优主机配置信息 */
|
||||
export const getOptimalHostInfo = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host_group/optimal_host', { params })
|
||||
}
|
||||
|
||||
/** 创建远程主机组 */
|
||||
export const createRemoteHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host_group/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改远程主机组 */
|
||||
export const updateRemoteHostGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除远程主机组 */
|
||||
export const deleteRemoteHostGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/host_group/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 宿主机管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取宿主机列表 */
|
||||
export const getRemoteHostList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/list', { params })
|
||||
}
|
||||
|
||||
/** 获取宿主机详情 */
|
||||
export const getRemoteHostDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取宿主机指标数据 */
|
||||
export const getRemoteHostMetrics = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/metrics', { params })
|
||||
}
|
||||
|
||||
/** 查询历史指标(宿主机或虚拟机) */
|
||||
export const getMetricsHistory = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/host/metrics_history', { params })
|
||||
}
|
||||
|
||||
/** 新增宿主机 */
|
||||
export const addRemoteHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改宿主机 */
|
||||
export const updateRemoteHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除宿主机 */
|
||||
export const deleteRemoteHost = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/host/delete', { params })
|
||||
}
|
||||
|
||||
/** 创建宿主机注册令牌 */
|
||||
export const createHostToken = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/host/create_token', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 镜像管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取镜像列表 */
|
||||
export const getImageList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/list', { params })
|
||||
}
|
||||
|
||||
/** 获取镜像详情 */
|
||||
export const getImageDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取镜像在指定宿主机上的状态 */
|
||||
export const getImageHostStatus = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/host_status', { params })
|
||||
}
|
||||
|
||||
/** 创建镜像 */
|
||||
export const createImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改镜像 */
|
||||
export const updateImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除镜像 */
|
||||
export const deleteImage = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/image/delete', { params })
|
||||
}
|
||||
|
||||
/** 重新下载镜像 */
|
||||
export const reloadImage = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/reload', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 向宿主机同步镜像 */
|
||||
export const syncImageToHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 指定宿主机重新下载指定镜像 */
|
||||
export const reloadImageOnHost = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/image/reload_host', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取宿主机镜像列表与状态(对比) */
|
||||
export const getImageCompareHost = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/image/compare_host', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 网络管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取网络列表 */
|
||||
export const getNetworkList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/network/list', { params })
|
||||
}
|
||||
|
||||
/** 获取网络详情 */
|
||||
export const getNetworkDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/network/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建网络 */
|
||||
export const createNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改网络 */
|
||||
export const updateNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量创建网络 */
|
||||
export const batchCreateNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/batch_create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除网络 */
|
||||
export const deleteNetwork = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/network/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 数据卷管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取数据卷列表 */
|
||||
export const getVolumeList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/volume/list', { params })
|
||||
}
|
||||
|
||||
/** 获取数据卷详情 */
|
||||
export const getVolumeDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/volume/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建数据卷 */
|
||||
export const createVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 调整数据卷大小 */
|
||||
export const resizeVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/resize', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 挂载卷到虚拟机 */
|
||||
export const mountVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/mount', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 卸载卷 */
|
||||
export const unmountVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/unmount', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 迁移卷 */
|
||||
export const transferVolume = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/volume/transfer', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除卷 */
|
||||
export const deleteVolume = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/volume/delete', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 虚拟机管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取虚拟机列表 */
|
||||
export const getVmList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/list', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机详情 */
|
||||
export const getVmDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/detail', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机状态 */
|
||||
export const getVmStatus = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/status', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机指标数据 */
|
||||
export const getVmMetrics = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/metrics', { params })
|
||||
}
|
||||
|
||||
/** 创建虚拟机 */
|
||||
export const createVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改虚拟机 */
|
||||
export const updateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重建虚拟机 */
|
||||
export const rebuildVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/rebuild', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重构虚拟机 */
|
||||
export const refactorVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/refactor', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改虚拟机带宽 */
|
||||
export const updateVmTraffic = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/update_traffic', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 流量策略 ==========
|
||||
// 测试未通过(接口新增,待联调)
|
||||
/** 获取虚拟机流量策略 */
|
||||
export const getVmTrafficPolicy = (params) => http2.get('/api/v1/admin/server/host_service/point/vm/traffic_policy', { params })
|
||||
/** 修改虚拟机流量策略 */
|
||||
export const updateVmTrafficPolicy = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/update', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
/** 增加虚拟机固定流量上限 */
|
||||
export const addVmFixedTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_fixed', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
/** 增加虚拟机一次性临时流量 */
|
||||
export const addVmTemporaryTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_temporary', data, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
/** 启动虚拟机 */
|
||||
export const startVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/start', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 停止虚拟机 */
|
||||
export const stopVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/stop', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 重启虚拟机 */
|
||||
export const rebootVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/reboot', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 暂停虚拟机 */
|
||||
export const suspendVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/suspend', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复虚拟机 */
|
||||
export const resumeVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/resume', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 虚拟机进入救援系统 */
|
||||
export const rescueVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/rescue', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 虚拟机退出救援系统 */
|
||||
export const exitRescueVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/exit_rescue', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除虚拟机 */
|
||||
export const deleteVm = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/vm/delete', { params })
|
||||
}
|
||||
|
||||
/** 迁移虚拟机(更换宿主机) */
|
||||
export const migrateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/migrate', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 发起虚拟机数据迁移 */
|
||||
export const dataMigrateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取虚拟机数据迁移进度 */
|
||||
export const getDataMigrateProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/progress', { params })
|
||||
}
|
||||
|
||||
/** 中断虚拟机数据迁移 */
|
||||
export const abortDataMigrate = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate/abort', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 安全组管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取安全组列表 */
|
||||
export const getSecurityGroupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/post_group/list', { params })
|
||||
}
|
||||
|
||||
/** 获取安全组详情 */
|
||||
export const getSecurityGroupDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/post_group/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建安全组 */
|
||||
export const createSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改安全组 */
|
||||
export const updateSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 同步安全组 */
|
||||
export const syncSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/sync', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定安全组到虚拟机 */
|
||||
export const bindSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/bind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 解绑安全组 */
|
||||
export const unbindSecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/unbind', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除安全组 */
|
||||
export const deleteSecurityGroup = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete', { params })
|
||||
}
|
||||
|
||||
/** 开启安全组白名单 */
|
||||
export const enableSecurityGroupWhitelist = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/enable_whitelist', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 关闭安全组白名单 */
|
||||
export const disableSecurityGroupWhitelist = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/disable_whitelist', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增安全组规则 */
|
||||
export const createSecurityGroupRule = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/create_rule', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改安全组规则 */
|
||||
export const updateSecurityGroupRule = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/update_rule', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除安全组规则 */
|
||||
export const deleteSecurityGroupRule = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete_rule', { params })
|
||||
}
|
||||
|
||||
/** 应用安全组 */
|
||||
export const applySecurityGroup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/apply', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - VNC 节点管理
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取 VNC 节点列表 */
|
||||
export const getVncNodeList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vnc/list', { params })
|
||||
}
|
||||
|
||||
/** 获取虚拟机 VNC 连接信息 */
|
||||
export const getVmVnc = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vnc/vm_vnc', { params })
|
||||
}
|
||||
|
||||
/** 新增 VNC 节点 */
|
||||
export const addVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 测试 VNC 节点连接 */
|
||||
export const testVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/test', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改 VNC 节点 */
|
||||
export const updateVncNode = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vnc/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 VNC 节点 */
|
||||
export const deleteVncNode = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/vnc/delete', { params })
|
||||
}
|
||||
|
||||
/** 设置安全组共享状态 */
|
||||
export const setSecurityGroupShared = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/post_group/set_shared', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 快照管理 ==========
|
||||
/** 获取快照列表 */
|
||||
export const getSnapshotList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/list', { params })
|
||||
}
|
||||
|
||||
/** 获取快照任务进度 */
|
||||
export const getSnapshotProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/progress', { params })
|
||||
}
|
||||
|
||||
/** 创建快照 */
|
||||
export const createSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复快照 */
|
||||
export const restoreSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/restore', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除快照 */
|
||||
export const deleteSnapshot = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/delete', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 备份管理 ==========
|
||||
/** 获取备份列表 */
|
||||
export const getBackupList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/list', { params })
|
||||
}
|
||||
|
||||
/** 获取备份任务进度 */
|
||||
export const getBackupProgress = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/progress', { params })
|
||||
}
|
||||
|
||||
/** 创建备份 */
|
||||
export const createBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复备份 */
|
||||
export const restoreBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/restore', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除备份 */
|
||||
export const deleteBackup = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/delete', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取快照数量与上限 */
|
||||
export const getSnapshotCount = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/snapshot/count', { params })
|
||||
}
|
||||
|
||||
/** 设置快照数量上限 */
|
||||
export const setSnapshotLimit = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/snapshot/set_limit', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取备份数量与上限 */
|
||||
export const getBackupCount = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/backup/count', { params })
|
||||
}
|
||||
|
||||
/** 设置备份数量上限 */
|
||||
export const setBackupLimit = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/backup/set_limit', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 用户组网管理 (UserNetworking)
|
||||
* ================================
|
||||
*/
|
||||
|
||||
/** 获取组网列表 */
|
||||
export const getUserNetworkingList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/networking/list', { params })
|
||||
}
|
||||
|
||||
/** 获取组网详情 */
|
||||
export const getUserNetworkingDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/networking/detail', { params })
|
||||
}
|
||||
|
||||
/** 创建用户组网 */
|
||||
export const createUserNetworking = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 为虚拟机分配组网 IP */
|
||||
export const assignUserNetworking = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/assign', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除组网 */
|
||||
export const deleteUserNetworking = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/networking/delete', { params })
|
||||
}
|
||||
|
||||
/** 删除组网下的指定网络 */
|
||||
export const removeUserNetworkingNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/networking/remove_network', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**重试订单流程 */
|
||||
export const retryOrderHook = (data) => {
|
||||
return http2.post('/api/v1/admin/order/retry_hook', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
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 getProductTagList = () => {
|
||||
return http2.get('/api/v1/admin/good/goods/tag_list')
|
||||
}
|
||||
/**创建商品 */
|
||||
export const createProduct = (data) => {
|
||||
return http2.post('/api/v1/admin/good/goods/create', data,{
|
||||
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', null, {
|
||||
params: 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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品套餐管理 */
|
||||
|
||||
/**获取商品套餐列表 */
|
||||
export const getProductPlanList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/plan/list', {params: params})
|
||||
}
|
||||
/**获取商品套餐详情 */
|
||||
export const getProductPlanDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/good/plan/detail', {params: params})
|
||||
}
|
||||
/**创建商品套餐 */
|
||||
export const createProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品套餐 */
|
||||
export const updateProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品套餐 */
|
||||
export const deleteProductPlan = (params) => {
|
||||
return http2.delete('/api/v1/admin/good/plan/delete', {params: params})
|
||||
}
|
||||
/**禁用商品套餐 */
|
||||
export const disableProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/disable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**启用商品套餐 */
|
||||
export const enableProductPlan = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/enable', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**禁用套餐固定价格 */
|
||||
export const disablePlanFixedPrice = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/disable_fixed_price', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**启用套餐固定价格 */
|
||||
export const enablePlanFixedPrice = (data) => {
|
||||
return http2.post('/api/v1/admin/good/plan/enable_fixed_price', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**商品分组标签管理 */
|
||||
|
||||
/**获取商品分组标签列表 */
|
||||
export const getProductGroupTagList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/group_tag/list', {params: params})
|
||||
}
|
||||
/**获取商品分组标签详情 */
|
||||
export const getProductGroupTagDetail = (params) => {
|
||||
return http2.get('/api/v1/admin/good/group_tag/detail', {params: params})
|
||||
}
|
||||
/**创建商品分组标签 */
|
||||
export const createProductGroupTag = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group_tag/create', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**更新商品分组标签 */
|
||||
export const updateProductGroupTag = (data) => {
|
||||
return http2.post('/api/v1/admin/good/group_tag/update', data,{
|
||||
headers:{
|
||||
'Content-Type':'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
/**删除商品分组标签 */
|
||||
export const deleteProductGroupTag = (params) => {
|
||||
return http2.delete('/api/v1/admin/good/group_tag/delete', {params: params})
|
||||
}
|
||||
|
||||
|
||||
/**---------------------------------- */
|
||||
/**已购商品管理 */
|
||||
|
||||
/**获取用户已购商品列表 */
|
||||
export const getUserGoodsList = (params) => {
|
||||
return http2.get('/api/v1/admin/good/user_goods/list', {params: params})
|
||||
}
|
||||
@@ -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,171 @@
|
||||
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 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?user_id='+data.user_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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**退款对应账单 */
|
||||
export const refundBalance = (data) => {
|
||||
return http2.get('/api/v1/admin/user/balance/refund', {
|
||||
params:data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const BASE = '/api/v1/admin/good/user_vm'
|
||||
const GOODS_BASE = '/api/v1/admin/good/user_goods'
|
||||
|
||||
const fd = (data) => {
|
||||
const f = new FormData()
|
||||
Object.entries(data).forEach(([k, v]) => {
|
||||
if (v === undefined || v === null) return
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach(item => f.append(k, item))
|
||||
} else if (typeof v === 'boolean') {
|
||||
f.append(k, v ? 'true' : 'false')
|
||||
} else {
|
||||
f.append(k, v)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
// ========== 用户虚拟机 ==========
|
||||
export const getUserVmList = (params) => http2.get(`${BASE}/list`, { params })
|
||||
export const getUserVmDetail = (params) => http2.get(`${BASE}/detail`, { params })
|
||||
export const getUserVmVnc = (params) => http2.get(`${BASE}/vnc`, { params })
|
||||
export const getUserVmHostImages = (params) => http2.get(`${BASE}/host_images`, { params })
|
||||
export const getGoodHostGroupImages = (params) => http2.get(`${BASE}/good_host_group_images`, { params })
|
||||
export const createUserVm = (data) => http2.post(`${BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const bindUserVm = (data) => http2.post(`${BASE}/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const transferUserVm = (data) => http2.post(`${BASE}/transfer`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const migrateUserVm = (data) => http2.post(`${BASE}/migrate`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmTraffic = (data) => http2.post(`${BASE}/update_traffic`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVm = (data) => http2.post(`${BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const refactorUserVm = (data) => http2.post(`${BASE}/refactor`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const startUserVm = (data) => http2.post(`${BASE}/start`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const stopUserVm = (data) => http2.post(`${BASE}/stop`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rebootUserVm = (data) => http2.post(`${BASE}/reboot`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const suspendUserVm = (data) => http2.post(`${BASE}/suspend`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const resumeUserVm = (data) => http2.post(`${BASE}/resume`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rescueUserVm = (data) => http2.post(`${BASE}/rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const exitRescueUserVm = (data) => http2.post(`${BASE}/exit_rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const rebuildUserVm = (data) => http2.post(`${BASE}/rebuild`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVm = (params) => http2.delete(`${BASE}/delete`, { params })
|
||||
|
||||
// ========== 数据卷 ==========
|
||||
export const getUserVmVolumeList = (params) => http2.get(`${BASE}/volume/list`, { params })
|
||||
export const getUserVmVolumeDetail = (params) => http2.get(`${BASE}/volume/detail`, { params })
|
||||
export const createUserVmVolume = (data) => http2.post(`${BASE}/volume/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const resizeUserVmVolume = (data) => http2.post(`${BASE}/volume/resize`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const mountUserVmVolume = (data) => http2.post(`${BASE}/volume/mount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const unmountUserVmVolume = (data) => http2.post(`${BASE}/volume/unmount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmVolume = (params) => http2.delete(`${BASE}/volume/delete`, { params })
|
||||
|
||||
// ========== 快照 ==========
|
||||
export const getUserVmSnapshotList = (params) => http2.get(`${BASE}/snapshot/list`, { params })
|
||||
export const getUserVmSnapshotProgress = (params) => http2.get(`${BASE}/snapshot/progress`, { params })
|
||||
export const getUserVmSnapshotCount = (params) => http2.get(`${BASE}/snapshot/count`, { params })
|
||||
export const createUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const restoreUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setUserVmSnapshotLimit = (data) => http2.post(`${BASE}/snapshot/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 备份 ==========
|
||||
export const getUserVmBackupList = (params) => http2.get(`${BASE}/backup/list`, { params })
|
||||
export const getUserVmBackupProgress = (params) => http2.get(`${BASE}/backup/progress`, { params })
|
||||
export const getUserVmBackupCount = (params) => http2.get(`${BASE}/backup/count`, { params })
|
||||
export const createUserVmBackup = (data) => http2.post(`${BASE}/backup/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const restoreUserVmBackup = (data) => http2.post(`${BASE}/backup/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmBackup = (data) => http2.post(`${BASE}/backup/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setUserVmBackupLimit = (data) => http2.post(`${BASE}/backup/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 安全组 ==========
|
||||
export const getUserVmPostGroupList = (params) => http2.get(`${BASE}/post_group/list`, { params })
|
||||
export const getUserVmPostGroupDetail = (params) => http2.get(`${BASE}/post_group/detail`, { params })
|
||||
export const getUserVmPostGroupUserList = (params) => http2.get(`${BASE}/post_group/user_list`, { params })
|
||||
export const createUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const bindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const unbindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/unbind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const applyUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/apply`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const setSharedUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/set_shared`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmPostGroup = (params) => http2.delete(`${BASE}/post_group/delete`, { params })
|
||||
export const enableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/enable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const disableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/disable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const createUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/create_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/update_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmPostGroupRule = (params) => http2.delete(`${BASE}/post_group/delete_rule`, { params })
|
||||
|
||||
// ========== 网络 ==========
|
||||
export const getUserVmNetworkList = (params) => http2.get(`${BASE}/network/list`, { params })
|
||||
export const getUserVmNetworkDetail = (params) => http2.get(`${BASE}/network/detail`, { params })
|
||||
|
||||
// ========== 组网 ==========
|
||||
export const getUserVmNetworkingList = (params) => http2.get(`${BASE}/networking/list`, { params })
|
||||
export const getUserVmNetworkingDetail = (params) => http2.get(`${BASE}/networking/detail`, { params })
|
||||
export const createUserVmNetworking = (data) => http2.post(`${BASE}/networking/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const assignUserVmNetworking = (data) => http2.post(`${BASE}/networking/assign`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const removeUserVmNetworkingNetwork = (data) => http2.post(`${BASE}/networking/remove_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserVmNetworking = (params) => http2.delete(`${BASE}/networking/delete`, { params })
|
||||
|
||||
// ========== 用户商品 ==========
|
||||
export const getUserGoodsList = (params) => http2.get(`${GOODS_BASE}/list`, { params })
|
||||
export const getUserGoodsDetail = (params) => http2.get(`${GOODS_BASE}/detail`, { params })
|
||||
export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params })
|
||||
|
||||
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
|
||||
|
||||
// ========== 流量策略 ==========
|
||||
// 测试未通过(接口新增,待联调)
|
||||
export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params })
|
||||
export const updateUserVmTrafficPolicy = (data) => http2.post(`${BASE}/traffic_policy/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const addUserVmFixedTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_fixed`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const addUserVmTemporaryTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_temporary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
|
||||
// ========== 到期提醒 ==========
|
||||
export const getExpireRemindList = (params) => http2.get(`${GOODS_BASE}/expire_remind/list`, { params })
|
||||
export const sendExpireRemind = (data) => http2.post(`${GOODS_BASE}/expire_remind/send`, data, { headers: { 'Content-Type': 'application/json' } })
|
||||
@@ -0,0 +1,24 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const fd = (data) => {
|
||||
const f = new FormData()
|
||||
Object.entries(data).forEach(([k, v]) => {
|
||||
if (v === undefined || v === null || v === '') return
|
||||
f.append(k, v)
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
const BASE_GROUP = '/api/v1/admin/server/vnc_command/group'
|
||||
const BASE_ITEM = '/api/v1/admin/server/vnc_command/item'
|
||||
|
||||
// 分组
|
||||
export const getVncCommandGroupList = () => http2.get(`${BASE_GROUP}/list`)
|
||||
export const createVncCommandGroup = (data) => http2.post(`${BASE_GROUP}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateVncCommandGroup = (data) => http2.post(`${BASE_GROUP}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteVncCommandGroup = (params) => http2.delete(`${BASE_GROUP}/delete`, { params })
|
||||
|
||||
// 指令项
|
||||
export const createVncCommandItem = (data) => http2.post(`${BASE_ITEM}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const updateVncCommandItem = (data) => http2.post(`${BASE_ITEM}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
export const deleteVncCommandItem = (params) => http2.delete(`${BASE_ITEM}/delete`, { params })
|
||||
@@ -0,0 +1,64 @@
|
||||
import { http2 } from "@/utils/request.js"
|
||||
|
||||
// ========== 后台菜单管理 ==========
|
||||
|
||||
/** 获取后台菜单列表 */
|
||||
export const getWebRoutsList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/list', { params })
|
||||
}
|
||||
|
||||
/** 新增后台菜单 */
|
||||
export const addWebRouts = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改后台菜单 */
|
||||
export const updateWebRouts = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除后台菜单 */
|
||||
export const deleteWebRouts = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/web_routs/delete', {
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 后台菜单权限管理 ==========
|
||||
|
||||
/** 获取后台菜单权限列表 */
|
||||
export const getWebRoutsPermissionList = (params) => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/permission/list', { params })
|
||||
}
|
||||
|
||||
/** 新增后台菜单权限 */
|
||||
export const addWebRoutsPermission = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/permission/add', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改后台菜单权限 */
|
||||
export const updateWebRoutsPermission = (data) => {
|
||||
return http2.post('/api/v1/admin/server/web_routs/permission/update', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除后台菜单权限 */
|
||||
export const deleteWebRoutsPermission = (data) => {
|
||||
return http2.delete('/api/v1/admin/server/web_routs/permission/delete', {
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取当前用户的后台菜单权限树 */
|
||||
export const getMyWebRoutsPermission = () => {
|
||||
return http2.get('/api/v1/admin/server/web_routs/my')
|
||||
}
|
||||
+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)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import request from "@/utils/request.js";
|
||||
|
||||
/**
|
||||
* 创建拼团
|
||||
* @param {Object} data - 拼团数据
|
||||
* @param {string} data.name - 拼团名称
|
||||
* @param {number} data.maxPerson - 最大人数
|
||||
* @param {string} data.cover - 封面图片URL
|
||||
* @returns {Promise} 返回拼团详情
|
||||
*/
|
||||
export const createGroupBuy = (data) => {
|
||||
return request.post("/api/v1/group-buy/create", data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查拼团
|
||||
* @param {string} groupBuyId - 拼团ID
|
||||
* @returns {Promise} 返回检查结果
|
||||
*/
|
||||
export const checkGroupBuy = (groupBuyId) => {
|
||||
return request.get(`/api/v1/group-buy/check/${groupBuyId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拼团详情
|
||||
* @param {string} groupBuyId - 拼团ID
|
||||
* @returns {Promise} 返回拼团详情
|
||||
*/
|
||||
export const getGroupBuyDetail = (groupBuyId) => {
|
||||
return request.get(`/api/v1/group-buy/${groupBuyId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拼团列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
* @returns {Promise} 返回拼团列表
|
||||
*/
|
||||
export const getGroupBuyList = (params) => {
|
||||
return request.get("/api/v1/users/activity/group_buy/list", params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入拼团
|
||||
* @param {string} groupBuyId - 拼团ID
|
||||
* @param {Object} data - 用户数据
|
||||
* @returns {Promise} 返回加入结果
|
||||
*/
|
||||
export const joinGroupBuy = (groupBuyId, data) => {
|
||||
return request.post(`/api/v1/group-buy/${groupBuyId}/join`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除拼团
|
||||
* @param {string} groupBuyId - 拼团ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export const deleteGroupBuy = (groupBuyId) => {
|
||||
return request.delete(`/api/v1/group-buy/${groupBuyId}`)
|
||||
}
|
||||
|
||||
// ==================== 拼团类型管理接口 ====================
|
||||
|
||||
/**
|
||||
* 获取拼团活动类型列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.count=10] - 每页条数
|
||||
* @param {string} [params.key] - 关键词筛选
|
||||
* @param {number} [params.expire_time] - 过期时间筛选(时间戳)
|
||||
* @param {string} [params.tag] - 标签筛选
|
||||
* @returns {Promise} 返回拼团类型列表
|
||||
*/
|
||||
export const getGroupBuyTypeList = (params) => {
|
||||
return request.get("/api/v1/admin/activity/group_buy/type/list", params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拼团活动类型标签列表
|
||||
* @returns {Promise} 返回标签列表
|
||||
*/
|
||||
export const getGroupBuyTypeTags = () => {
|
||||
return request.get("/api/v1/admin/activity/group_buy/type/tags")
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增拼团活动类型
|
||||
* @param {Object} data - 类型数据
|
||||
* @param {string} data.name - 名称
|
||||
* @param {string} [data.note] - 备注
|
||||
* @param {string} data.price - 价格(分)
|
||||
* @param {string} [data.renew_price] - 续费价格(分)
|
||||
* @param {string} data.max_person - 拼团需要人数
|
||||
* @param {string} [data.tag] - 标签
|
||||
* @param {number} [data.expire_time] - 活动过期时间
|
||||
* @returns {Promise} 返回新增结果
|
||||
*/
|
||||
export const addGroupBuyType = (data) => {
|
||||
return request.post("/api/v1/admin/activity/group_buy/type/add", data,{
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改拼团活动类型
|
||||
* @param {Object} data - 类型数据
|
||||
* @param {string} data.id - ID编号
|
||||
* @param {string} [data.name] - 名称
|
||||
* @param {string} [data.note] - 备注
|
||||
* @param {string} [data.price] - 价格(分)
|
||||
* @param {string} [data.renew_price] - 续费价格(分)
|
||||
* @param {string} [data.max_person] - 拼团需要人数
|
||||
* @param {string} [data.tag] - 标签
|
||||
* @param {number} [data.expire_time] - 活动过期时间
|
||||
* @returns {Promise} 返回修改结果
|
||||
*/
|
||||
export const updateGroupBuyType = (data) => {
|
||||
return request.post("/api/v1/admin/activity/group_buy/type/update", data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除拼团活动类型
|
||||
* @param {string} id - 类型ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export const deleteGroupBuyType = (id) => {
|
||||
return request.delete("/api/v1/admin/activity/group_buy/type/delete", { params: { id } })
|
||||
}
|
||||
|
||||
// ==================== 拼团队伍管理接口 ====================
|
||||
|
||||
/**
|
||||
* 检查队伍列表
|
||||
* @returns {Promise} 返回队伍检查结果
|
||||
*/
|
||||
export const checkGroupBuyTeams = () => {
|
||||
return request.get("/api/v1/admin/activity/group_buy/check")
|
||||
}
|
||||
|
||||
/**
|
||||
* 为队伍添加随机伪人
|
||||
* @param {string} groupBuyId - 队伍ID
|
||||
* @returns {Promise} 返回添加结果
|
||||
*/
|
||||
export const addRandomUser = (groupBuyId) => {
|
||||
return request.post("/api/v1/admin/activity/group_buy/add_random_user", { group_buy_id: groupBuyId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机伪人队伍
|
||||
* @param {Object} data - 队伍数据
|
||||
* @param {string} data.name - 队伍名称
|
||||
* @param {string} data.group_buy_type_id - 队伍类型ID
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export const addRandomGroup = (data) => {
|
||||
return request.post("/api/v1/admin/activity/group_buy/add_random_group", data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出成功队伍信息
|
||||
* @returns {Promise} 返回导出数据
|
||||
*/
|
||||
export const exportGroupBuyIdcInfo = () => {
|
||||
return request.get("/api/v1/admin/activity/group_buy/export_idc_info")
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定队伍下发订单
|
||||
* @param {string} groupBuyId - 队伍ID
|
||||
* @returns {Promise} 返回下发结果
|
||||
*/
|
||||
export const setGroupBuyOrder = (groupBuyId) => {
|
||||
return request.post("/api/v1/admin/activity/group_buy/set_order", { group_buy_id: groupBuyId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定队伍
|
||||
* @param {string} groupBuyId - 队伍ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export const removeGroupBuy = (groupBuyId) => {
|
||||
return request.delete("/api/v1/admin/activity/group_buy/remove", { params: { group_buy_id: groupBuyId } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有队伍
|
||||
* @returns {Promise} 返回清除结果
|
||||
*/
|
||||
export const clearAllGroupBuy = () => {
|
||||
return request.delete("/api/v1/admin/activity/group_buy/clear")
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的所有队伍
|
||||
* @param {string} userId - 用户ID
|
||||
* @returns {Promise} 返回清除结果
|
||||
*/
|
||||
export const clearUserGroupBuy = (userId) => {
|
||||
return request.delete("/api/v1/admin/activity/group_buy/user_clear", { params: { user_id: userId } })
|
||||
}
|
||||
@@ -7,4 +7,14 @@ export const userLogin = (username,password) => {
|
||||
|
||||
export const getUserInfo = () => {
|
||||
return request.get("/api/v1/users/info/info")
|
||||
}
|
||||
|
||||
// 获取交换token(用于无感刷新)
|
||||
export const getRefreshToken = (domain) => {
|
||||
return request.get("/api/v1/users/info/refresh_token", { domain })
|
||||
}
|
||||
|
||||
// 使用交换token获取新的access token
|
||||
export const refreshAccessToken = (refresh_token) => {
|
||||
return request.post("/api/v1/user/refresh_token", { refresh_token })
|
||||
}
|
||||
+61
-5
@@ -5,8 +5,15 @@ import request from "@/utils/request.js";
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
export function getTickerList(count, page, status) {
|
||||
return request.get('/api/v1/admin/work_order/list', { count, page, status })
|
||||
export function getTickerList(count, page, status, orderBy, order, userId, keyword) {
|
||||
const params = { count, page }
|
||||
if (status !== undefined && status !== '') params.status = status
|
||||
if (orderBy) params.orderBy = orderBy
|
||||
if (order) params.order = order
|
||||
if (userId) params.user_id = userId
|
||||
if (keyword) params.keyword = keyword
|
||||
console.log('工单列表请求参数:', params) // 调试日志
|
||||
return request.get('/api/v1/admin/work_order/list', params)
|
||||
}
|
||||
|
||||
// 待处理
|
||||
@@ -29,12 +36,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 +74,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,272 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="选择用户"
|
||||
width="700px"
|
||||
class="user-selector-dialog"
|
||||
append-to-body
|
||||
@update:model-value="handleVisibleChange"
|
||||
>
|
||||
<div class="user-selector-content">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="selector-search">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索用户名、邮箱或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="handleReset" class="reset-btn">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
max-height="350"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||
>
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column prop="user_name" label="用户名" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="user-name-cell">
|
||||
<el-avatar v-if="row.cover" :src="row.cover" :size="28" />
|
||||
<el-avatar v-else :size="28">
|
||||
{{ row.user_name?.charAt(0)?.toUpperCase() || 'U' }}
|
||||
</el-avatar>
|
||||
<span class="user-name">{{ row.user_name || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="text-ellipsis">{{ row.email || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.disable" type="danger" size="small">禁用</el-tag>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
background
|
||||
small
|
||||
class="selector-pagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<span v-if="selectedUser" class="selected-info">
|
||||
已选择: <el-tag type="primary" size="small">{{ selectedUser.user_name }} (ID: {{ selectedUser.user_id }})</el-tag>
|
||||
</span>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getUserList } from '@/api/admin/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'select'])
|
||||
|
||||
const loading = ref(false)
|
||||
const userList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedUser = ref(null)
|
||||
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 visible 变化,打开时加载数据
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
selectedUser.value = null
|
||||
fetchUserList()
|
||||
}
|
||||
})
|
||||
|
||||
const handleVisibleChange = (val) => {
|
||||
emit('update:visible', val)
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const fetchUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserList(searchParams)
|
||||
if (res.data.code === 200) {
|
||||
userList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedUser.value = row
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const confirmSelection = () => {
|
||||
if (!selectedUser.value) {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
return
|
||||
}
|
||||
emit('select', selectedUser.value)
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-selector-content {
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selector-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selector-pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
margin-right: auto;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row):hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.current-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.current-row td) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-avatar) {
|
||||
background-color: var(--el-color-primary-light-5);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="avatar-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 文件列表 -->
|
||||
<el-tab-pane label="文件" name="userFiles">
|
||||
<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: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
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')
|
||||
formData.append('open_down', 'true')
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择优惠码"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="discount-code-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择优惠码 -->
|
||||
<el-tab-pane label="选择优惠码" name="selectCode">
|
||||
<div class="code-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索优惠码名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 优惠码列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="codeList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="优惠码ID" width="100" align="center" />
|
||||
<el-table-column prop="name" label="优惠码名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="code" label="优惠码" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" effect="plain">{{ row.code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
|
||||
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠值" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.percentage > 0" class="discount-value">{{ row.percentage }}%</span>
|
||||
<span v-else class="discount-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>无限制</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'expired': isExpired(row.endTime) }">
|
||||
{{ formatDate(row.endTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="codeList.length === 0 && !loading" description="暂无优惠码数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedCode"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getDiscountCodeList } from '@/api/admin/discount'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的优惠码ID(用于回显)
|
||||
currentCodeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 类型过滤:discount_code - 优惠码
|
||||
codeType: {
|
||||
type: String,
|
||||
default: 'code'
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectCode')
|
||||
const loading = ref(false)
|
||||
const codeList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedCode = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectCode'
|
||||
selectedCode.value = null
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取优惠码列表
|
||||
const fetchCodeList = async () => {
|
||||
loading.value = true
|
||||
codeList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
discount_type: props.codeType
|
||||
}
|
||||
if (searchParams.key) {
|
||||
params.key = searchParams.key
|
||||
}
|
||||
|
||||
const res = await getDiscountCodeList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
codeList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的优惠码ID,自动选中
|
||||
if (props.currentCodeId) {
|
||||
const currentCode = codeList.value.find(
|
||||
code => code.id === props.currentCodeId
|
||||
)
|
||||
if (currentCode) {
|
||||
selectedCode.value = currentCode
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取优惠码列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠码列表失败:', error)
|
||||
ElMessage.error('获取优惠码列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectCode') {
|
||||
fetchCodeList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchCodeList()
|
||||
}
|
||||
|
||||
// 选择优惠码
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedCode.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedCode.value && row.id === selectedCode.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedCode.value = null
|
||||
codeList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 判断是否过期
|
||||
const isExpired = (endTime) => {
|
||||
if (!endTime) return false
|
||||
return new Date(endTime) < new Date()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedCode.value) {
|
||||
emit('confirm', selectedCode.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个优惠码')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discount-code-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.code-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.discount-value {
|
||||
color: #e6a23c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expired {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择宿主机组" width="650px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索宿主机组名称" clearable style="width:200px" @keyup.enter="handleSearch" @clear="handleSearch" />
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="serviceId" label="服务ID" width="80" />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getHostGroupList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!keyword.value) return list.value
|
||||
const kw = keyword.value.toLowerCase()
|
||||
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw))
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getHostGroupList({ service_id: props.serviceId, page: page.value, count: pageSize })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const items = Array.isArray(body.data) ? body.data : (body.data.data || body.data.list || [])
|
||||
list.value = items.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name ?? i.Name,
|
||||
note: i.note ?? i.Note,
|
||||
serviceId: i.serviceId ?? i.service_id ?? 0,
|
||||
serviceHostGroupId: i.serviceHostGroupId ?? 0
|
||||
}))
|
||||
total.value = body.data.total ?? body.data.all_count ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择宿主机" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索宿主机名称/IP" clearable style="width:200px" @keyup.enter="loadList" @clear="loadList" />
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="ip" label="IP" min-width="130" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '在线' : '离线' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getRemoteHostList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostGroupId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!keyword.value) return list.value
|
||||
const kw = keyword.value.toLowerCase()
|
||||
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw) || (i.ip || '').includes(kw))
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (props.hostGroupId) params.host_group_id = props.hostGroupId
|
||||
const res = await getRemoteHostList(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
const hosts = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
list.value = hosts.map(i => ({
|
||||
id: i.id, name: i.name, ip: i.ip, is_active: i.is_active ?? true,
|
||||
host_group_id: i.host_group_id
|
||||
}))
|
||||
total.value = inner.total ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="icon-selector">
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
@click="popoverVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon v-if="modelValue" :size="18">
|
||||
<component :is="modelValue" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<el-icon v-if="modelValue" class="clear-btn" @click.stop="handleClear"><CircleClose /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog v-model="popoverVisible" title="选择图标" width="680px" append-to-body>
|
||||
<el-input v-model="searchKey" placeholder="搜索图标名称" clearable class="icon-search">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="icon-grid">
|
||||
<div
|
||||
v-for="name in filteredIcons"
|
||||
:key="name"
|
||||
class="icon-item"
|
||||
:class="{ active: modelValue === name }"
|
||||
@click="handleSelect(name)"
|
||||
>
|
||||
<el-icon :size="22"><component :is="name" /></el-icon>
|
||||
<span class="icon-name">{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredIcons.length === 0" class="icon-empty">
|
||||
未找到匹配的图标
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { Search, CircleClose } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const popoverVisible = ref(false)
|
||||
const searchKey = ref('')
|
||||
|
||||
const allIcons = Object.keys(ElementPlusIconsVue).sort()
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
if (!searchKey.value) return allIcons
|
||||
const key = searchKey.value.toLowerCase()
|
||||
return allIcons.filter(name => name.toLowerCase().includes(key))
|
||||
})
|
||||
|
||||
const handleSelect = (name) => {
|
||||
emit('update:modelValue', name)
|
||||
popoverVisible.value = false
|
||||
searchKey.value = ''
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-selector { width: 100%; }
|
||||
.icon-search { margin-bottom: 12px; }
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 10px 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.icon-item:hover {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.icon-item.active {
|
||||
border-color: #409eff;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
.icon-name {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icon-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.clear-btn {
|
||||
cursor: pointer;
|
||||
color: #c0c4cc;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.clear-btn:hover { color: #f56c6c; }
|
||||
</style>
|
||||
@@ -0,0 +1,684 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择图片"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="image-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 文件库 -->
|
||||
<el-tab-pane label="文件库" name="fileLibrary">
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
<h4>图片文件库</h4>
|
||||
<div class="header-actions">
|
||||
<span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
|
||||
已选 {{ selectedIds.size }} 个文件
|
||||
</span>
|
||||
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||
上传新图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索过滤 -->
|
||||
<div class="filter-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="width: 300px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="file-grid" v-loading="loading">
|
||||
<div
|
||||
v-for="file in filteredFileList"
|
||||
:key="file.id"
|
||||
class="file-item"
|
||||
:class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
<div class="file-check-badge" v-if="props.multiple && selectedIds.has(file.id)">
|
||||
<el-icon><Select /></el-icon>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<img
|
||||
:src="processImageUrl(file.url)"
|
||||
:alt="file.realName"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<p class="file-name" :title="file.realName">{{ file.realName }}</p>
|
||||
<p class="file-size">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="filteredFileList.length === 0 && !loading" description="暂无图片文件" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<el-tab-pane label="上传图片" name="upload">
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
accept="image/*"
|
||||
multiple
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持jpg、png、gif、webp等图片格式,单个文件不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<!-- 待上传文件列表 -->
|
||||
<div v-if="pendingFiles.length > 0" class="pending-files">
|
||||
<div class="pending-header">
|
||||
<h4>待上传文件 ({{ pendingFiles.length }})</h4>
|
||||
<el-button type="danger" link @click="pendingFiles = []">清空</el-button>
|
||||
</div>
|
||||
<div class="pending-list">
|
||||
<div v-for="(file, index) in pendingFiles" :key="index" class="pending-item">
|
||||
<img :src="file.previewUrl" class="pending-preview" />
|
||||
<span class="pending-name" :title="file.name">{{ file.name }}</span>
|
||||
<span class="pending-size">{{ formatFileSize(file.size) }}</span>
|
||||
<el-button type="danger" link size="small" @click="removePendingFile(index)">移除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleBatchUpload"
|
||||
:loading="uploading"
|
||||
style="margin-top: 16px; width: 100%;"
|
||||
>
|
||||
开始上传 ({{ pendingFiles.length }} 个文件)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="props.multiple ? selectedIds.size === 0 : !selectedId"
|
||||
>
|
||||
确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, UploadFilled, Search, Select, Delete } from '@element-plus/icons-vue'
|
||||
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentFileId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('fileLibrary')
|
||||
const fileList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedId = ref('')
|
||||
const selectedIds = ref(new Set()) // 多选模式下选中的文件ID集合
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const total = ref(0)
|
||||
const searchKeyword = ref('')
|
||||
const pendingFiles = ref([]) // 待上传文件列表
|
||||
const uploading = ref(false) // 批量上传中
|
||||
let fetchVersion = 0 // 防止 fetchFileList 竞态条件
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
selectedId.value = props.currentFileId
|
||||
selectedIds.value = new Set()
|
||||
currentPage.value = 1
|
||||
searchKeyword.value = ''
|
||||
fetchFileList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 过滤后的文件列表
|
||||
const filteredFileList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return fileList.value
|
||||
}
|
||||
return fileList.value.filter(file =>
|
||||
file.realName?.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 处理图片URL,确保正确显示
|
||||
const processImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
// 先处理转义字符:将 \u0026 替换为 &
|
||||
let processedUrl = url.replace(/\\u0026/g, '&')
|
||||
// 再进行URL解码
|
||||
return decodeURIComponent(processedUrl)
|
||||
}
|
||||
|
||||
// 获取文件列表(带版本号防止竞态条件)
|
||||
const fetchFileList = async () => {
|
||||
const currentFetchVersion = ++fetchVersion
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await getFileList({
|
||||
page: currentPage.value,
|
||||
count: pageSize.value
|
||||
})
|
||||
|
||||
// 如果有更新的请求发起,丢弃当前结果
|
||||
if (currentFetchVersion !== fetchVersion) return
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const list = res.data.data.list || []
|
||||
total.value = res.data.data.all_count || 0
|
||||
|
||||
// 并行获取所有文件详情(替代逐个串行,大幅提升速度)
|
||||
const detailPromises = list.map(item =>
|
||||
getFileDetail({ file_id: item.id })
|
||||
.then(res2 => {
|
||||
if (res2.data.code === 200) {
|
||||
return {
|
||||
id: res2.data.data.data.id,
|
||||
url: res2.data.data.url,
|
||||
size: res2.data.data.data.size,
|
||||
realName: res2.data.data.data.realName
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取文件详情失败:', error)
|
||||
return null
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.all(detailPromises)
|
||||
|
||||
// 再次检查版本号,防止旧结果覆盖新结果
|
||||
if (currentFetchVersion !== fetchVersion) return
|
||||
|
||||
fileList.value = results.filter(item => item !== null)
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentFetchVersion === fetchVersion) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
ElMessage.error('获取文件列表失败')
|
||||
}
|
||||
} finally {
|
||||
if (currentFetchVersion === fetchVersion) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.name === 'fileLibrary') {
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索时重置到第一页
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchFileList()
|
||||
}
|
||||
|
||||
// 切换到上传标签页
|
||||
const switchToUpload = () => {
|
||||
activeTab.value = 'upload'
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (!size) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let unitIndex = 0
|
||||
let fileSize = size
|
||||
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
const selectFile = (file) => {
|
||||
if (props.multiple) {
|
||||
// 多选模式:切换选中状态
|
||||
const newSet = new Set(selectedIds.value)
|
||||
if (newSet.has(file.id)) {
|
||||
newSet.delete(file.id)
|
||||
} else {
|
||||
newSet.add(file.id)
|
||||
}
|
||||
selectedIds.value = newSet
|
||||
} else {
|
||||
selectedId.value = file.id
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择变化(收集待上传文件)
|
||||
const handleFileChange = (file) => {
|
||||
const rawFile = file.raw
|
||||
if (!rawFile) return
|
||||
|
||||
// 验证文件类型
|
||||
const isImage = rawFile.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error(`${rawFile.name} 超过 5MB,已跳过`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否重复添加
|
||||
const exists = pendingFiles.value.some(f => f.name === rawFile.name && f.size === rawFile.size)
|
||||
if (exists) return
|
||||
|
||||
// 添加到待上传列表,生成本地预览URL
|
||||
pendingFiles.value.push({
|
||||
raw: rawFile,
|
||||
name: rawFile.name,
|
||||
size: rawFile.size,
|
||||
previewUrl: URL.createObjectURL(rawFile)
|
||||
})
|
||||
}
|
||||
|
||||
// 移除待上传文件
|
||||
const removePendingFile = (index) => {
|
||||
const file = pendingFiles.value[index]
|
||||
if (file?.previewUrl) {
|
||||
URL.revokeObjectURL(file.previewUrl)
|
||||
}
|
||||
pendingFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 批量上传(所有文件合并为一次请求,多个 file_names 和 files 条目)
|
||||
const handleBatchUpload = async () => {
|
||||
if (pendingFiles.value.length === 0) {
|
||||
ElMessage.warning('请先选择要上传的文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
|
||||
const formData = new FormData()
|
||||
pendingFiles.value.forEach(file => {
|
||||
formData.append('file_names', file.name)
|
||||
formData.append('files', file.raw)
|
||||
})
|
||||
formData.append('update_type', 'cover')
|
||||
formData.append('open_down', 'true')
|
||||
|
||||
try {
|
||||
const res = await uploadFile(formData)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const count = pendingFiles.value.length
|
||||
// 释放所有预览URL
|
||||
pendingFiles.value.forEach(f => {
|
||||
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
|
||||
})
|
||||
pendingFiles.value = []
|
||||
ElMessage.success(`成功上传 ${count} 个文件`)
|
||||
|
||||
// 刷新文件列表并切换到文件库
|
||||
currentPage.value = 1
|
||||
await fetchFileList()
|
||||
activeTab.value = 'fileLibrary'
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error)
|
||||
ElMessage.error('上传失败,请重试')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载错误处理
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedId.value = ''
|
||||
selectedIds.value = new Set()
|
||||
fileList.value = []
|
||||
currentPage.value = 1
|
||||
total.value = 0
|
||||
searchKeyword.value = ''
|
||||
// 清理待上传文件的预览URL
|
||||
pendingFiles.value.forEach(f => {
|
||||
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
|
||||
})
|
||||
pendingFiles.value = []
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (props.multiple) {
|
||||
// 多选模式:返回选中的文件数组
|
||||
if (selectedIds.value.size === 0) return
|
||||
const selectedFiles = fileList.value
|
||||
.filter(file => selectedIds.value.has(file.id))
|
||||
.map(file => ({
|
||||
id: file.id,
|
||||
url: file.url || '',
|
||||
realName: file.realName || ''
|
||||
}))
|
||||
emit('confirm', selectedFiles)
|
||||
handleClose()
|
||||
} else {
|
||||
// 单选模式:返回单个文件对象
|
||||
if (selectedId.value) {
|
||||
const selectedFile = fileList.value.find(file => file.id === selectedId.value)
|
||||
emit('confirm', {
|
||||
id: selectedId.value,
|
||||
url: selectedFile?.url || '',
|
||||
realName: selectedFile?.realName || ''
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-selector {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.file-list-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-list-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #409EFF;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
border-color: #409EFF;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-check-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
color: #409EFF;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
margin: 0 0 4px 0;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 待上传文件列表 */
|
||||
.pending-files {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pending-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pending-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pending-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pending-item:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.pending-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.pending-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pending-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择镜像" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索镜像名称" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="filterOsType" placeholder="系统类型" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="Linux" value="linux" />
|
||||
<el-option label="Windows" value="windows" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="系统" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.os_type === 'linux' ? 'success' : 'primary'" size="small">{{ row.os_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'system' ? '' : 'warning'" size="small">{{ row.type === 'system' ? '系统' : '数据' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getImageList } from '@/api/admin/kvmService'
|
||||
import { getUserVmHostImages, getGoodHostGroupImages } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
goodId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 },
|
||||
useUserVmApi: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const keyword = ref('')
|
||||
const filterOsType = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
let res
|
||||
if (props.goodId > 0) {
|
||||
const params = { good_id: props.goodId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getGoodHostGroupImages(params)
|
||||
} else if (props.useUserVmApi) {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getUserVmHostImages(params)
|
||||
} else {
|
||||
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (filterOsType.value) params.os_type = filterOsType.value
|
||||
res = await getImageList(params)
|
||||
}
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
let items = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
if (props.useUserVmApi || props.goodId > 0) {
|
||||
items = items.map(item => item.image || item).filter(Boolean)
|
||||
}
|
||||
list.value = items
|
||||
total.value = inner.total ?? inner.all_count ?? list.value.length
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择主控服务" width="640px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索服务名称/地址" clearable style="width:220px"
|
||||
@keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="320" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="服务名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="地址" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="font-family:monospace;color:#409eff">{{ row.host }}:{{ row.port }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无主控服务" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getKvmServiceList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Boolean, default: false } })
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
const res = await getKvmServiceList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
list.value = raw.map(s => ({ id: s.id ?? s.Id, name: s.name ?? s.Name, host: s.host ?? s.Host, port: s.port ?? s.Port, note: s.note ?? s.Note }))
|
||||
total.value = inner.all_count ?? inner.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
const handleRefresh = () => { keyword.value = ''; page.value = 1; loadList() }
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="menu-path-selector">
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
placeholder="点击从菜单中选择路径,或手动输入"
|
||||
clearable
|
||||
@input="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="dialogVisible = true">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="选择菜单路径" width="550px" append-to-body>
|
||||
<el-input v-model="searchKey" placeholder="搜索菜单名称或路径" clearable class="path-search">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="menu-tree">
|
||||
<el-tree
|
||||
:data="filteredMenuTree"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
node-key="path"
|
||||
:default-expand-all="!!searchKey"
|
||||
:expand-on-click-node="false"
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="tree-node" :class="{ 'is-selected': modelValue === data.path, 'no-path': !data.path }">
|
||||
<el-icon v-if="data.icon" :size="16" style="margin-right: 6px; flex-shrink: 0;">
|
||||
<component :is="data.icon" />
|
||||
</el-icon>
|
||||
<span class="node-title">{{ data.title }}</span>
|
||||
<el-tag v-if="data.path" size="small" type="info" class="node-path">{{ data.path }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
<div v-if="filteredMenuTree.length === 0" class="tree-empty">
|
||||
未找到匹配的菜单
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { Search, FolderOpened } from '@element-plus/icons-vue'
|
||||
import { menus } from '@/config/menus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const searchKey = ref('')
|
||||
|
||||
const buildTreeData = (menuList) => {
|
||||
return menuList.map(item => {
|
||||
const node = {
|
||||
path: item.path || '',
|
||||
title: item.title,
|
||||
icon: item.icon || '',
|
||||
label: item.title
|
||||
}
|
||||
if (item.children?.length) {
|
||||
node.children = buildTreeData(item.children)
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const menuTree = computed(() => buildTreeData(menus))
|
||||
|
||||
const filterTree = (nodes, keyword) => {
|
||||
const key = keyword.toLowerCase()
|
||||
const result = []
|
||||
for (const node of nodes) {
|
||||
const titleMatch = node.title?.toLowerCase().includes(key)
|
||||
const pathMatch = node.path?.toLowerCase().includes(key)
|
||||
let filteredChildren = []
|
||||
if (node.children?.length) {
|
||||
filteredChildren = filterTree(node.children, keyword)
|
||||
}
|
||||
if (titleMatch || pathMatch || filteredChildren.length > 0) {
|
||||
result.push({
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : node.children
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const filteredMenuTree = computed(() => {
|
||||
if (!searchKey.value) return menuTree.value
|
||||
return filterTree(menuTree.value, searchKey.value)
|
||||
})
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
if (!data.path) return
|
||||
emit('update:modelValue', data.path)
|
||||
dialogVisible.value = false
|
||||
searchKey.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-path-selector { width: 100%; }
|
||||
.path-search { margin-bottom: 12px; }
|
||||
.menu-tree {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tree-node.is-selected {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.tree-node.no-path {
|
||||
color: #909399;
|
||||
cursor: default;
|
||||
}
|
||||
.node-title { margin-right: 8px; font-size: 13px; }
|
||||
.node-path { flex-shrink: 0; }
|
||||
.tree-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.el-tree-node__content) { height: 36px; }
|
||||
:deep(.el-tree-node__content:hover) { background-color: #f5f7fa; }
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-if="!filterType" v-model="typeFilter" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="网桥(Bridge)" value="bridge" />
|
||||
<el-option label="内网(NAT)" value="nat" />
|
||||
</el-select>
|
||||
<el-tag v-else type="success" size="small">仅{{ filterType === 'bridge' ? '网桥' : filterType === 'nat' ? '内网' : filterType }}</el-tag>
|
||||
<el-select v-if="!filterUsed" v-model="usedFilter" placeholder="占用状态" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="未占用" value="false" />
|
||||
<el-option label="已占用" value="true" />
|
||||
</el-select>
|
||||
<el-tag v-else :type="filterUsed === 'false' ? 'success' : 'info'" size="small">{{ filterUsed === 'false' ? '仅未占用' : '仅已占用' }}</el-tag>
|
||||
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
|
||||
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="gateway" label="网关" width="130" />
|
||||
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row._used === true" type="danger" size="small">已占用</el-tag>
|
||||
<el-tag v-else-if="row._used === false" type="success" size="small">空闲</el-tag>
|
||||
<el-tag v-else type="info" size="small">-</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建网络</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getNetworkList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 },
|
||||
filterType: { type: String, default: '' },
|
||||
filterUsed: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const typeFilter = ref('')
|
||||
const usedFilter = ref('')
|
||||
const ipVersionFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
const type = ref('bridge')
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
typeFilter.value = props.filterType || ''
|
||||
usedFilter.value = props.filterUsed || ''
|
||||
ipVersionFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId || !props.hostId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value }
|
||||
const effectiveType = props.filterType || typeFilter.value || type.value
|
||||
if (effectiveType) params.type = effectiveType
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const effectiveUsed = props.filterUsed || usedFilter.value
|
||||
if (effectiveUsed) params.used = effectiveUsed
|
||||
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
|
||||
const res = await getNetworkList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const items = inner.data || inner.networks || (Array.isArray(inner) ? inner : [])
|
||||
list.value = items.map(item => ({
|
||||
...item,
|
||||
_used: item.used !== undefined ? item.used
|
||||
: effectiveUsed === 'true' ? true
|
||||
: effectiveUsed === 'false' ? false
|
||||
: null
|
||||
}))
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
emit('create')
|
||||
}
|
||||
|
||||
defineExpose({ loadList })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择订单" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索订单名称/ID" clearable style="width:220px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="360" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="订单名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="价格" width="100">
|
||||
<template #default="{ row }">¥{{ ((row.price || 0) / 100).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.state === 1 ? 'success' : row.state === 0 ? 'warning' : 'info'" size="small">
|
||||
{{ row.state === 1 ? '已支付' : row.state === 0 ? '待支付' : '已失效' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期时间" width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.expireTime || row.expire_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无订单" />
|
||||
<div class="selector-selected" v-if="selected">
|
||||
<el-tag type="primary" size="large" closable @close="selected = null">已选:{{ selected.name }} (ID: {{ selected.id }})</el-tag>
|
||||
</div>
|
||||
<div class="selector-footer-bar">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }" @current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getOrderList } from '@/api/admin/order'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Boolean, default: false } })
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm') : '-'
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
const res = await getOrderList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.list || d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
const handleRefresh = () => { keyword.value = ''; page.value = 1; loadList() }
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-selected { margin-top: 12px; }
|
||||
.selector-footer-bar { display: flex; justify-content: flex-end; align-items: center; margin-top: 10px; }
|
||||
</style>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择路径权限"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="permission-selector">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索路径或名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求方法">
|
||||
<el-select
|
||||
v-model="searchParams.method"
|
||||
placeholder="全部方法"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
<el-option label="DELETE" value="DELETE" />
|
||||
<el-option label="PATCH" value="PATCH" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 权限列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="400"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="method" label="方法" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.method" :type="getMethodTag(row.method)" size="small">
|
||||
{{ row.method }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 已选信息 -->
|
||||
<div class="selected-info" v-if="selectedPermission">
|
||||
<el-alert type="success" :closable="false">
|
||||
<template #title>
|
||||
<div class="selected-content">
|
||||
<span>已选择: </span>
|
||||
<el-tag v-if="selectedPermission.method" :type="getMethodTag(selectedPermission.method)" size="small" style="margin-right: 8px;">
|
||||
{{ selectedPermission.method }}
|
||||
</el-tag>
|
||||
<span class="selected-path">{{ selectedPermission.path }}</span>
|
||||
<span class="selected-name" v-if="selectedPermission.name"> - {{ selectedPermission.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedPermission">
|
||||
确认选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getPermissionList } from '@/api/admin/Permission'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentPermissionId: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
method: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const permissionList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedPermission = ref(null)
|
||||
|
||||
// 过滤后的列表
|
||||
const filteredList = computed(() => {
|
||||
let list = permissionList.value
|
||||
|
||||
// 关键词过滤
|
||||
if (searchParams.key) {
|
||||
const keyword = searchParams.key.toLowerCase()
|
||||
list = list.filter(item =>
|
||||
(item.path && item.path.toLowerCase().includes(keyword)) ||
|
||||
(item.name && item.name.toLowerCase().includes(keyword)) ||
|
||||
(item.note && item.note.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 方法过滤
|
||||
if (searchParams.method) {
|
||||
list = list.filter(item => item.method === searchParams.method)
|
||||
}
|
||||
|
||||
return list
|
||||
})
|
||||
|
||||
// 获取方法标签颜色
|
||||
const getMethodTag = (method) => {
|
||||
const tagMap = {
|
||||
'GET': 'success',
|
||||
'POST': 'primary',
|
||||
'PUT': 'warning',
|
||||
'DELETE': 'danger',
|
||||
'PATCH': 'info'
|
||||
}
|
||||
return tagMap[method?.toUpperCase()] || 'info'
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedPermission.value && row.id === selectedPermission.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
if (props.currentPermissionId && row.id === props.currentPermissionId) {
|
||||
return 'current-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取权限列表
|
||||
const fetchPermissionList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPermissionList({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
permissionList.value = res.data.data?.list || []
|
||||
total.value = permissionList.value.length
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '获取权限列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error)
|
||||
ElMessage.error('获取权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.method = ''
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
}
|
||||
|
||||
// 选择行
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedPermission.value = row
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedPermission.value) {
|
||||
emit('confirm', selectedPermission.value)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedPermission.value = null
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 监听弹窗打开
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
fetchPermissionList()
|
||||
// 如果有当前选中的ID,尝试预选
|
||||
if (props.currentPermissionId) {
|
||||
const found = permissionList.value.find(p => p.id === props.currentPermissionId)
|
||||
if (found) {
|
||||
selectedPermission.value = found
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-selector {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafbfc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.selected-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selected-path {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
:deep(.el-table .selected-row) {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .current-row) {
|
||||
background-color: #f0f9eb !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .selected-row td),
|
||||
:deep(.el-table .current-row td) {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.el-dialog) {
|
||||
width: 95% !important;
|
||||
margin: 2vh auto !important;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-form .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-form .el-input,
|
||||
.search-form .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择套餐" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建套餐</el-button>
|
||||
<span style="color:#909399;font-size:13px" v-if="goodId">商品 ID: {{ goodId }}</span>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="300" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="套餐名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="说明" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.disable ? 'danger' : 'success'" size="small">{{ row.disable ? '禁用' : '启用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无套餐" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }" @current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新建套餐弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新建套餐" width="680px" append-to-body destroy-on-close class="scrollable-dialog">
|
||||
<el-form :model="createForm" label-width="90px">
|
||||
<el-form-item label="套餐名称" required><el-input v-model="createForm.name" placeholder="请输入套餐名称" /></el-form-item>
|
||||
<el-form-item label="说明"><el-input v-model="createForm.note" type="textarea" :rows="2" placeholder="请输入套餐说明" /></el-form-item>
|
||||
<el-form-item label="参数配置">
|
||||
<div style="width:100%">
|
||||
<div v-if="!goodId" style="color:#c0c4cc;font-size:13px">请先选择商品</div>
|
||||
<div v-else-if="createSpecLoading" style="color:#909399;font-size:13px">加载参数中...</div>
|
||||
<div v-else-if="createSpecList.length === 0" style="color:#909399;font-size:13px">该商品暂无参数</div>
|
||||
<div v-else>
|
||||
<div v-for="spec in createSpecList" :key="spec.id" style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #f5f5f5">
|
||||
<div style="font-size:13px;font-weight:500;color:#303133;margin-bottom:6px">
|
||||
{{ spec.name }}
|
||||
<el-tag v-if="spec.must" size="small" type="danger" style="margin-left:4px">必填</el-tag>
|
||||
</div>
|
||||
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
|
||||
<el-radio-group v-model="createSpecValues[spec.id]" size="small" @change="buildCreateArgsJson">
|
||||
<el-radio-button v-for="attr in spec.attrs" :key="attr.id" :value="attr.id">{{ attr.name }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template v-else-if="spec.type === 'number'">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<el-input-number
|
||||
v-model="createDisplayValues[spec.id]"
|
||||
:min="hasUnit(spec) ? fromBaseUnit(spec.min ?? 0, createDisplayUnits[spec.id], getArgKey(spec)) : (spec.min ?? 0)"
|
||||
:max="hasUnit(spec) ? fromBaseUnit(spec.max ?? 0, createDisplayUnits[spec.id], getArgKey(spec)) : (spec.max ?? 0)"
|
||||
:step="hasUnit(spec) ? (fromBaseUnit(spec.step ?? 1, createDisplayUnits[spec.id], getArgKey(spec)) || 1) : (spec.step ?? 1)"
|
||||
:step-strictly="true"
|
||||
size="small"
|
||||
@change="onCreateNumberChange(spec)"
|
||||
style="width:180px"
|
||||
/>
|
||||
<el-select v-if="hasUnit(spec)" :model-value="createDisplayUnits[spec.id]" size="small" style="width:90px" @change="(newUnit) => onCreateUnitChange(spec, newUnit)">
|
||||
<el-option v-for="u in getParamUnits(spec)" :key="u" :label="u" :value="u" />
|
||||
</el-select>
|
||||
<span style="font-size:12px;color:#909399">范围: {{ spec.min ?? 0 }} ~ {{ spec.max ?? 0 }}
|
||||
<template v-if="hasUnit(spec)"> {{ getBaseUnit(getArgKey(spec)) }}</template>,步长: {{ spec.step ?? 1 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="createSpecValues[spec.id]" placeholder="请输入值" size="small" style="width:200px" @input="buildCreateArgsJson" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="createForm.args" style="margin-top:8px">
|
||||
<div style="font-size:12px;color:#909399;margin-bottom:4px">参数 JSON:</div>
|
||||
<el-input v-model="createForm.args" type="textarea" :rows="3" readonly style="font-family:monospace;font-size:12px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="createForm.index" :min="0" controls-position="right" style="width:120px" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getProductPlanList, createProductPlan, getProductParameterList } from '@/api/admin/product'
|
||||
import { hasUnit, getArgKey, getBaseUnit, getParamUnits, getParamDefaultUnit, toBaseUnit, fromBaseUnit } from '@/utils/dynamicUnit'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
goodId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const selected = ref(null)
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', note: '', index: 0, args: '' })
|
||||
const createSpecList = ref([])
|
||||
const createSpecLoading = ref(false)
|
||||
const createSpecValues = reactive({})
|
||||
const createDisplayValues = reactive({})
|
||||
const createDisplayUnits = reactive({})
|
||||
|
||||
watch(showCreate, (v) => {
|
||||
if (v && props.goodId) loadCreateSpec()
|
||||
})
|
||||
|
||||
const loadCreateSpec = async () => {
|
||||
createSpecLoading.value = true
|
||||
try {
|
||||
const res = await getProductParameterList({ good_id: props.goodId })
|
||||
if (res?.data?.code === 200) {
|
||||
createSpecList.value = res.data.data || []
|
||||
for (const spec of createSpecList.value) {
|
||||
if (spec.type === 'number') {
|
||||
if (createSpecValues[spec.id] === undefined) createSpecValues[spec.id] = spec.min ?? 0
|
||||
if (hasUnit(spec)) {
|
||||
createDisplayUnits[spec.id] = getParamDefaultUnit(spec)
|
||||
createDisplayValues[spec.id] = fromBaseUnit(spec.min ?? 0, createDisplayUnits[spec.id], getArgKey(spec))
|
||||
} else {
|
||||
createDisplayValues[spec.id] = spec.min ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { createSpecList.value = [] } finally { createSpecLoading.value = false }
|
||||
}
|
||||
|
||||
const onCreateNumberChange = (spec) => {
|
||||
if (hasUnit(spec)) {
|
||||
const argKey = getArgKey(spec)
|
||||
const unit = createDisplayUnits[spec.id]
|
||||
createSpecValues[spec.id] = Math.round(toBaseUnit(createDisplayValues[spec.id] || 0, unit, argKey))
|
||||
} else {
|
||||
createSpecValues[spec.id] = createDisplayValues[spec.id]
|
||||
}
|
||||
buildCreateArgsJson()
|
||||
}
|
||||
|
||||
const onCreateUnitChange = (spec, newUnit) => {
|
||||
const argKey = getArgKey(spec)
|
||||
const oldUnit = createDisplayUnits[spec.id]
|
||||
const oldDisplay = createDisplayValues[spec.id] || 0
|
||||
const baseValue = oldUnit ? toBaseUnit(oldDisplay, oldUnit, argKey) : oldDisplay
|
||||
createDisplayUnits[spec.id] = newUnit
|
||||
createDisplayValues[spec.id] = fromBaseUnit(baseValue, newUnit, argKey)
|
||||
createSpecValues[spec.id] = Math.round(baseValue)
|
||||
buildCreateArgsJson()
|
||||
}
|
||||
|
||||
const buildCreateArgsJson = () => {
|
||||
const result = []
|
||||
for (const spec of createSpecList.value) {
|
||||
const val = createSpecValues[spec.id]
|
||||
if (val === undefined || val === null || val === '') continue
|
||||
if (spec.type === 'select') {
|
||||
const attr = spec.attrs?.find(a => a.id === val)
|
||||
if (attr) result.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value, number: 0, key: getArgKey(spec) || undefined })
|
||||
} else if (spec.type === 'number') {
|
||||
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: '', number: val, key: getArgKey(spec) || undefined })
|
||||
} else {
|
||||
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val), number: 0, key: getArgKey(spec) || undefined })
|
||||
}
|
||||
}
|
||||
createForm.args = result.length > 0 ? JSON.stringify(result) : ''
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, count: pageSize.value }
|
||||
if (props.goodId) params.good_id = props.goodId
|
||||
const res = await getProductPlanList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入套餐名称'); return }
|
||||
if (!props.goodId) { ElMessage.warning('请先选择商品'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('good_id', props.goodId)
|
||||
fd.append('name', createForm.name)
|
||||
if (createForm.note) fd.append('note', createForm.note)
|
||||
fd.append('index', createForm.index)
|
||||
if (createForm.args) fd.append('args', createForm.args)
|
||||
const res = await createProductPlan(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', note: '', index: 0, args: '' })
|
||||
for (const k in createSpecValues) delete createSpecValues[k]
|
||||
for (const k in createDisplayValues) delete createDisplayValues[k]
|
||||
for (const k in createDisplayUnits) delete createDisplayUnits[k]
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择商品组"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="group-selector">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索商品组名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 商品组列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="商品组名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="父级ID" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.parentId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.tag" size="small" type="info">{{ row.tag?.name || row.tag }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.disable ? 'danger' : 'success'" size="small">
|
||||
{{ row.disable ? '禁用' : '启用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="groupList.length === 0 && !loading" description="暂无商品组数据" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedGroup"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getProductGroupList } from '@/api/admin/product'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的商品组ID(用于回显)
|
||||
currentGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedGroup = ref(null)
|
||||
const keyword = ref('')
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
selectedGroup.value = null
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取商品组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
groupList.value = []
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
if (keyword.value.trim()) {
|
||||
params.keyword = keyword.value.trim()
|
||||
}
|
||||
const res = await getProductGroupList(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
const items = Array.isArray(inner) ? inner : (inner.data || inner.list || [])
|
||||
// 过滤掉已删除的
|
||||
groupList.value = items.filter(item => !item.delete)
|
||||
total.value = inner.all_count ?? inner.total ?? groupList.value.length
|
||||
|
||||
// 如果有当前选中的商品组ID,自动选中
|
||||
if (props.currentGroupId) {
|
||||
const current = groupList.value.find(
|
||||
g => g.id === Number(props.currentGroupId)
|
||||
)
|
||||
if (current) {
|
||||
selectedGroup.value = current
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(body?.message || '获取商品组列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品组列表失败:', error)
|
||||
ElMessage.error('获取商品组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择商品组
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedGroup.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedGroup.value && row.id === selectedGroup.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedGroup.value = null
|
||||
groupList.value = []
|
||||
keyword.value = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedGroup.value) {
|
||||
emit('confirm', selectedGroup.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个商品组')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-selector {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择商品"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="product-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择商品 -->
|
||||
<el-tab-pane label="选择商品" name="selectProduct">
|
||||
<div class="product-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="商品分组">
|
||||
<el-select
|
||||
v-model="searchParams.good_group_id"
|
||||
placeholder="全部分组"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in groupOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品标签">
|
||||
<el-select
|
||||
v-model="searchParams.tag"
|
||||
placeholder="全部标签"
|
||||
:clearable="!defaultTag"
|
||||
:disabled="!!defaultTag"
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tagOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="productList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="商品ID" width="100" align="center" />
|
||||
<el-table-column label="商品图片" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.image || '/logo.svg'"
|
||||
fit="cover"
|
||||
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="table" label="所属表" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="价格" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="price">¥{{ (row.price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.inventory > 0 ? 'success' : 'danger'" size="small">
|
||||
{{ row.inventory }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="productList.length === 0 && !loading" description="暂无商品数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedProduct"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getProductList, getProductGroupList, getProductTagList } from '@/api/admin/product'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的商品ID(用于回显)
|
||||
currentProductId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 默认标签过滤(设置后自动锁定该标签)
|
||||
defaultTag: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectProduct')
|
||||
const loading = ref(false)
|
||||
const productList = ref([])
|
||||
const groupOptions = ref([])
|
||||
const tagOptions = ref([])
|
||||
const total = ref(0)
|
||||
const selectedProduct = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
good_group_id: '',
|
||||
tag: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
activeTab.value = 'selectProduct'
|
||||
selectedProduct.value = null
|
||||
searchParams.page = 1
|
||||
if (props.defaultTag) {
|
||||
searchParams.tag = props.defaultTag
|
||||
}
|
||||
fetchGroupList()
|
||||
fetchTagList()
|
||||
fetchProductList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取商品分组列表
|
||||
const fetchGroupList = async () => {
|
||||
try {
|
||||
const res = await getProductGroupList({ page: 1, count: 10 })
|
||||
if (res.data.code === 200) {
|
||||
groupOptions.value = res.data.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分组列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品标签列表
|
||||
const fetchTagList = async () => {
|
||||
try {
|
||||
const res = await getProductTagList()
|
||||
if (res.data.code === 200) {
|
||||
tagOptions.value = res.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
const fetchProductList = async () => {
|
||||
loading.value = true
|
||||
productList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
if (searchParams.good_group_id) {
|
||||
params.good_group_id = searchParams.good_group_id
|
||||
}
|
||||
if (searchParams.tag) {
|
||||
params.tag = searchParams.tag
|
||||
}
|
||||
|
||||
const res = await getProductList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
const allData = res.data.data.data || []
|
||||
// 过滤掉已删除的数据(兼容 delete 字段不存在的情况)
|
||||
productList.value = allData.filter(item => item.delete !== true)
|
||||
total.value = res.data.data.all_count ?? allData.length
|
||||
|
||||
// cover 字段直接是图片 URL,无需再请求 file detail
|
||||
productList.value.forEach(item => {
|
||||
if (item.cover) item.image = item.cover
|
||||
})
|
||||
|
||||
// 如果有当前选中的商品ID,自动选中
|
||||
if (props.currentProductId) {
|
||||
const currentProduct = productList.value.find(
|
||||
product => product.id === props.currentProductId
|
||||
)
|
||||
if (currentProduct) {
|
||||
selectedProduct.value = currentProduct
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取商品列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
ElMessage.error('获取商品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectProduct') {
|
||||
fetchProductList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.good_group_id = ''
|
||||
searchParams.tag = props.defaultTag || ''
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
// 选择商品
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedProduct.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedProduct.value && row.id === selectedProduct.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedProduct.value = null
|
||||
productList.value = []
|
||||
searchParams.good_group_id = ''
|
||||
searchParams.tag = props.defaultTag || ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedProduct.value) {
|
||||
emit('confirm', selectedProduct.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个商品')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.product-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择安全组" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索安全组" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === 'in' ? 'primary' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : '出站' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="锁定" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="白名单" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建安全组</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getSecurityGroupList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { page.value = 1; keyword.value = ''; selectedItem.value = null; loadList() }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, page: page.value, page_size: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const res = await getSecurityGroupList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
list.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="adminGroup ? '选择管理员组' : '选择用户组'"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="user-group-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择用户组 -->
|
||||
<el-tab-pane :label="adminGroup ? '选择管理员组' : '选择用户组'" name="selectGroup">
|
||||
<div class="group-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
:placeholder="adminGroup ? '搜索管理员组名称' : '搜索用户组名称'"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 管理员组列表表格 -->
|
||||
<el-table
|
||||
v-if="adminGroup"
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="组名称" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="group-name">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="auth" label="权限标识" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.note || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 用户组列表表格 -->
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="groupList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="组ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.group_id || row.GroupId || row.id || row.Id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="组名称" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="升级金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.floor_price || row.FloorPrice" class="price-text">
|
||||
¥{{ row.floor_price || row.FloorPrice }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下一级组ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.higher_level_id || row.HigherLevelId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
|
||||
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成员数量" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small" effect="plain">
|
||||
{{ row.member_count || row.MemberCount || 0 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="groupList.length === 0 && !loading" :description="adminGroup ? '暂无管理员组数据' : '暂无用户组数据'" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedGroup"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getUserGroupList } from '@/api/admin/user'
|
||||
import { getAdminGroupList } from '@/api/admin/group'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的用户组ID(用于回显)
|
||||
currentGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 排除的用户组ID(避免选择自己作为下一级)
|
||||
excludeGroupId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 是否请求管理员组接口
|
||||
adminGroup: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectGroup')
|
||||
const loading = ref(false)
|
||||
const groupList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedGroup = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectGroup'
|
||||
selectedGroup.value = null
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取用户组列表
|
||||
const fetchGroupList = async () => {
|
||||
loading.value = true
|
||||
groupList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count
|
||||
}
|
||||
|
||||
const res = props.adminGroup ? await getAdminGroupList(params) : await getUserGroupList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
let responseData = res.data?.data || res.data
|
||||
|
||||
if (props.adminGroup) {
|
||||
groupList.value = responseData?.data || []
|
||||
total.value = responseData?.total || groupList.value.length
|
||||
} else if (Array.isArray(responseData)) {
|
||||
groupList.value = responseData
|
||||
total.value = responseData.length
|
||||
} else if (responseData.list) {
|
||||
groupList.value = responseData.list || []
|
||||
total.value = responseData.total || responseData.all_count || 0
|
||||
} else if (responseData.data && Array.isArray(responseData.data)) {
|
||||
groupList.value = responseData.data
|
||||
total.value = responseData.all_count || responseData.data.length
|
||||
} else {
|
||||
groupList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 过滤掉排除的用户组
|
||||
if (props.excludeGroupId) {
|
||||
groupList.value = groupList.value.filter(item => {
|
||||
const itemId = item.group_id || item.GroupId || item.id || item.Id
|
||||
return itemId !== props.excludeGroupId
|
||||
})
|
||||
}
|
||||
|
||||
// 关键词过滤
|
||||
if (searchParams.key) {
|
||||
const keyword = searchParams.key.toLowerCase()
|
||||
groupList.value = groupList.value.filter(item => {
|
||||
const name = (item.group_name || item.name || item.Name || '').toLowerCase()
|
||||
const id = String(item.group_id || item.GroupId || item.id || item.Id)
|
||||
return name.includes(keyword) || id.includes(keyword)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有当前选中的用户组ID,自动选中
|
||||
if (props.currentGroupId) {
|
||||
const currentGroup = groupList.value.find(item => {
|
||||
const itemId = item.group_id || item.GroupId || item.id || item.Id
|
||||
return itemId === props.currentGroupId
|
||||
})
|
||||
if (currentGroup) {
|
||||
selectedGroup.value = currentGroup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取用户组列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户组列表失败:', error)
|
||||
ElMessage.error('获取用户组列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectGroup') {
|
||||
fetchGroupList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchGroupList()
|
||||
}
|
||||
|
||||
// 选择用户组
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedGroup.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedGroup.value) {
|
||||
const selectedId = selectedGroup.value.group_id || selectedGroup.value.GroupId || selectedGroup.value.id || selectedGroup.value.Id
|
||||
const rowId = row.group_id || row.GroupId || row.id || row.Id
|
||||
if (rowId === selectedId) {
|
||||
return 'selected-row'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedGroup.value = null
|
||||
groupList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedGroup.value) {
|
||||
emit('confirm', selectedGroup.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个用户组')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-group-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.group-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择用户"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="user-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择用户 -->
|
||||
<el-tab-pane label="选择用户" name="selectUser">
|
||||
<div class="user-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索用户名或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户状态">
|
||||
<el-select
|
||||
v-model="searchParams.status"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="success" @click="switchToAdd" :icon="Plus">
|
||||
添加新用户
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" align="center" />
|
||||
<el-table-column prop="user_name" label="用户名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info-cell">
|
||||
<el-avatar :size="32" :src="row.cover">
|
||||
<el-icon><User /></el-icon>
|
||||
</el-avatar>
|
||||
<span class="user-name">{{ row.user_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" width="130" show-overflow-tooltip />
|
||||
<!-- <el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="created_at" label="注册时间" width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="userList.length === 0 && !loading" description="暂无用户数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 添加用户 -->
|
||||
<el-tab-pane label="添加用户" name="addUser">
|
||||
<div class="add-user-section">
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addFormRules"
|
||||
label-width="100px"
|
||||
class="add-user-form"
|
||||
>
|
||||
<el-form-item label="用户名" prop="user_name">
|
||||
<el-input
|
||||
v-model="addForm.user_name"
|
||||
placeholder="请输入用户名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="addForm.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
type="email"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="addForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="addForm.password"
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="addForm.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleAddUser" :loading="addLoading">
|
||||
<el-icon><Plus /></el-icon>
|
||||
立即创建
|
||||
</el-button>
|
||||
<el-button @click="resetAddForm">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置表单
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedUser"
|
||||
v-if="activeTab === 'selectUser'"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
|
||||
import { getUserList, createTask } from '@/api/admin/user'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的用户ID(用于回显)
|
||||
currentUserId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectUser')
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
const userList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedUser = ref(null)
|
||||
const addFormRef = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
status: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 添加用户表单
|
||||
const addForm = reactive({
|
||||
user_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 密码确认验证
|
||||
const validateConfirmPassword = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== addForm.password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户表单验证规则
|
||||
const addFormRules = {
|
||||
user_name: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '用户名长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{ validator: validateConfirmPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectUser'
|
||||
selectedUser.value = null
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUserList = async () => {
|
||||
loading.value = true
|
||||
userList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
key: searchParams.key || ''
|
||||
}
|
||||
|
||||
const res = await getUserList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
userList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的用户ID,自动选中
|
||||
if (props.currentUserId) {
|
||||
const currentUser = userList.value.find(
|
||||
user => user.user_id === props.currentUserId
|
||||
)
|
||||
if (currentUser) {
|
||||
selectedUser.value = currentUser
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectUser') {
|
||||
fetchUserList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.status = ''
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchUserList()
|
||||
}
|
||||
|
||||
// 切换到添加用户标签页
|
||||
const switchToAdd = () => {
|
||||
activeTab.value = 'addUser'
|
||||
}
|
||||
|
||||
// 选择用户
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedUser.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedUser.value && row.user_id === selectedUser.value.user_id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
addLoading.value = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('user_name', addForm.user_name)
|
||||
formData.append('email', addForm.email)
|
||||
if (addForm.phone) {
|
||||
formData.append('phone', addForm.phone)
|
||||
}
|
||||
formData.append('password', addForm.password)
|
||||
|
||||
const res = await createTask(formData)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('用户创建成功')
|
||||
|
||||
// 获取新创建的用户信息
|
||||
const newUser = res.data.data
|
||||
|
||||
// 自动选择新创建的用户
|
||||
if (newUser) {
|
||||
selectedUser.value = {
|
||||
user_id: newUser.user_id || newUser.id,
|
||||
user_name: newUser.user_name || addForm.user_name,
|
||||
email: newUser.email || addForm.email,
|
||||
phone: newUser.phone || addForm.phone,
|
||||
...newUser
|
||||
}
|
||||
|
||||
// 触发确认事件并关闭弹窗
|
||||
emit('confirm', selectedUser.value)
|
||||
handleClose()
|
||||
} else {
|
||||
// 如果没有返回用户信息,切换到选择标签页并刷新列表
|
||||
activeTab.value = 'selectUser'
|
||||
searchParams.page = 1
|
||||
await fetchUserList()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
resetAddForm()
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '用户创建失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('用户创建失败:', error)
|
||||
ElMessage.error('用户创建失败')
|
||||
} finally {
|
||||
addLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置添加表单
|
||||
const resetAddForm = () => {
|
||||
addForm.user_name = ''
|
||||
addForm.email = ''
|
||||
addForm.phone = ''
|
||||
addForm.password = ''
|
||||
addForm.confirmPassword = ''
|
||||
addFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedUser.value = null
|
||||
userList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.status = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
resetAddForm()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedUser.value) {
|
||||
emit('confirm', selectedUser.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个用户')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.user-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.user-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.add-user-section {
|
||||
padding: 30px 60px;
|
||||
}
|
||||
|
||||
.add-user-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-user-form :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-tag v-if="filterType" :type="filterType === 'bridge' ? 'success' : 'warning'" size="small" effect="dark">仅{{ filterType === 'bridge' ? '网桥' : 'NAT' }}</el-tag>
|
||||
<el-tag v-if="filterUnused" type="success" size="small" effect="dark">仅未占用</el-tag>
|
||||
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
|
||||
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="gateway" label="网关" min-width="120" />
|
||||
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button v-if="props.showCreateButton" type="success" @click="handleCreate">创建网络</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getUserVmNetworkList } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 },
|
||||
filterType: { type: String, default: '' },
|
||||
filterUnused: { type: Boolean, default: false },
|
||||
showCreateButton: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const ipVersionFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
ipVersionFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
|
||||
const res = await getUserVmNetworkList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
let all = inner.data || (Array.isArray(inner) ? inner : [])
|
||||
if (props.filterType) {
|
||||
all = all.filter(n => n.type === props.filterType)
|
||||
}
|
||||
if (props.filterUnused) {
|
||||
all = all.filter(n => !n.vm_id)
|
||||
}
|
||||
list.value = all
|
||||
total.value = inner.meta?.count ?? inner.total ?? all.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择安全组" width="640px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索安全组名称" clearable style="width:200px"
|
||||
@keyup.enter="loadList" @clear="loadList">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新增安全组</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="280" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === 'in' ? 'success' : 'warning'" size="small">
|
||||
{{ row.direction === 'in' ? '入站' : row.direction === 'out' ? '出站' : (row.direction || '-') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="白名单" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="共享" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.shared ? 'success' : 'info'" size="small">{{ row.shared ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无安全组" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增安全组弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新增安全组" width="440px" append-to-body destroy-on-close>
|
||||
<el-form :model="createForm" label-width="90px">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="createForm.name" placeholder="安全组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方向">
|
||||
<el-select v-model="createForm.direction" style="width:100%">
|
||||
<el-option label="入站 (in)" value="in" />
|
||||
<el-option label="出站 (out)" value="out" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="锁定">
|
||||
<el-switch v-model="createForm.lock" />
|
||||
</el-form-item>
|
||||
<el-form-item label="白名单">
|
||||
<el-switch v-model="createForm.drop_all" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserVmPostGroupUserList, createUserVmPostGroup } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', direction: 'in', lock: false, drop_all: false })
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { user_goods_id: props.userGoodsId, page: page.value, page_size: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const res = await getUserVmPostGroupUserList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.groups || d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const res = await createUserVmPostGroup({
|
||||
user_goods_id: props.userGoodsId,
|
||||
name: createForm.name,
|
||||
direction: createForm.direction,
|
||||
lock: createForm.lock,
|
||||
drop_all: createForm.drop_all
|
||||
})
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', direction: 'in', lock: false, drop_all: false })
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择数据卷进行挂载" width="680px" append-to-body @close="handleClose">
|
||||
<div class="selector-toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索数据卷名称" clearable style="width:200px"
|
||||
@keyup.enter="loadList" @clear="loadList">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建数据卷</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||
@current-change="row => selected = row" :height="280" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="80">
|
||||
<template #default="{ row }">{{ row.size }} GB</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂载" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_mount ? 'success' : 'info'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无数据卷" />
|
||||
<div class="selector-footer-bar">
|
||||
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||
layout="total,sizes,prev,pager,next" small background
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected || !!selected.is_mount" @click="handleConfirm">
|
||||
{{ selected?.is_mount ? '已挂载' : '确定挂载' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新建数据卷弹窗 -->
|
||||
<el-dialog v-model="showCreate" title="新建数据卷" width="440px" append-to-body destroy-on-close>
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="名称" required><el-input v-model="createForm.name" placeholder="数据卷名称" /></el-form-item>
|
||||
<el-form-item label="大小">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="createForm.size" :min="1" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="createForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标设备名"><el-input v-model="createForm.target_device" placeholder="不填自动生成" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserVmVolumeList, createUserVmVolume } from '@/api/admin/userVm'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
userGoodsId: { type: Number, default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const selected = ref(null)
|
||||
|
||||
const showCreate = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({ name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
|
||||
|
||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||
watch(visible, (v) => emit('update:modelValue', v))
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.userGoodsId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserVmVolumeList({ user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
}
|
||||
} catch { /* */ } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
|
||||
createLoading.value = true
|
||||
try {
|
||||
const sizeGb = createForm._sizeUnit === 'TB' ? createForm.size * 1024 : createForm.size
|
||||
const res = await createUserVmVolume({
|
||||
user_goods_id: props.userGoodsId,
|
||||
name: createForm.name,
|
||||
size: sizeGb,
|
||||
target_device: createForm.target_device
|
||||
})
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
showCreate.value = false
|
||||
Object.assign(createForm, { name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
|
||||
loadList()
|
||||
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||
}
|
||||
|
||||
const handleClose = () => { visible.value = false }
|
||||
const handleConfirm = () => {
|
||||
if (selected.value && !selected.value.is_mount) {
|
||||
emit('confirm', selected.value)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-select { width: 90px; flex-shrink: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择虚拟机" width="700px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="hostIdFilter" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="loadList">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="配置" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.vcpu }}核 / {{ formatMem(row.memory) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { getRemoteHostList, getVmList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 },
|
||||
currentId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedItem = ref(null)
|
||||
const hostIdFilter = ref('')
|
||||
const hostOptions = ref([])
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) { loadHostOptions(); if (props.hostId) { hostIdFilter.value = props.hostId; loadList() } }
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const loadHostOptions = async () => {
|
||||
try {
|
||||
const res = await getRemoteHostList({ service_id: props.serviceId, page: 1, page_size: 10 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
hostOptions.value = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
if (!hostIdFilter.value && hostOptions.value.length) hostIdFilter.value = hostOptions.value[0].id
|
||||
if (hostIdFilter.value) loadList()
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const loadList = async () => {
|
||||
if (!hostIdFilter.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVmList({ service_id: props.serviceId, host_id: hostIdFilter.value, page: 1, count: 10 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
list.value = inner.data || (Array.isArray(inner) ? inner : [])
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const formatMem = (kb) => {
|
||||
if (!kb) return '-'
|
||||
if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'
|
||||
if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'
|
||||
return kb + ' KB'
|
||||
}
|
||||
|
||||
const statusType = (s) => ({ running: 'success', ready: 'success', stopped: 'danger', error: 'danger', paused: 'warning' }[s] || 'info')
|
||||
const statusLabel = (s) => ({ running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中', stopped: '已停止', stop: '已停止', error: '错误', paused: '已暂停' }[s] || s || '-')
|
||||
|
||||
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
:deep(.current-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择数据卷" width="750px" append-to-body @close="handleClose">
|
||||
<div class="selector-container">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索数据卷" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="就绪" value="ready" />
|
||||
<el-option label="等待中" value="pending" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="90">
|
||||
<template #default="{ row }">{{ row.size ? row.size + ' GB' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂载" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_mount ? 'warning' : 'success'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||
@current-change="p => { page = p; loadList() }" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<el-button type="success" @click="handleCreate">创建数据卷</el-button>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getVolumeList } from '@/api/admin/kvmService'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const statusFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
statusFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
watch(visible, (val) => emit('update:modelValue', val))
|
||||
|
||||
const handleSearch = () => { page.value = 1; loadList() }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!props.serviceId || !props.hostId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, count: pageSize.value }
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (statusFilter.value) params.status = statusFilter.value
|
||||
const res = await getVolumeList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
list.value = inner.data || inner.volumes || (Array.isArray(inner) ? inner : [])
|
||||
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const statusType = (s) => ({ ready: 'success', pending: 'warning', error: 'danger' }[s] || 'info')
|
||||
const statusLabel = (s) => ({ ready: '就绪', pending: '等待中', creating: '创建中', error: '错误' }[s] || s || '-')
|
||||
|
||||
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||
const handleConfirm = () => {
|
||||
if (selectedItem.value) {
|
||||
emit('confirm', selectedItem.value)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
const handleClose = () => { selectedItem.value = null }
|
||||
const handleCreate = () => {
|
||||
visible.value = false
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-container { min-height: 200px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||
:deep(.el-table__body tr) { cursor: pointer; }
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择代金券"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="voucher-selector">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 选择代金券 -->
|
||||
<el-tab-pane label="选择代金券" name="selectVoucher">
|
||||
<div class="voucher-list-container">
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="searchParams" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchParams.key"
|
||||
placeholder="搜索代金券名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :icon="Search">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset" :icon="Refresh">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 代金券列表表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="voucherList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
:height="350"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="代金券ID" width="100" align="center" />
|
||||
<el-table-column prop="name" label="代金券名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="code" label="代金券码" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" effect="plain">{{ row.code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
|
||||
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面值" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.percentage > 0" class="voucher-value">{{ row.percentage }}%</span>
|
||||
<span v-else class="voucher-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
|
||||
<span v-else>无限制</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'expired': isExpired(row.endTime) }">
|
||||
{{ formatDate(row.endTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="voucherList.length === 0 && !loading" description="暂无代金券数据" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedVoucher"
|
||||
>
|
||||
确定选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { getDiscountCodeList } from '@/api/admin/discount'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前已选中的代金券ID(用于回显)
|
||||
currentVoucherId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('selectVoucher')
|
||||
const loading = ref(false)
|
||||
const voucherList = ref([])
|
||||
const total = ref(0)
|
||||
const selectedVoucher = ref(null)
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
key: '',
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 重置状态
|
||||
activeTab.value = 'selectVoucher'
|
||||
selectedVoucher.value = null
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 获取代金券列表
|
||||
const fetchVoucherList = async () => {
|
||||
loading.value = true
|
||||
voucherList.value = []
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: searchParams.page,
|
||||
count: searchParams.count,
|
||||
discount_type: 'coupon' // 代金券类型
|
||||
}
|
||||
if (searchParams.key) {
|
||||
params.key = searchParams.key
|
||||
}
|
||||
|
||||
const res = await getDiscountCodeList(params)
|
||||
|
||||
if (res.data.code === 200) {
|
||||
voucherList.value = res.data.data?.data || []
|
||||
total.value = res.data.data?.all_count || 0
|
||||
|
||||
// 如果有当前选中的代金券ID,自动选中
|
||||
if (props.currentVoucherId) {
|
||||
const currentVoucher = voucherList.value.find(
|
||||
voucher => voucher.id === props.currentVoucherId
|
||||
)
|
||||
if (currentVoucher) {
|
||||
selectedVoucher.value = currentVoucher
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.data.msg || '获取代金券列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代金券列表失败:', error)
|
||||
ElMessage.error('获取代金券列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.paneName === 'selectVoucher') {
|
||||
fetchVoucherList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
searchParams.count = size
|
||||
searchParams.page = 1
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.page = page
|
||||
fetchVoucherList()
|
||||
}
|
||||
|
||||
// 选择代金券
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedVoucher.value = row
|
||||
}
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (selectedVoucher.value && row.id === selectedVoucher.value.id) {
|
||||
return 'selected-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedVoucher.value = null
|
||||
voucherList.value = []
|
||||
searchParams.key = ''
|
||||
searchParams.page = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 判断是否过期
|
||||
const isExpired = (endTime) => {
|
||||
if (!endTime) return false
|
||||
return new Date(endTime) < new Date()
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedVoucher.value) {
|
||||
emit('confirm', selectedVoucher.value)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.warning('请选择一个代金券')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voucher-selector {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.voucher-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.voucher-value {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.expired {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.selected-row td) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.current-row > td) {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="selector-field-row">
|
||||
<el-input
|
||||
:model-value="displayText"
|
||||
readonly
|
||||
:placeholder="placeholder"
|
||||
style="flex:1"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="disabled"
|
||||
style="margin-left:8px"
|
||||
@click="$emit('select')"
|
||||
>{{ buttonText }}</el-button>
|
||||
<el-button
|
||||
v-if="clearable && modelValue"
|
||||
style="margin-left:4px"
|
||||
@click="$emit('update:modelValue', null); $emit('clear')"
|
||||
>清除</el-button>
|
||||
</div>
|
||||
<div v-if="hint" :style="{ fontSize: '12px', color: hintType === 'disabled' ? '#c0c4cc' : '#909399', marginTop: '4px' }">
|
||||
{{ hint }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Number, String, Object], default: null },
|
||||
displayText: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '请选择' },
|
||||
buttonText: { type: String, default: '选择' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: true },
|
||||
hint: { type: String, default: '' },
|
||||
hintType: { type: String, default: 'normal' }
|
||||
})
|
||||
|
||||
defineEmits(['select', 'clear', 'update:modelValue'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selector-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,36 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-layout" :class="{ 'sidebar-collapsed': isCollapsed, 'mobile-open': isMobileMenuOpen }">
|
||||
<!-- 移动端遮罩层 -->
|
||||
<div class="mobile-overlay" v-if="isMobileMenuOpen" @click="closeMobileMenu"></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" :class="{ 'collapsed': isCollapsed }">
|
||||
<div class="logo-container">
|
||||
<h1 class="title">零零七云计算后台控制面板</h1>
|
||||
<img src="@/assets/logo.png" alt="Logo" class="logo-img" v-show="!isCollapsed" />
|
||||
<img src="@/assets/logo.svg" alt="Logo" class="logo-img-mini" v-show="isCollapsed" />
|
||||
</div>
|
||||
<el-scrollbar>
|
||||
<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"
|
||||
:collapse="isCollapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
>
|
||||
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<!-- 收缩按钮 -->
|
||||
<div class="collapse-btn" @click="toggleCollapse">
|
||||
<el-icon :size="18">
|
||||
<Fold v-if="!isCollapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主区域 -->
|
||||
@@ -25,10 +38,14 @@
|
||||
<!-- 顶部导航 -->
|
||||
<div class="navbar">
|
||||
<div class="navbar-left">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<el-button type="text" class="mobile-menu-btn" @click="toggleMobileMenu">
|
||||
<el-icon :size="22"><Menu /></el-icon>
|
||||
</el-button>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-item hidden-mobile">
|
||||
<el-tooltip content="全屏" placement="bottom">
|
||||
<el-button type="text" class="header-btn" @click="toggleFullScreen">
|
||||
<el-icon :size="18"><full-screen /></el-icon>
|
||||
@@ -39,9 +56,9 @@
|
||||
<div class="navbar-item">
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
||||
<span class="username">{{ userStore.userInfo.user_name }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
<el-avatar :size="32" :src="userStore.getUserAvatar() || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'" />
|
||||
<span class="username hidden-mobile">{{ userStore.userInfo.user_name }}</span>
|
||||
<el-icon class="el-icon--right hidden-mobile"><arrow-down /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -81,7 +98,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import SidebarMenuItem from './SidebarMenuItem.vue'
|
||||
import Breadcrumb from './Breadcrumb.vue'
|
||||
@@ -92,7 +109,10 @@ import {
|
||||
ArrowDown,
|
||||
User,
|
||||
Key,
|
||||
SwitchButton
|
||||
SwitchButton,
|
||||
Fold,
|
||||
Expand,
|
||||
Menu
|
||||
} from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
@@ -105,11 +125,46 @@ const router = useRouter()
|
||||
// 侧边栏菜单数据
|
||||
const menus = ref(menuConfig)
|
||||
|
||||
// 侧边栏收缩状态
|
||||
const isCollapsed = ref(false)
|
||||
|
||||
// 移动端菜单状态
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
// 检测是否是移动端
|
||||
const isMobile = ref(false)
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
// 移动端默认收起侧边栏
|
||||
if (isMobile.value) {
|
||||
isCollapsed.value = false
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前激活的菜单项
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 切换侧边栏收缩
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
// 保存状态到localStorage
|
||||
localStorage.setItem('sidebarCollapsed', isCollapsed.value)
|
||||
}
|
||||
|
||||
// 切换移动端菜单
|
||||
const toggleMobileMenu = () => {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
// 关闭移动端菜单
|
||||
const closeMobileMenu = () => {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
@@ -129,9 +184,35 @@ const handleLogout = () => {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('tokenExpire')
|
||||
localStorage.removeItem('userInfo')
|
||||
userStore.clearUserInfo()
|
||||
router.push('/login')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 监听路由变化,移动端自动关闭菜单
|
||||
router.afterEach(() => {
|
||||
if (isMobile.value) {
|
||||
closeMobileMenu()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 恢复侧边栏状态
|
||||
const savedState = localStorage.getItem('sidebarCollapsed')
|
||||
if (savedState !== null) {
|
||||
isCollapsed.value = savedState === 'true'
|
||||
}
|
||||
|
||||
// 检测设备类型
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -141,44 +222,95 @@ const handleLogout = () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端遮罩层 */
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 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;
|
||||
z-index: 999;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
.logo-img {
|
||||
height: 50px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-img-mini {
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
width: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
.sidebar-scrollbar {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 60px);
|
||||
min-height: 100%;
|
||||
background-color: transparent !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 收缩按钮 */
|
||||
.collapse-btn {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
color: #2c3e50;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 移动端菜单按钮 */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
margin-right: 12px;
|
||||
padding: 8px;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
/* 主容器样式 */
|
||||
@@ -188,49 +320,56 @@ const handleLogout = () => {
|
||||
flex-direction: column;
|
||||
background-color: #f0f2f5;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
.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;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
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 +378,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 +425,353 @@ const handleLogout = () => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 移动端隐藏元素 */
|
||||
.hidden-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 移动端响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: -260px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 260px;
|
||||
left: -260px;
|
||||
}
|
||||
|
||||
.admin-layout.mobile-open .sidebar {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.admin-layout.mobile-open .sidebar.collapsed {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu) {
|
||||
border-radius: 0;
|
||||
border: 1px solid #e1e8ed;
|
||||
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;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 一级菜单标题(有子菜单) */
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 2px 8px;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
color: #34495e !important;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
/* 一级菜单项(无子菜单) */
|
||||
:deep(.sidebar-menu > .el-menu-item) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 2px 8px;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
color: #34495e !important;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-menu-item.is-active::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 24px;
|
||||
background-color: #2c3e50;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* 展开的一级菜单标题 */
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 600;
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
/* 二级菜单容器 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu) {
|
||||
background-color: transparent !important;
|
||||
padding: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
/* 二级菜单项 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 2px 8px 2px 16px;
|
||||
padding: 0 16px 0 28px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266 !important;
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #c0c4cc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 二级菜单中的子菜单标题(三级菜单父级) */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 2px 8px 2px 16px;
|
||||
padding: 0 16px 0 28px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266 !important;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #c0c4cc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover::before) {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title) {
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title::before) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 三级菜单容器 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu) {
|
||||
background-color: transparent !important;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 三级菜单项 */
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin: 2px 8px 2px 28px;
|
||||
padding: 0 16px 0 24px !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #909399 !important;
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
|
||||
content: '-';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #606266 !important;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
|
||||
background-color: rgba(44, 62, 80, 0.08) !important;
|
||||
color: #2c3e50 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
|
||||
content: '•';
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 子菜单箭头图标 */
|
||||
:deep(.el-sub-menu__icon-arrow) {
|
||||
color: #909399 !important;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu:hover > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
color: #7f8c8d !important;
|
||||
}
|
||||
|
||||
: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>
|
||||
@@ -1,22 +1,25 @@
|
||||
<template>
|
||||
<el-sub-menu v-if="hasChildren" :index="menu.path">
|
||||
<template #title>
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.title || menu.meta?.title }}</span>
|
||||
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
|
||||
</template>
|
||||
<sidebar-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.path"
|
||||
:menu="child"
|
||||
:menu="child"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="menu.path">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ menu.title || menu.meta?.title }}</template>
|
||||
<template #title>
|
||||
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
@@ -29,6 +32,10 @@ const props = defineProps({
|
||||
menu: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,65 +46,45 @@ 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 {
|
||||
/* 菜单图标样式 */
|
||||
.menu-icon {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
color: #7f8c8d;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 激活菜单项特效 */
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
background-color: #e6f7ff !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
/* 菜单标题 */
|
||||
.menu-title {
|
||||
font-size: inherit;
|
||||
letter-spacing: 0.2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
/* 图标交互状态 */
|
||||
.el-menu-item .menu-icon,
|
||||
:deep(.el-sub-menu__title .menu-icon) {
|
||||
color: #7f8c8d !important;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
.el-menu-item:hover .menu-icon,
|
||||
:deep(.el-sub-menu__title:hover .menu-icon) {
|
||||
color: #34495e !important;
|
||||
}
|
||||
|
||||
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
/* 激活菜单项图标 */
|
||||
.el-menu-item.is-active .menu-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;
|
||||
}
|
||||
|
||||
/* 修复箭头颜色 */
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
:deep(.el-menu--inline) {
|
||||
background-color: #fafafa;
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .menu-icon) {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
</style>
|
||||
+246
-140
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<div class="tags-view-wrapper">
|
||||
<div class="tags-view-container"
|
||||
@mouseenter="hovered = true" @mouseleave="hovered = false">
|
||||
<div class="tags-view-wrapper" ref="scrollWrapperRef"
|
||||
@wheel.prevent="handleWheel"
|
||||
@scroll="onScroll">
|
||||
<div class="tags-view-scroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
@@ -23,6 +26,10 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-track" :class="{ visible: hovered && hasOverflow }">
|
||||
<div class="scroll-thumb" :style="thumbStyle" @mousedown="onThumbDown"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ul v-show="visible" :style="{left: left+'px', top: top+'px'}" class="contextmenu">
|
||||
@@ -59,130 +66,79 @@ 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()
|
||||
|
||||
const visitedViews = computed(() => tagsViewStore.visitedViews)
|
||||
const affixTags = computed(() => tagsViewStore.affixTags)
|
||||
|
||||
// 固定标签
|
||||
const affixTags = ref([])
|
||||
// 访问过的标签
|
||||
const visitedViews = ref([])
|
||||
// 右键菜单
|
||||
const visible = ref(false)
|
||||
const top = ref(0)
|
||||
const left = ref(0)
|
||||
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)
|
||||
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
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
router.push(selectedTag.value)
|
||||
tagsViewStore.delOthersViews(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)
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到最后一个标签或首页
|
||||
const toLastView = (visitedViews, view) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
router.push(latestView)
|
||||
} else {
|
||||
// 如果没有标签,则跳转到首页
|
||||
if (view && view.name === 'Dashboard') {
|
||||
// 如果当前是首页,则刷新页面
|
||||
router.push('/redirect' + '/dashboard')
|
||||
} else {
|
||||
router.push('/')
|
||||
@@ -190,17 +146,14 @@ const toLastView = (visitedViews, view) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是当前激活的标签
|
||||
const isActive = (tag) => {
|
||||
return tag.path === route.path
|
||||
}
|
||||
|
||||
// 判断是否是固定标签
|
||||
const isAffix = (tag) => {
|
||||
return affixTags.value.some(t => t.path === tag.path)
|
||||
return tag.meta && tag.meta.affix
|
||||
}
|
||||
|
||||
// 打开右键菜单
|
||||
const openMenu = (e, tag) => {
|
||||
const menuMinWidth = 125
|
||||
const offsetLeft = e.clientX
|
||||
@@ -214,30 +167,112 @@ const openMenu = (e, tag) => {
|
||||
selectedTag.value = tag
|
||||
}
|
||||
|
||||
// 关闭右键菜单
|
||||
// ---- 滚动 & 滚动条 ----
|
||||
const scrollWrapperRef = ref(null)
|
||||
const hovered = ref(false)
|
||||
const hasOverflow = ref(false)
|
||||
const thumbStyle = ref({ width: '0px', left: '0px' })
|
||||
|
||||
const handleWheel = (e) => {
|
||||
if (scrollWrapperRef.value) {
|
||||
scrollWrapperRef.value.scrollLeft += e.deltaY || e.deltaX
|
||||
}
|
||||
}
|
||||
|
||||
const refreshState = () => {
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = el
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
hasOverflow.value = maxScroll > 1
|
||||
|
||||
if (!hasOverflow.value) {
|
||||
thumbStyle.value = { width: '0px', left: '0px' }
|
||||
return
|
||||
}
|
||||
|
||||
const trackWidth = clientWidth
|
||||
const thumbW = Math.max((clientWidth / scrollWidth) * trackWidth, 30)
|
||||
const scrollRatio = maxScroll > 0 ? scrollLeft / maxScroll : 0
|
||||
const thumbLeft = scrollRatio * (trackWidth - thumbW)
|
||||
thumbStyle.value = { width: thumbW + 'px', left: thumbLeft + 'px' }
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
refreshState()
|
||||
}
|
||||
|
||||
const scrollToActiveTag = () => {
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
const activeEl = el.querySelector('.active-tag')
|
||||
if (!activeEl) return
|
||||
const wrapperRect = el.getBoundingClientRect()
|
||||
const tagRect = activeEl.getBoundingClientRect()
|
||||
if (tagRect.left < wrapperRect.left + 28) {
|
||||
el.scrollLeft -= (wrapperRect.left + 28 - tagRect.left + 12)
|
||||
} else if (tagRect.right > wrapperRect.right - 28) {
|
||||
el.scrollLeft += (tagRect.right - wrapperRect.right + 28 + 12)
|
||||
}
|
||||
}
|
||||
|
||||
const onThumbDown = (e) => {
|
||||
e.preventDefault()
|
||||
const el = scrollWrapperRef.value
|
||||
if (!el) return
|
||||
const startX = e.clientX
|
||||
const startScroll = el.scrollLeft
|
||||
const maxScroll = el.scrollWidth - el.clientWidth
|
||||
const trackWidth = el.clientWidth
|
||||
const thumbW = Math.max((el.clientWidth / el.scrollWidth) * trackWidth, 30)
|
||||
const movable = trackWidth - thumbW
|
||||
|
||||
const onMove = (ev) => {
|
||||
const dx = ev.clientX - startX
|
||||
const scrollDelta = movable > 0 ? (dx / movable) * maxScroll : 0
|
||||
el.scrollLeft = Math.min(Math.max(startScroll + scrollDelta, 0), maxScroll)
|
||||
}
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove)
|
||||
document.removeEventListener('mouseup', onUp)
|
||||
}
|
||||
document.addEventListener('mousemove', onMove)
|
||||
document.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
watch(visitedViews, () => nextTick(() => { refreshState(); scrollToActiveTag() }), { deep: true })
|
||||
|
||||
const closeMenu = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 监听路由变化,添加标签
|
||||
watch(route, (newRoute) => {
|
||||
if (newRoute.name) {
|
||||
addVisitedView(newRoute)
|
||||
tagsViewStore.addVisitedView(newRoute)
|
||||
}
|
||||
nextTick(scrollToActiveTag)
|
||||
})
|
||||
|
||||
// 点击其他区域关闭右键菜单
|
||||
const handleClickOutside = () => {
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
refreshState()
|
||||
scrollToActiveTag()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
nextTick(() => { refreshState(); scrollToActiveTag() })
|
||||
window.addEventListener('resize', onResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener('resize', onResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -245,21 +280,24 @@ 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;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 标签滚动区域 */
|
||||
.tags-view-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.tags-view-wrapper::-webkit-scrollbar {
|
||||
@@ -267,44 +305,94 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.tags-view-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tag, .active-tag {
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
background-color: #f4f4f5;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 底部自定义滚动条(在容器上,不在滚动区域内) */
|
||||
.scroll-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.scroll-track.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.scroll-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: rgba(180,188,199,0.45);
|
||||
transition: background 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scroll-thumb:hover {
|
||||
background: rgba(180,188,199,0.65);
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.tag, .active-tag {
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
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;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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 +403,47 @@ 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 +451,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;
|
||||
}
|
||||
</style>
|
||||
|
||||
.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>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 环境配置文件
|
||||
* 所有硬编码的 URL / 域名 / 环境变量统一在此管理
|
||||
*/
|
||||
|
||||
// 当前环境
|
||||
const isDevelopment = import.meta.env.MODE === 'development'
|
||||
|
||||
// API 基础地址
|
||||
// 开发环境使用 vite 代理 (baseUrl 为空),生产环境使用实际地址
|
||||
const API_BASE_MAP = {
|
||||
development: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren', // 直接请求后端,不走 vite proxy
|
||||
production: import.meta.env.VITE_API_BASE_URL || 'https://cloudapi.007yjs.com',
|
||||
staging: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren'
|
||||
}
|
||||
|
||||
// 获取当前环境的 API 基础地址
|
||||
const currentEnv = import.meta.env.VITE_APP_ENV || import.meta.env.MODE || 'development'
|
||||
export const baseUrl = API_BASE_MAP[currentEnv] || API_BASE_MAP.development
|
||||
|
||||
// ACS 服务基础地址
|
||||
export const acsBaseUrl = baseUrl
|
||||
|
||||
// 网站标题
|
||||
export const siteTitle = '007UI管理系统'
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
export const requestTimeout = 50000
|
||||
export const acsRequestTimeout = 30000
|
||||
|
||||
// Token 存储键名
|
||||
export const TOKEN_KEY = 'token'
|
||||
export const TOKEN_EXPIRE_KEY = 'tokenExpire'
|
||||
export const USER_INFO_KEY = 'userInfo'
|
||||
|
||||
// 不需要 token 认证的 URL 前缀
|
||||
export const noAuthUrls = [
|
||||
'/v1/user/login',
|
||||
'/v1/user/check/get_code_img',
|
||||
'/v1/user/register',
|
||||
'/v1/user/refresh_token'
|
||||
]
|
||||
|
||||
export default {
|
||||
isDevelopment,
|
||||
baseUrl,
|
||||
acsBaseUrl,
|
||||
siteTitle,
|
||||
requestTimeout,
|
||||
acsRequestTimeout,
|
||||
TOKEN_KEY,
|
||||
TOKEN_EXPIRE_KEY,
|
||||
USER_INFO_KEY,
|
||||
noAuthUrls
|
||||
}
|
||||
+146
-18
@@ -5,10 +5,93 @@ export const menus = [
|
||||
icon: 'DataBoard'
|
||||
},
|
||||
{
|
||||
path : '/ticket',
|
||||
title: '工单处理',
|
||||
icon: 'DataBoard'
|
||||
path: '/ticket',
|
||||
title: '工单管理',
|
||||
icon: 'Tickets',
|
||||
children: [
|
||||
{
|
||||
path: '/ticket/list',
|
||||
title: '工单列表'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
title: '用户管理',
|
||||
icon: 'User',
|
||||
children: [
|
||||
{
|
||||
path: '/user/list',
|
||||
title: '用户列表'
|
||||
},
|
||||
{
|
||||
path: '/user/group',
|
||||
title: '用户组管理'
|
||||
},
|
||||
{
|
||||
path: '/user/admin-group',
|
||||
title: '管理员组管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/product',
|
||||
title: '商品管理',
|
||||
icon: 'Goods',
|
||||
children: [
|
||||
{ path: '/product/manage', title: '商品管理' }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user-goods',
|
||||
title: '用户商品管理',
|
||||
icon: 'ShoppingCart',
|
||||
children: [
|
||||
{ path: '/user-goods/list', title: '所有商品' },
|
||||
{ path: '/user-goods/vm-list', 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: '/activity/groupbuy',
|
||||
title: '拼团管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/acs',
|
||||
@@ -33,39 +116,84 @@ 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: '全局设置' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '虚拟化平台管理',
|
||||
icon: 'Platform',
|
||||
children: [
|
||||
{
|
||||
path: '/virtualization/kvm-service',
|
||||
title: '主控服务管理'
|
||||
},
|
||||
{
|
||||
path: '/virtualization/host-group-mapping',
|
||||
title: '宿主机组映射管理'
|
||||
},
|
||||
{
|
||||
path: '/virtualization/vnc-command',
|
||||
title: 'VNC指令管理'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
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-manage',
|
||||
title: '配置管理'
|
||||
},
|
||||
{
|
||||
path: '/system/menu',
|
||||
title: '菜单管理',
|
||||
children: [
|
||||
{ path: '/system/menu-manage', title: '菜单列表' },
|
||||
{ path: '/system/menu-permission', title: '菜单权限' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
+449
-34
@@ -39,9 +39,29 @@ const routes = [
|
||||
title: '工单管理',
|
||||
icon: 'Tickets'
|
||||
},
|
||||
component: () => import('../views/ticket/TicketChat.vue'),
|
||||
redirect: '/ticket/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'TicketList',
|
||||
component: () => import('../views/ticket/TicketList.vue'),
|
||||
meta: {
|
||||
title: '工单列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'TicketDetail',
|
||||
component: () => import('../views/ticket/TicketDetail.vue'),
|
||||
meta: {
|
||||
title: '工单详情',
|
||||
hidden: true,
|
||||
activeMenu: '/ticket/list'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// ACS管理路由
|
||||
{
|
||||
path: 'acs',
|
||||
@@ -130,7 +150,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 +172,195 @@ 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/manage',
|
||||
children: [
|
||||
{
|
||||
path: 'manage',
|
||||
name: 'ProductManage',
|
||||
component: () => import('../views/product/ProductGroup.vue'),
|
||||
meta: { title: '商品管理' }
|
||||
},
|
||||
{ path: 'list', redirect: '/product/manage' },
|
||||
{ path: 'group', redirect: '/product/manage' }
|
||||
]
|
||||
},
|
||||
// 用户商品管理路由
|
||||
{
|
||||
path: 'user-goods',
|
||||
name: 'UserGoods',
|
||||
meta: { title: '用户商品管理', icon: 'ShoppingCart' },
|
||||
redirect: '/user-goods/list',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'UserGoodsList',
|
||||
component: () => import('../views/product/UserGoodsList.vue'),
|
||||
meta: { title: '所有商品' }
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
name: 'UserGoodsDetail',
|
||||
component: () => import('../views/product/UserGoodsDetail.vue'),
|
||||
meta: { title: '用户商品详情', hidden: true, activeMenu: '/user-goods/list' }
|
||||
},
|
||||
{
|
||||
path: 'vm-list',
|
||||
name: 'UserVmList',
|
||||
component: () => import('../views/user-vm/UserVmList.vue'),
|
||||
meta: { title: '云服务器' }
|
||||
},
|
||||
{
|
||||
path: 'vm-detail',
|
||||
name: 'UserVmDetail',
|
||||
component: () => import('../views/user-vm/UserVmDetail.vue'),
|
||||
meta: { title: '用户虚拟机详情', hidden: true, activeMenu: '/user-goods/vm-list' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 订单管理路由
|
||||
{
|
||||
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: '/activity/groupbuy',
|
||||
name: 'GroupBuyManage',
|
||||
component: () => import('../views/activity/GroupBuyManage.vue'),
|
||||
meta: {
|
||||
title: '拼团管理'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
name: 'System',
|
||||
@@ -147,28 +368,222 @@ 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-manage',
|
||||
name: 'SettingManage',
|
||||
component: () => import('../views/system/SettingManage.vue'),
|
||||
meta: { title: '配置管理' }
|
||||
},
|
||||
{
|
||||
path: 'menu-manage',
|
||||
name: 'MenuManage',
|
||||
component: () => import('../views/system/MenuManage.vue'),
|
||||
meta: { title: '菜单管理' }
|
||||
},
|
||||
{
|
||||
path: 'menu-permission',
|
||||
name: 'MenuPermission',
|
||||
component: () => import('../views/system/MenuPermission.vue'),
|
||||
meta: { title: '菜单权限' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'virtualization',
|
||||
name: 'Virtualization',
|
||||
meta: {
|
||||
title: '虚拟化平台管理',
|
||||
icon: 'Platform'
|
||||
},
|
||||
redirect: '/virtualization/kvm-service',
|
||||
children: [
|
||||
{
|
||||
path: 'kvm-service',
|
||||
name: 'KvmService',
|
||||
component: () => import('../views/virtualization/KvmService.vue'),
|
||||
meta: {
|
||||
title: '主控服务管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'kvm-service-detail',
|
||||
name: 'KvmServiceDetail',
|
||||
component: () => import('../views/virtualization/KvmServiceDetail.vue'),
|
||||
meta: {
|
||||
title: '主控服务详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-group-mapping',
|
||||
name: 'HostGroupMapping',
|
||||
component: () => import('../views/virtualization/HostGroupMapping.vue'),
|
||||
meta: {
|
||||
title: '宿主机组映射管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-manage',
|
||||
name: 'HostManage',
|
||||
component: () => import('../views/virtualization/HostManage.vue'),
|
||||
meta: {
|
||||
title: '宿主机管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'image-manage',
|
||||
name: 'ImageManage',
|
||||
component: () => import('../views/virtualization/ImageManage.vue'),
|
||||
meta: {
|
||||
title: '镜像管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'network-manage',
|
||||
name: 'NetworkManage',
|
||||
component: () => import('../views/virtualization/NetworkManage.vue'),
|
||||
meta: {
|
||||
title: '网络管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'volume-manage',
|
||||
name: 'VolumeManage',
|
||||
component: () => import('../views/virtualization/VolumeManage.vue'),
|
||||
meta: {
|
||||
title: '数据卷管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vm-manage',
|
||||
name: 'VmManage',
|
||||
component: () => import('../views/virtualization/VmManage.vue'),
|
||||
meta: {
|
||||
title: '虚拟机管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'security-group',
|
||||
name: 'SecurityGroupManage',
|
||||
component: () => import('../views/virtualization/SecurityGroupManage.vue'),
|
||||
meta: {
|
||||
title: '安全组管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vnc-node',
|
||||
name: 'VncNodeManage',
|
||||
component: () => import('../views/virtualization/VncNodeManage.vue'),
|
||||
meta: {
|
||||
title: 'VNC节点管理',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vnc-command',
|
||||
name: 'VncCommandManage',
|
||||
component: () => import('../views/virtualization/VncCommandManage.vue'),
|
||||
meta: {
|
||||
title: 'VNC指令管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'host-detail',
|
||||
name: 'VirtHostDetail',
|
||||
component: () => import('../views/virtualization/HostDetail.vue'),
|
||||
meta: {
|
||||
title: '宿主机详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'image-detail',
|
||||
name: 'VirtImageDetail',
|
||||
component: () => import('../views/virtualization/ImageDetail.vue'),
|
||||
meta: {
|
||||
title: '镜像详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vm-detail',
|
||||
name: 'VirtVmDetail',
|
||||
component: () => import('../views/virtualization/VmDetail.vue'),
|
||||
meta: {
|
||||
title: '虚拟机详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'security-group-detail',
|
||||
name: 'VirtSecurityGroupDetail',
|
||||
component: () => import('../views/virtualization/SecurityGroupDetail.vue'),
|
||||
meta: {
|
||||
title: '安全组详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'volume-detail',
|
||||
name: 'VirtVolumeDetail',
|
||||
component: () => import('../views/virtualization/VolumeDetail.vue'),
|
||||
meta: {
|
||||
title: '数据卷详情',
|
||||
hidden: true,
|
||||
activeMenu: '/virtualization/kvm-service'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -269,21 +684,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 +732,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
|
||||
}
|
||||
})
|
||||
+19
-2
@@ -4,11 +4,28 @@ import {ref} from "vue";
|
||||
|
||||
export const useUserStore = defineStore('userStore',() => {
|
||||
|
||||
let userInfo = ref({})
|
||||
// 初始化时从localStorage读取用户信息
|
||||
const savedUserInfo = localStorage.getItem('userInfo')
|
||||
let userInfo = ref(savedUserInfo ? JSON.parse(savedUserInfo) : {})
|
||||
|
||||
function setUserInfo(u){
|
||||
userInfo.value = u
|
||||
// 同步保存到localStorage
|
||||
if (u && Object.keys(u).length > 0) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(u))
|
||||
}
|
||||
}
|
||||
|
||||
return {userInfo,setUserInfo}
|
||||
// 清除用户信息
|
||||
function clearUserInfo() {
|
||||
userInfo.value = {}
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
// 获取用户头像
|
||||
function getUserAvatar() {
|
||||
return userInfo.value?.cover || ''
|
||||
}
|
||||
|
||||
return {userInfo, setUserInfo, clearUserInfo, getUserAvatar}
|
||||
})
|
||||
+374
-1
@@ -114,11 +114,384 @@ body {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 响应式工具类 */
|
||||
/* 可点击元素统一手型光标 */
|
||||
.el-button,
|
||||
.el-button--link,
|
||||
.el-tag.is-closable .el-tag__close,
|
||||
.el-dropdown,
|
||||
.el-dropdown-menu__item,
|
||||
.el-switch,
|
||||
.el-checkbox,
|
||||
.el-radio,
|
||||
.el-select .el-input__wrapper,
|
||||
.el-table__body tr.el-table__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.back-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ==================== 全局弹窗卡片样式 ==================== */
|
||||
/* 自动为所有未手动分区的弹窗表单添加卡片背景 */
|
||||
.el-dialog:not(.tk-dialog):not(.token-dialog):not(.token-result-dialog) .el-dialog__body > .el-form {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 20px 20px 4px;
|
||||
border: 1px solid #f0f2f5;
|
||||
}
|
||||
/* 统一弹窗 footer 按钮对齐 */
|
||||
.el-dialog .el-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.tk-dialog .el-dialog__body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
.tk-dialog .el-dialog__body::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
.tk-dialog .el-form {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tk-section {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 20px 20px 4px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f0f2f5;
|
||||
}
|
||||
.tk-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 18px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid #409eff;
|
||||
line-height: 1;
|
||||
}
|
||||
.tk-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.tk-resource-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 24px;
|
||||
}
|
||||
.tk-resource-grid .el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tk-resource-grid .el-form-item .el-form-item__label {
|
||||
width: 80px !important;
|
||||
}
|
||||
.tk-resource-grid .el-form-item .el-form-item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.tk-resource-grid .el-input-number {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tk-unit-select {
|
||||
width: 68px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tk-res-unit {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tk-inline-unit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
.tk-inline-unit .el-input-number,
|
||||
.tk-inline-unit .el-input,
|
||||
.tk-inline-unit .el-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ==================== 全局页面布局组件 ==================== */
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.page-header .header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-header .header-info h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
.page-header .sub-info {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.page-header .header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 嵌入式工具栏 */
|
||||
.embedded-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 通用工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 筛选区域(卡片式) */
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* 绑定选择器行 */
|
||||
.bind-selector-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 详情操作按钮组 */
|
||||
.detail-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ==================== 全局表格增强 ==================== */
|
||||
.el-table {
|
||||
--el-table-header-bg-color: #fafafa;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
--el-table-border-color: #ebeef5;
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
font-weight: 600 !important;
|
||||
color: #1d2129 !important;
|
||||
font-size: 13px !important;
|
||||
border-bottom: 2px solid #e1e8ed !important;
|
||||
}
|
||||
.el-table td.el-table__cell {
|
||||
border-bottom: 1px solid #f0f2f5 !important;
|
||||
color: #34495e !important;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.el-table .el-table__empty-block {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.el-table .el-table__empty-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* 表格固定列阴影 */
|
||||
.el-table__fixed {
|
||||
box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.el-table__fixed-right {
|
||||
box-shadow: -4px 0 8px -4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ==================== 全局骨架屏样式 ==================== */
|
||||
@keyframes tk-skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.skeleton-container {
|
||||
padding: 20px;
|
||||
}
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 16px;
|
||||
}
|
||||
.skeleton-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.skeleton-cell {
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: tk-skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 全局过渡动画 ==================== */
|
||||
.el-table,
|
||||
.el-card,
|
||||
.el-tag,
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* ==================== 通用文本类 ==================== */
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.mono-text {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ==================== 视觉增强 ==================== */
|
||||
|
||||
/* 卡片式筛选区域 */
|
||||
.filter-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 通用结果/令牌展示 */
|
||||
.tk-result-wrapper { text-align: center; }
|
||||
.tk-result-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; text-align: left; }
|
||||
.tk-result-icon { font-size: 36px; color: #e6a23c; background: #fdf6ec; border-radius: 50%; padding: 10px; }
|
||||
.tk-result-name { font-size: 16px; font-weight: 600; color: #1d2129; }
|
||||
.tk-result-meta { font-size: 13px; color: #909399; margin-top: 2px; }
|
||||
.tk-token-block { background: #1d2129; border-radius: 8px; padding: 16px; margin-bottom: 16px; text-align: left; }
|
||||
.tk-token-label { font-size: 11px; color: #909399; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.tk-token-value { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; color: #67c23a; word-break: break-all; line-height: 1.6; user-select: all; }
|
||||
.tk-copy-btn { width: 100%; }
|
||||
|
||||
/* 表单提示 */
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 资源信息标签组 */
|
||||
.resource-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 响应式工具类 ==================== */
|
||||
|
||||
/* 表格横向滚动提示 */
|
||||
.el-table {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hidden-xs {
|
||||
display: none !important;
|
||||
}
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-header .header-right {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.filter-bar .el-input,
|
||||
.filter-bar .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
.pagination-wrapper .el-pagination {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 弹窗在移动端更宽 */
|
||||
.el-dialog {
|
||||
width: 92% !important;
|
||||
margin: 5vh auto !important;
|
||||
}
|
||||
/* 表格小屏字号调整 */
|
||||
.el-table td.el-table__cell {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
/* 表单小屏行距压缩 */
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* tk-resource-grid 在移动端变为单列 */
|
||||
.tk-resource-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕适配 */
|
||||
@media (max-width: 1200px) {
|
||||
.el-table .el-table__body-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 992px) {
|
||||
|
||||
@@ -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)=> {
|
||||
|
||||
+15
-12
@@ -1,17 +1,20 @@
|
||||
import {http2} from "@/utils/request.js";
|
||||
/**获取镜像列表 */
|
||||
export const getMirrorList = data => {
|
||||
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}`);
|
||||
if(typeof data == "string"){
|
||||
return http2.get("/acs/v1/image/list?server_id=" + data + "&count=9999999")
|
||||
}
|
||||
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"
|
||||
}
|
||||
@@ -19,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"
|
||||
}
|
||||
@@ -27,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"
|
||||
}
|
||||
@@ -35,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"
|
||||
}
|
||||
@@ -50,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"
|
||||
}
|
||||
@@ -64,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
|
||||
@@ -80,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,160 @@
|
||||
/**
|
||||
* Dynamic Unit System
|
||||
*
|
||||
* Handles dynamic unit conversion and display for product parameters.
|
||||
* Base units: storage=GB, bandwidth=Mbps, cpu=Core
|
||||
*/
|
||||
|
||||
const UNIT_CONVERSIONS = {
|
||||
cpu: { Core: 1 },
|
||||
bandwidth_up: { Mbps: 1, Gbps: 1000 },
|
||||
bandwidth_down: { Mbps: 1, Gbps: 1000 },
|
||||
storage: { GB: 1, TB: 1024 },
|
||||
ipv4: { '个': 1 },
|
||||
ipv6: { '个': 1 },
|
||||
custom: {}
|
||||
}
|
||||
|
||||
const BASE_UNITS = {
|
||||
cpu: 'Core',
|
||||
bandwidth_up: 'Mbps',
|
||||
bandwidth_down: 'Mbps',
|
||||
storage: 'GB',
|
||||
ipv4: '个',
|
||||
ipv6: '个',
|
||||
custom: ''
|
||||
}
|
||||
|
||||
const DEFAULT_DISPLAY_UNITS = {
|
||||
cpu: 'Core',
|
||||
bandwidth_up: 'Mbps',
|
||||
bandwidth_down: 'Mbps',
|
||||
storage: 'GB',
|
||||
ipv4: '个',
|
||||
ipv6: '个',
|
||||
custom: ''
|
||||
}
|
||||
|
||||
const ARG_KEY_OPTIONS = [
|
||||
{ label: 'CPU (cpu)', value: 'cpu' },
|
||||
{ label: 'IPv4', value: 'ipv4' },
|
||||
{ label: 'IPv6', value: 'ipv6' },
|
||||
{ label: '上行带宽 (bandwidth_up)', value: 'bandwidth_up' },
|
||||
{ label: '下行带宽 (bandwidth_down)', value: 'bandwidth_down' },
|
||||
{ label: '存储空间 (storage)', value: 'storage' },
|
||||
{ label: '自定义 (custom)', value: 'custom' }
|
||||
]
|
||||
|
||||
/**
|
||||
* Convert value between units
|
||||
* @param {number} value
|
||||
* @param {string} fromUnit
|
||||
* @param {string} toUnit
|
||||
* @param {string} argKey - e.g. 'storage', 'bandwidth_up'
|
||||
*/
|
||||
export function convertUnit(value, fromUnit, toUnit, argKey) {
|
||||
if (value === null || value === undefined || fromUnit === toUnit) return value
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
if (!conversions || !conversions[fromUnit] || !conversions[toUnit]) return value
|
||||
const baseValue = value * conversions[fromUnit]
|
||||
return baseValue / conversions[toUnit]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from display unit to base unit for storage/submission
|
||||
*/
|
||||
export function toBaseUnit(value, displayUnit, argKey) {
|
||||
const baseUnit = BASE_UNITS[argKey]
|
||||
if (!baseUnit || !displayUnit) return value
|
||||
return convertUnit(value, displayUnit, baseUnit, argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from base unit to display unit for showing in UI
|
||||
*/
|
||||
export function fromBaseUnit(value, displayUnit, argKey) {
|
||||
const baseUnit = BASE_UNITS[argKey]
|
||||
if (!baseUnit || !displayUnit) return value
|
||||
return convertUnit(value, baseUnit, displayUnit, argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base unit string for a given argKey
|
||||
*/
|
||||
export function getBaseUnit(argKey) {
|
||||
return BASE_UNITS[argKey] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default display unit for a given argKey
|
||||
*/
|
||||
export function getDefaultDisplayUnit(argKey) {
|
||||
return DEFAULT_DISPLAY_UNITS[argKey] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available units for a parameter type
|
||||
*/
|
||||
export function getAvailableUnits(argKey) {
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
return conversions ? Object.keys(conversions) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get argKey select options
|
||||
*/
|
||||
export function getArgKeyOptions() {
|
||||
return ARG_KEY_OPTIONS
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a parameter has dynamic unit enabled.
|
||||
* Returns true when arg_key maps to a known unit type with multiple selectable units.
|
||||
*/
|
||||
export function hasUnit(param) {
|
||||
if (!param) return false
|
||||
const argKey = param.argKey || param.arg_key || param.key || ''
|
||||
if (!argKey || !(argKey in UNIT_CONVERSIONS)) return false
|
||||
return Object.keys(UNIT_CONVERSIONS[argKey]).length > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argKey from a parameter object (handles camelCase, snake_case, and plain key)
|
||||
*/
|
||||
export function getArgKey(param) {
|
||||
if (!param) return ''
|
||||
return param.argKey || param.arg_key || param.key || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available units from a parameter object
|
||||
*/
|
||||
export function getParamUnits(param) {
|
||||
if (!hasUnit(param)) return []
|
||||
const argKey = getArgKey(param)
|
||||
const paramUnits = param.availableUnits || param.available_units
|
||||
if (paramUnits && paramUnits.length > 0) return paramUnits
|
||||
return getAvailableUnits(argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default unit from a parameter object
|
||||
*/
|
||||
export function getParamDefaultUnit(param) {
|
||||
if (!hasUnit(param)) return ''
|
||||
const argKey = getArgKey(param)
|
||||
return param.defaultUnit || param.default_unit || getDefaultDisplayUnit(argKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a unit is valid for a parameter type
|
||||
*/
|
||||
export function isValidUnit(unit, argKey) {
|
||||
const conversions = UNIT_CONVERSIONS[argKey]
|
||||
return conversions && Object.prototype.hasOwnProperty.call(conversions, unit)
|
||||
}
|
||||
|
||||
export function formatValueWithUnit(value, unit) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return unit ? `${value} ${unit}` : String(value)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
const ERROR_CODE_MAP = {
|
||||
// 主控服务
|
||||
kvm_service_list_error: '获取主控服务列表失败',
|
||||
kvm_service_detail_error: '获取主控服务详情失败',
|
||||
kvm_service_create_error: '创建主控服务失败',
|
||||
kvm_service_update_error: '修改主控服务失败',
|
||||
kvm_service_delete_error: '删除主控服务失败',
|
||||
|
||||
// 宿主机组(本地)
|
||||
kvm_host_group_list_error: '获取宿主机组列表失败',
|
||||
kvm_host_group_sync_error: '同步宿主机组失败',
|
||||
kvm_host_group_bind_error: '绑定宿主机组失败',
|
||||
kvm_host_group_update_error: '修改宿主机组失败',
|
||||
kvm_host_group_delete_error: '删除宿主机组失败',
|
||||
kvm_host_group_generate_error: '生成商品失败',
|
||||
kvm_host_group_optimal_error: '获取最优主机失败',
|
||||
|
||||
// 宿主机组(远程)
|
||||
kvm_remote_host_group_list_error: '获取远程宿主机组列表失败',
|
||||
kvm_remote_host_group_detail_error: '获取远程宿主机组详情失败',
|
||||
kvm_remote_host_group_tree_error: '获取远程宿主机组树失败',
|
||||
kvm_remote_host_group_create_error: '创建远程宿主机组失败',
|
||||
kvm_remote_host_group_update_error: '修改远程宿主机组失败',
|
||||
kvm_remote_host_group_delete_error: '删除远程宿主机组失败',
|
||||
|
||||
// 宿主机
|
||||
kvm_host_list_error: '获取宿主机列表失败',
|
||||
kvm_host_detail_error: '获取宿主机详情失败',
|
||||
kvm_host_metrics_error: '获取宿主机指标失败',
|
||||
kvm_host_add_error: '新增宿主机失败',
|
||||
kvm_host_update_error: '修改宿主机失败',
|
||||
kvm_host_delete_error: '删除宿主机失败',
|
||||
|
||||
// 镜像
|
||||
kvm_image_list_error: '获取镜像列表失败',
|
||||
kvm_image_detail_error: '获取镜像详情失败',
|
||||
kvm_image_host_status_error: '获取镜像宿主机状态失败',
|
||||
kvm_image_create_error: '创建镜像失败',
|
||||
kvm_image_update_error: '修改镜像失败',
|
||||
kvm_image_delete_error: '删除镜像失败',
|
||||
kvm_image_reload_error: '重新下载镜像失败',
|
||||
kvm_image_sync_error: '同步镜像到宿主机失败',
|
||||
kvm_image_reload_host_error: '重新下载镜像到宿主机失败',
|
||||
|
||||
// 网络
|
||||
kvm_network_list_error: '获取网络列表失败',
|
||||
kvm_network_detail_error: '获取网络详情失败',
|
||||
kvm_network_create_error: '创建网络失败',
|
||||
kvm_network_update_error: '修改网络失败',
|
||||
kvm_network_delete_error: '删除网络失败',
|
||||
|
||||
// 数据卷
|
||||
kvm_volume_list_error: '获取数据卷列表失败',
|
||||
kvm_volume_detail_error: '获取数据卷详情失败',
|
||||
kvm_volume_create_error: '创建数据卷失败',
|
||||
kvm_volume_resize_error: '调整数据卷大小失败',
|
||||
kvm_volume_mount_error: '挂载数据卷失败',
|
||||
kvm_volume_unmount_error: '卸载数据卷失败',
|
||||
kvm_volume_transfer_error: '迁移数据卷失败',
|
||||
kvm_volume_delete_error: '删除数据卷失败',
|
||||
|
||||
// 虚拟机
|
||||
kvm_vm_list_error: '获取虚拟机列表失败',
|
||||
kvm_vm_detail_error: '获取虚拟机详情失败',
|
||||
kvm_vm_status_error: '获取虚拟机状态失败',
|
||||
kvm_vm_metrics_error: '获取虚拟机指标失败',
|
||||
kvm_vm_create_error: '创建虚拟机失败',
|
||||
kvm_vm_update_error: '修改虚拟机失败',
|
||||
kvm_vm_rebuild_error: '重建虚拟机失败',
|
||||
kvm_vm_refactor_error: '重构虚拟机失败',
|
||||
kvm_vm_update_traffic_error: '修改虚拟机带宽失败',
|
||||
kvm_vm_start_error: '启动虚拟机失败',
|
||||
kvm_vm_stop_error: '停止虚拟机失败',
|
||||
kvm_vm_reboot_error: '重启虚拟机失败',
|
||||
kvm_vm_suspend_error: '暂停虚拟机失败',
|
||||
kvm_vm_resume_error: '恢复虚拟机失败',
|
||||
kvm_vm_rescue_error: '进入救援系统失败',
|
||||
kvm_vm_exit_rescue_error: '退出救援系统失败',
|
||||
kvm_vm_delete_error: '删除虚拟机失败',
|
||||
|
||||
// 安全组
|
||||
kvm_post_group_list_error: '获取安全组列表失败',
|
||||
kvm_post_group_detail_error: '获取安全组详情失败',
|
||||
kvm_post_group_create_error: '创建安全组失败',
|
||||
kvm_post_group_update_error: '修改安全组失败',
|
||||
kvm_post_group_sync_error: '同步安全组失败',
|
||||
kvm_post_group_bind_error: '绑定安全组失败',
|
||||
kvm_post_group_unbind_error: '解绑安全组失败',
|
||||
kvm_post_group_delete_error: '删除安全组失败',
|
||||
kvm_post_group_enable_whitelist_error: '开启安全组白名单失败',
|
||||
kvm_post_group_disable_whitelist_error: '关闭安全组白名单失败',
|
||||
kvm_post_group_create_rule_error: '新增安全组规则失败',
|
||||
kvm_post_group_update_rule_error: '修改安全组规则失败',
|
||||
kvm_post_group_delete_rule_error: '删除安全组规则失败',
|
||||
kvm_post_group_apply_error: '应用安全组失败',
|
||||
kvm_security_group_list_error: '获取安全组列表失败',
|
||||
kvm_security_group_detail_error: '获取安全组详情失败',
|
||||
kvm_security_group_create_error: '创建安全组失败',
|
||||
kvm_security_group_update_error: '修改安全组失败',
|
||||
kvm_security_group_delete_error: '删除安全组失败',
|
||||
|
||||
// VNC
|
||||
kvm_vnc_list_error: '获取VNC节点列表失败',
|
||||
kvm_vnc_add_error: '新增VNC节点失败',
|
||||
kvm_vnc_test_error: '测试VNC节点连接失败',
|
||||
kvm_vnc_update_error: '修改VNC节点失败',
|
||||
kvm_vnc_delete_error: '删除VNC节点失败',
|
||||
kvm_vnc_vm_vnc_error: '获取VNC连接信息失败',
|
||||
}
|
||||
|
||||
/**
|
||||
* 从嵌套的 RPC 错误字符串中提取有意义的中文描述
|
||||
*/
|
||||
function parseRpcError(err) {
|
||||
if (!err) return ''
|
||||
const descMatch = err.match(/desc\s*=\s*(.+)/)
|
||||
if (descMatch) {
|
||||
const descContent = descMatch[1]
|
||||
const jsonMatch = descContent.match(/body=(\{.+\})/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[1])
|
||||
if (parsed.message) return parsed.message
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const clean = descContent.trim()
|
||||
if (clean && !clean.startsWith('http')) return clean
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一提取 API 响应中的错误信息
|
||||
* @param {object} body - axios response.data (即 { code, message, error, data })
|
||||
* @param {string} fallback - 兜底文案
|
||||
* @returns {string} 中文错误描述
|
||||
*/
|
||||
export function extractApiError(body, fallback = '操作失败') {
|
||||
if (!body) return fallback
|
||||
|
||||
// 识别数据库唯一约束冲突
|
||||
if (body.error && body.error.includes('duplicate key value violates unique constraint')) {
|
||||
const nameMatch = body.error.match(/create \w+ \[(.+?)\] error/)
|
||||
const hint = nameMatch ? `「${nameMatch[1]}」已存在,请勿重复生成` : '数据已存在,请勿重复操作'
|
||||
return hint
|
||||
}
|
||||
|
||||
const rpcMsg = parseRpcError(body.error)
|
||||
if (rpcMsg) return rpcMsg
|
||||
|
||||
const mapped = ERROR_CODE_MAP[body.message]
|
||||
if (mapped) return mapped
|
||||
|
||||
if (body.message && !/^[a-z_]+$/.test(body.message)) return body.message
|
||||
|
||||
return fallback
|
||||
}
|
||||
@@ -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": "新加坡"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
+150
-27
@@ -1,26 +1,104 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
|
||||
// 基础URL
|
||||
const baseUrl = 'https://apiservertest.s1f.ren'
|
||||
// const baseUrl = 'https://cloudapi.007yjs.com'
|
||||
import {getRefreshToken,refreshAccessToken} from "@/api/login.js";
|
||||
import { baseUrl, acsBaseUrl, noAuthUrls as noAuthUrlList, requestTimeout, acsRequestTimeout, TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js'
|
||||
|
||||
// 检查URL是否需要认证
|
||||
const urlNeedAuth = (url) => {
|
||||
// 这里可以添加不需要认证的URL列表
|
||||
const noAuthUrls = ['/v1/user/login', '/v1/user/check/get_code_img', '/v1/user/register']
|
||||
return !noAuthUrls.some(noAuthUrl => url.includes(noAuthUrl))
|
||||
return !noAuthUrlList.some(noAuthUrl => url.includes(noAuthUrl))
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
const isTokenExpired = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
|
||||
if (!token) return true
|
||||
|
||||
// 这里可以添加token过期检查逻辑,如果有JWT可以解析它
|
||||
// 简单实现,仅检查token是否存在
|
||||
return false
|
||||
// 检查过期时间
|
||||
if (expire) {
|
||||
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
|
||||
const now = Date.now()
|
||||
return now >= expireTime
|
||||
}
|
||||
|
||||
// 没有过期时间时,默认认为Token已过期(因为无法验证有效性)
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查token是否即将过期(5分钟内)
|
||||
const isTokenExpiringSoon = () => {
|
||||
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
|
||||
if (!expire) return false
|
||||
|
||||
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
|
||||
const now = Date.now()
|
||||
const fiveMinutes = 5 * 60 * 1000 // 5分钟
|
||||
|
||||
// 如果已过期,返回false(由isTokenExpired处理)
|
||||
if (now >= expireTime) return false
|
||||
|
||||
// 如果在5分钟内过期,返回true
|
||||
return (expireTime - now) <= fiveMinutes
|
||||
}
|
||||
|
||||
// 正在刷新token的标志
|
||||
let isRefreshing = false
|
||||
// 等待刷新token的请求队列
|
||||
let refreshSubscribers = []
|
||||
|
||||
// 添加请求到队列
|
||||
const subscribeTokenRefresh = (callback) => {
|
||||
refreshSubscribers.push(callback)
|
||||
}
|
||||
|
||||
// 刷新token后执行队列中的请求
|
||||
const onTokenRefreshed = (newToken) => {
|
||||
refreshSubscribers.forEach(callback => callback(newToken))
|
||||
refreshSubscribers = []
|
||||
}
|
||||
|
||||
// 执行token刷新
|
||||
const doRefreshToken = async () => {
|
||||
try {
|
||||
const domain = window.location.hostname
|
||||
// 获取交换token
|
||||
const refreshTokenRes = await getRefreshToken(domain,{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (refreshTokenRes.data?.code === 200 && refreshTokenRes.data?.data?.refresh_token) {
|
||||
// 使用交换token获取新的access token
|
||||
const newTokenRes = await refreshAccessToken(refreshTokenRes.data.data.refresh_token)
|
||||
|
||||
if (newTokenRes.data?.code === 200 && newTokenRes.data?.data?.token) {
|
||||
const { token, expire } = newTokenRes.data.data
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
if (expire) {
|
||||
localStorage.setItem(TOKEN_EXPIRE_KEY, expire.toString())
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Token刷新失败:', error)
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
@@ -37,7 +115,7 @@ class Request {
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么
|
||||
// 例如:添加 token
|
||||
const token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
@@ -106,33 +184,78 @@ class Request {
|
||||
// 创建默认实例
|
||||
const request = new Request({
|
||||
baseURL: baseUrl,
|
||||
timeout: 50000,
|
||||
timeout: requestTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
export const mainUrl = baseUrl + "/acs"
|
||||
export const mainUrl = baseUrl + '/acs'
|
||||
export const baseURL = baseUrl
|
||||
|
||||
export const http2 = axios.create({
|
||||
baseURL: baseUrl,
|
||||
timeout: 30000,
|
||||
timeout: acsRequestTimeout,
|
||||
headers: {},
|
||||
});
|
||||
|
||||
http2.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage
|
||||
if(urlNeedAuth(config.url) && isTokenExpired()){
|
||||
if (token){
|
||||
localStorage.removeItem('token');
|
||||
ElMessage.warning('登陆过期,请重新登陆')
|
||||
http2.interceptors.request.use(async config => {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
|
||||
// 检查是否需要认证
|
||||
if (urlNeedAuth(config.url)) {
|
||||
// 检查token是否已过期
|
||||
if (isTokenExpired()) {
|
||||
if (token) {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
}
|
||||
router.push('/login')
|
||||
return Promise.reject(new Error('Token已过期'))
|
||||
}
|
||||
|
||||
// 检查token是否即将过期,进行无感刷新
|
||||
if (isTokenExpiringSoon() && !isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newToken = await doRefreshToken()
|
||||
if (newToken) {
|
||||
console.log('Token已无感刷新')
|
||||
onTokenRefreshed(newToken)
|
||||
config.headers.Authorization = `Bearer ${newToken}`
|
||||
} else {
|
||||
// 刷新失败,doRefreshToken已处理登出逻辑,直接拒绝请求
|
||||
return Promise.reject(new Error('Token刷新失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token刷新异常:', error)
|
||||
// 刷新异常,doRefreshToken已处理登出逻辑,直接拒绝请求
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
} else if (isRefreshing) {
|
||||
// 正在刷新,等待刷新完成
|
||||
return new Promise((resolve, reject) => {
|
||||
subscribeTokenRefresh((newToken) => {
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`
|
||||
// 重新发送原始请求
|
||||
resolve(config)
|
||||
} else {
|
||||
reject(new Error('Token刷新失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 正常情况,直接使用token
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
router.push('/login')
|
||||
return Promise.reject();
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
config.url = '/acs' + config.url
|
||||
// 不需要认证的请求,不添加token
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -146,7 +269,7 @@ http2.interceptors.response.use(
|
||||
}
|
||||
const { status } = error.response;
|
||||
if (status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
ElMessage.warning('登陆过期,请重新登陆')
|
||||
router.push('/login')
|
||||
return Promise.reject();
|
||||
|
||||
+206
-1
@@ -1,4 +1,209 @@
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 分转元显示(返回 ¥xx.xx 或 '-')
|
||||
*/
|
||||
export function formatPrice(fen, fallback = '-') {
|
||||
if (!fen && fen !== 0) return fallback
|
||||
return '¥' + (fen / 100).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 元转分(四舍五入取整)
|
||||
*/
|
||||
export function yuanToFen(yuan) {
|
||||
return Math.round((yuan || 0) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化到期时间(year < 2000 视为永久)
|
||||
*/
|
||||
export function formatExpireTime(t) {
|
||||
if (!t) return '-'
|
||||
const d = new Date(t)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
if (d.getFullYear() < 2000) return '永久'
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ISO 格式时间字符串转换为毫秒级时间戳(用于时间选择器)
|
||||
* @param {string|Date|number} time - 输入时间(支持 ISO 格式字符串如 '2023-11-08T01:10:00+08:00'、Date 对象、时间戳等)
|
||||
* @returns {number|null} 转换后的毫秒级时间戳(失败或无效时间返回 null)
|
||||
*/
|
||||
export function isoToMilliseconds(time) {
|
||||
// 处理空值
|
||||
if (!time || time === null || time === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 处理特殊的无效时间标识
|
||||
if (typeof time === 'string' && (time === '0001-01-01T00:00:00Z' || time === '0001-01-01T00:00:00+00:00')) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果已经是数字(时间戳),直接返回
|
||||
if (typeof time === 'number') {
|
||||
// 如果是秒级时间戳(小于 13 位),转换为毫秒
|
||||
if (time < 1000000000000) {
|
||||
return time * 1000
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
// 处理 Date 对象
|
||||
if (time instanceof Date) {
|
||||
const timestamp = time.getTime()
|
||||
return isNaN(timestamp) ? null : timestamp
|
||||
}
|
||||
|
||||
// 处理字符串格式
|
||||
if (typeof time === 'string') {
|
||||
try {
|
||||
const date = new Date(time)
|
||||
const timestamp = date.getTime()
|
||||
|
||||
// 检查是否为有效时间
|
||||
if (isNaN(timestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return timestamp
|
||||
} catch (error) {
|
||||
console.error('时间转换失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为 "YYYY-MM-DD HH:mm:ss" 格式(用于接口提交)
|
||||
* @param {string|Date|number} time
|
||||
* @returns {string} 格式化后的时间字符串,无效时返回 ''
|
||||
*/
|
||||
export function formatToApiTime(time) {
|
||||
if (!time) return ''
|
||||
const d = time instanceof Date ? time : new Date(time)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// ========== 虚拟机状态映射 ==========
|
||||
const VM_STATUS_MAP = {
|
||||
pending: { label: '等待中', type: 'info' },
|
||||
creating: { label: '创建中', type: 'warning' },
|
||||
ready: { label: '就绪', type: 'success' },
|
||||
running: { label: '运行中', type: 'success' },
|
||||
stopped: { label: '已停止', type: 'danger' },
|
||||
stop: { label: '已停止', type: 'danger' },
|
||||
shutoff: { label: '已关闭', type: 'danger' },
|
||||
error: { label: '错误', type: 'danger' },
|
||||
paused: { label: '已暂停', type: 'warning' },
|
||||
reboot: { label: '重启中', type: 'warning' },
|
||||
poweroff: { label: '已关机', type: 'info' },
|
||||
unknown: { label: '未知', type: 'info' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟机状态标签文字
|
||||
*/
|
||||
export function vmStatusLabel(status) {
|
||||
return VM_STATUS_MAP[status]?.label || status || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟机状态 Tag 类型
|
||||
*/
|
||||
export function vmStatusType(status) {
|
||||
return VM_STATUS_MAP[status]?.type || 'info'
|
||||
}
|
||||
|
||||
// ========== 磁盘状态映射 ==========
|
||||
const VOLUME_STATUS_MAP = {
|
||||
pending: { label: '等待中', type: 'info' },
|
||||
creating: { label: '创建中', type: 'warning' },
|
||||
ready: { label: '就绪', type: 'success' },
|
||||
in_use: { label: '使用中', type: 'success' },
|
||||
attaching: { label: '挂载中', type: 'warning' },
|
||||
detaching: { label: '卸载中', type: 'warning' },
|
||||
resizing: { label: '扩容中', type: 'warning' },
|
||||
deleting: { label: '删除中', type: 'danger' },
|
||||
error: { label: '错误', type: 'danger' },
|
||||
unknown: { label: '未知', type: 'info' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘状态标签文字
|
||||
*/
|
||||
export function volumeStatusLabel(status) {
|
||||
return VOLUME_STATUS_MAP[status]?.label || status || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘状态 Tag 类型
|
||||
*/
|
||||
export function volumeStatusType(status) {
|
||||
return VOLUME_STATUS_MAP[status]?.type || 'info'
|
||||
}
|
||||
|
||||
+11
-2
@@ -105,15 +105,24 @@ const forgetPassword = () => {
|
||||
const handleLogin = () => {
|
||||
loginFormRef.value?.validate(async valid =>{
|
||||
window.localStorage.removeItem('token')
|
||||
window.localStorage.removeItem('tokenExpire')
|
||||
window.localStorage.removeItem('userInfo')
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
let resp = await userLogin(loginForm.username, loginForm.password)
|
||||
console.log("login:",resp)
|
||||
loading.value = false
|
||||
if(resp.code === 200){
|
||||
|
||||
window.localStorage.setItem('token',resp.data.token)
|
||||
// 保存token和过期时间
|
||||
window.localStorage.setItem('token', resp.data.token)
|
||||
if (resp.data.expire) {
|
||||
window.localStorage.setItem('tokenExpire', resp.data.expire.toString())
|
||||
}
|
||||
|
||||
let userInfo = await getUserInfo()
|
||||
if(userInfo.data.is_admin){
|
||||
// 保存用户信息到localStorage
|
||||
window.localStorage.setItem('userInfo', JSON.stringify(userInfo.data))
|
||||
await router.push('/dashboard')
|
||||
} else {
|
||||
ElMessage.warning('你不是管理员,不能登陆到后台控制面板')
|
||||
|
||||
@@ -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" />
|
||||
@@ -674,7 +666,7 @@ const toLoad = async (data) => {
|
||||
})
|
||||
form.server_id = data
|
||||
nowserver_id.value = data
|
||||
let res = await getServerPlan(data)
|
||||
let res = await getServerPlan({server_id:data,count:10})
|
||||
planlist.value = res.data.data.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
@@ -756,7 +748,7 @@ const fetchCategoryList = async (serverId) => {
|
||||
// 编辑镜像
|
||||
const handleEdit = async (data) => {
|
||||
try {
|
||||
let res = await getServerPlan({server_id: data.server_id,count: 100})
|
||||
let res = await getServerPlan({server_id: data.server_id,count: 10})
|
||||
if (res.data && res.data.data) {
|
||||
planlist.value = res.data.data.map(item => {
|
||||
return {
|
||||
@@ -882,7 +874,7 @@ const getit = async () => {
|
||||
|
||||
// 选择图片
|
||||
const picPagin = reactive({
|
||||
count: 50,
|
||||
count: 10,
|
||||
page: 1,
|
||||
key: '',
|
||||
user_type: 1
|
||||
@@ -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>
|
||||
@@ -264,7 +262,7 @@ const categoryRules = {
|
||||
// 素材库相关
|
||||
const picSwitch = ref(false)
|
||||
const picPagin = reactive({
|
||||
count: 50,
|
||||
count: 10,
|
||||
page: 1,
|
||||
key: '',
|
||||
user_type: 1
|
||||
@@ -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: 10,
|
||||
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: 10,
|
||||
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>
|
||||
@@ -466,7 +467,7 @@
|
||||
<h3 class="tab-title">数据卷列表</h3>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showAddVolumeDialog = true"
|
||||
@click="handleAddVolume"
|
||||
:icon="Plus"
|
||||
:disabled="vmInfo.state != 2"
|
||||
>
|
||||
@@ -670,8 +671,11 @@
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||||
<el-form-item label="大小(GB)" prop="size">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
|
||||
<el-form-item label="大小" prop="size">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -692,8 +696,11 @@
|
||||
>
|
||||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||||
|
||||
<el-form-item label="大小(GB)" prop="size">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
|
||||
<el-form-item label="大小" prop="size">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -825,7 +832,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;
|
||||
};
|
||||
@@ -1066,6 +1073,7 @@ const showMigrateVolumeDialog = ref(false);
|
||||
const currentVolumeToEdit = ref(null);
|
||||
const volumeForm = reactive({
|
||||
size: 10,
|
||||
_sizeUnit: 'GB'
|
||||
});
|
||||
const volumeFormRef = ref(null);
|
||||
const volumeRules = {
|
||||
@@ -1379,12 +1387,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 +1567,13 @@ const refreshData = () => {
|
||||
loadAllData(instanceId, false);
|
||||
};
|
||||
|
||||
// 清除所有缓存
|
||||
const clearAllCache = () => {
|
||||
console.log('清除所有虚拟机缓存');
|
||||
dataCache.value.clear();
|
||||
ElMessage.success('已清除所有缓存');
|
||||
};
|
||||
|
||||
// 启动虚拟机
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
@@ -2358,6 +2378,7 @@ const handleAddVolume = () => {
|
||||
showAddVolumeDialog.value = true;
|
||||
// 重置表单
|
||||
volumeForm.size = 10;
|
||||
volumeForm._sizeUnit = 'GB';
|
||||
};
|
||||
|
||||
// 编辑数据卷
|
||||
@@ -2365,6 +2386,7 @@ const handleEditVolume = (volume) => {
|
||||
currentVolumeToEdit.value = volume;
|
||||
// 填充表单
|
||||
volumeForm.size = volume.size;
|
||||
volumeForm._sizeUnit = 'GB';
|
||||
showEditVolumeDialog.value = true;
|
||||
};
|
||||
|
||||
@@ -2391,9 +2413,10 @@ const submitAddVolume = async () => {
|
||||
if (valid) {
|
||||
addingVolume.value = true;
|
||||
try {
|
||||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||||
const res = await addVolume({
|
||||
instance_id: route.query.instance_id,
|
||||
size: String(volumeForm.size),
|
||||
size: String(sizeGb),
|
||||
user_id: user_id.value
|
||||
});
|
||||
console.log("添加数据卷112",res)
|
||||
@@ -2425,9 +2448,10 @@ const submitEditVolume = async () => {
|
||||
editingVolume.value = true;
|
||||
try {
|
||||
// 这里应该调用修改数据卷的API
|
||||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||||
const res = await updateVolume({
|
||||
volume_id: currentVolumeToEdit.value.id,
|
||||
size: volumeForm.size
|
||||
size: sizeGb
|
||||
});
|
||||
console.log("编辑数据卷数据:",res)
|
||||
|
||||
@@ -2757,4 +2781,7 @@ const fetchServersList = async () => {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-select { width: 90px; flex-shrink: 0; }
|
||||
</style>
|
||||
|
||||
@@ -223,9 +223,9 @@
|
||||
:value="mirror.id"
|
||||
>
|
||||
<span style="float: left">{{ mirror.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||||
{{ mirror.size }}MB
|
||||
</span>
|
||||
<!-- <span style="float: right; color: #8492a6; font-size: 13px">-->
|
||||
<!-- {{ mirror.size }}MB-->
|
||||
<!-- </span>-->
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -618,7 +618,7 @@ const fetchPlanList = async () => {
|
||||
try {
|
||||
const response = await getServerPlan({
|
||||
server_id: props.ID,
|
||||
count: 100
|
||||
count: 10
|
||||
});
|
||||
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
|
||||
@@ -315,7 +315,11 @@
|
||||
class="data-table"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="size" label="空间大小(MB)" width="140" />
|
||||
<el-table-column prop="size" label="空间大小(MB)" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.size != null && row.size !== '' ? `${row.size} MB` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mount_path" label="挂载路径" min-width="200" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
</el-table>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user