Compare commits
139 Commits
41295f27f0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e81d33285 | |||
| 38c63cc451 | |||
| 4180f73c53 | |||
| 3227a50f9a | |||
| 86794145f1 | |||
| 84769954c4 | |||
| a827fc5c41 | |||
| 0829dc9ce4 | |||
| d01c4e2e34 | |||
| f667fe420a | |||
| 09fb74cd0d | |||
| d2af66f8c8 | |||
| c18622226e | |||
| 928d14aada | |||
| 1b44186e44 | |||
| 765f925482 | |||
| 9974a82ac8 | |||
| 61d777af8e | |||
| a443e4f147 | |||
| a5f8a9ef13 | |||
| 564e6cc017 | |||
| 98678859cb | |||
| dc63020943 | |||
| 59c5d16082 | |||
| ea571563e0 | |||
| e38ea4cc32 | |||
| 0dcce0822d | |||
| 802eaa396b | |||
| 3d783cd224 | |||
| 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 | |||
| e19d15dbf2 | |||
| ac9837b5a1 |
@@ -17,7 +17,12 @@ store封装到src/store目录下。
|
|||||||
|
|
||||||
注册侧边栏在/config/menus.js文件中。
|
注册侧边栏在/config/menus.js文件中。
|
||||||
|
|
||||||
|
新添加要求:
|
||||||
|
在遇到用户id需要填写和修改的弹窗将其修改为可预览样式
|
||||||
|
关于填写表单为推荐人id的需要使用组件AvatarSelector展示,如果是文件id或者是封面id 的也需要预览展示需要向头像列表组件一样,可以弄个文件组件/api/v1/admin/file/list这个是文件列表接口
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1.只要涉及弹窗添加和修改xxxid类型的就需要生成一个弹窗组件并使用到页面中
|
||||||
|
|
||||||
## 1. 基础布局规范
|
## 1. 基础布局规范
|
||||||
```css
|
```css
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install --ignore-scripts
|
||||||
|
pnpm rebuild
|
||||||
|
|
||||||
- name: 替换域名
|
- name: 替换域名
|
||||||
run: |
|
run: |
|
||||||
@@ -28,19 +29,22 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
|
- name: Compress artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf dist.tar.gz -C ./dist .
|
||||||
|
|
||||||
- name: Save artifact
|
- name: Save artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: vue3-build
|
name: vue3-build
|
||||||
path: |
|
path: dist.tar.gz
|
||||||
./dist
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ninBo
|
||||||
steps:
|
steps:
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: vue3-build
|
name: vue3-build
|
||||||
|
|
||||||
@@ -49,11 +53,11 @@ jobs:
|
|||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.PUBLICT_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
echo "${{ secrets.PUBLICT_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H ${{ vars.WEB_SERVICE_SERVER_IP }} >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
run: |
|
run: |
|
||||||
ssh-keyscan -H ${{ vars.WEB_SERVICE_SERVER_IP_1 }} >> ~/.ssh/known_hosts
|
DEPLOY_DIR="/home/www/web-online/admin.007yjs.com/"
|
||||||
scp -o StrictHostKeyChecking=no -r ./* ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_SERVICE_SERVER_IP_1 }}:/home/www/admin.007yjs.com/
|
ssh ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_SERVICE_SERVER_IP }} "mkdir -p $DEPLOY_DIR"
|
||||||
ssh-keyscan -H ${{ vars.WEB_SERVICE_SERVER_IP_2 }} >> ~/.ssh/known_hosts
|
scp -o StrictHostKeyChecking=no dist.tar.gz ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_SERVICE_SERVER_IP }}:$DEPLOY_DIR
|
||||||
scp -o StrictHostKeyChecking=no -r ./* ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_SERVICE_SERVER_IP_2 }}:/home/www/admin.007yjs.com/
|
ssh ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_SERVICE_SERVER_IP }} "cd $DEPLOY_DIR && tar -xzf dist.tar.gz && rm -f dist.tar.gz"
|
||||||
|
|
||||||
|
|||||||
@@ -18,25 +18,29 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install --ignore-scripts
|
||||||
|
pnpm rebuild
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: |
|
run: |
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
|
- name: Compress artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf dist.tar.gz -C ./dist .
|
||||||
|
|
||||||
- name: Save artifact
|
- name: Save artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: vue3-build
|
name: vue3-build
|
||||||
path: |
|
path: dist.tar.gz
|
||||||
./dist
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ninBo
|
||||||
steps:
|
steps:
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: https://gitea.s1f.ren/actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: vue3-build
|
name: vue3-build
|
||||||
|
|
||||||
@@ -49,5 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
run: |
|
run: |
|
||||||
scp -o StrictHostKeyChecking=no -r ./* ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_TEST_SERVER_IP }}:/www/wwwroot/apiserver_admin.s1f.ren/
|
DEPLOY_DIR="/www/wwwroot/apiserver_admin.s1f.ren/"
|
||||||
|
ssh ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_TEST_SERVER_IP }} "mkdir -p $DEPLOY_DIR"
|
||||||
|
scp -o StrictHostKeyChecking=no dist.tar.gz ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_TEST_SERVER_IP }}:$DEPLOY_DIR
|
||||||
|
ssh ${{ vars.ROOT_USER_NAME }}@${{ vars.WEB_TEST_SERVER_IP }} "cd $DEPLOY_DIR && tar -xzf dist.tar.gz && rm -f dist.tar.gz"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# 管理员后台pc端
|
||||||
|
|
||||||
# 007UI 后台管理系统
|
# 007UI 后台管理系统
|
||||||
|
|
||||||
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="007UI - 高端蓝色扁平化后台管理系统模板" />
|
<meta name="description" content="007UI - 高端蓝色扁平化后台管理系统模板" />
|
||||||
<meta name="keywords" content="管理系统,后台,模板,Vue3,ElementPlus" />
|
<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()
|
const userStore = useUserStore()
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let resp = await getUserInfo()
|
let resp = await getUserInfo()
|
||||||
|
console.log("用户信息:",resp)
|
||||||
userStore.setUserInfo(resp.data)
|
userStore.setUserInfo(resp.data)
|
||||||
console.log(userStore.userInfo)
|
console.log(userStore.userInfo)
|
||||||
})
|
})
|
||||||
@@ -87,40 +88,421 @@ html, body {
|
|||||||
background: #a8a8a8;
|
background: #a8a8a8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Element Plus样式优化 */
|
/* Element Plus全局配色优化 */
|
||||||
|
|
||||||
|
/* 按钮扁平化 */
|
||||||
.el-button {
|
.el-button {
|
||||||
font-weight: 400;
|
border-radius: 0 !important;
|
||||||
border-radius: 4px;
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主按钮 - 深蓝灰色 */
|
||||||
|
.el-button--primary {
|
||||||
|
background-color: #2c3e50 !important;
|
||||||
|
border-color: #2c3e50 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:active {
|
||||||
|
background-color: #1a252f !important;
|
||||||
|
border-color: #1a252f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功按钮 - 绿色系 */
|
||||||
|
.el-button--success {
|
||||||
|
background-color: #27ae60 !important;
|
||||||
|
border-color: #27ae60 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success:hover {
|
||||||
|
background-color: #2ecc71 !important;
|
||||||
|
border-color: #2ecc71 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success:active {
|
||||||
|
background-color: #229954 !important;
|
||||||
|
border-color: #229954 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮 - 红色系 */
|
||||||
|
.el-button--danger {
|
||||||
|
background-color: #e74c3c !important;
|
||||||
|
border-color: #e74c3c !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger:hover {
|
||||||
|
background-color: #ec7063 !important;
|
||||||
|
border-color: #ec7063 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger:active {
|
||||||
|
background-color: #c0392b !important;
|
||||||
|
border-color: #c0392b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 默认按钮 */
|
||||||
|
.el-button--default {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-color: #d5d9e0 !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default:hover {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
border-color: #c0c4cc !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link按钮 */
|
||||||
|
.el-button.is-link {
|
||||||
|
color: #3498db !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.is-link:hover {
|
||||||
|
color: #2980b9 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link {
|
||||||
|
color: #3498db !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link:hover {
|
||||||
|
color: #2980b9 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框扁平化 */
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 1px #2c3e50 inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签扁平化 */
|
||||||
|
.el-tag {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功标签 */
|
||||||
|
.el-tag--success {
|
||||||
|
background-color: #d5f4e6 !important;
|
||||||
|
color: #27ae60 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险标签 */
|
||||||
|
.el-tag--danger {
|
||||||
|
background-color: #fadbd8 !important;
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息标签 */
|
||||||
|
.el-tag--info {
|
||||||
|
background-color: #ebf5fb !important;
|
||||||
|
color: #3498db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片扁平化 + 层次感 */
|
||||||
.el-card {
|
.el-card {
|
||||||
border-radius: 4px;
|
border-radius: 0 !important;
|
||||||
|
border: 1px solid #e1e8ed !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
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 {
|
.el-menu {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table {
|
/* 表单标签 */
|
||||||
border-radius: 4px;
|
.el-form-item__label {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog 扁平化样式 */
|
||||||
|
.el-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-overlay-dialog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15);
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__wrapper {
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px 24px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
font-weight: 500;
|
background-color: #fafbfc;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
top: 20px;
|
||||||
|
right: 24px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close:hover {
|
||||||
|
color: #2c3e50;
|
||||||
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
|
color: #34495e;
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__footer {
|
.el-dialog__footer {
|
||||||
padding: 10px 20px 20px;
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background-color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog 内表单组件样式 */
|
||||||
|
.el-dialog .el-input__wrapper {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-select .el-input__wrapper {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-textarea__inner {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-form-item__label {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
</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,818 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置主IP */
|
||||||
|
export const setNetworkPrimary = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/host_service/point/network/set_primary', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置虚拟机MAC地址 */
|
||||||
|
export const resetVmMac = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/host_service/point/vm/reset_mac', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 断开虚拟机外部网络 */
|
||||||
|
export const disconnectVmNetwork = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/host_service/point/vm/disconnect_network', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 恢复虚拟机外部网络 */
|
||||||
|
export const connectVmNetwork = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/host_service/point/vm/connect_network', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询虚拟机每小时流量 */
|
||||||
|
export const getVmTrafficHourly = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/host_service/point/vm/traffic_hourly', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取宿主机额度统计 */
|
||||||
|
export const getHostQuotaStats = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/host_service/point/host/quota_stats', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取宿主机 KSM 状态 */
|
||||||
|
export const getHostKsmStatus = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/host_service/point/host/ksm/status', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置宿主机 KSM */
|
||||||
|
export const configureHostKsm = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/host_service/point/host/ksm/configure', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ================================
|
||||||
|
* 主控服务接口 - 数据卷管理
|
||||||
|
* ================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 获取数据卷列表 */
|
||||||
|
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'
|
||||||
|
|
||||||
|
const formHeaders = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||||
|
|
||||||
|
// ========== 邮件主控服务 ==========
|
||||||
|
|
||||||
|
export const getMailServiceList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMailServiceDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMailService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMailService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMailService = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/mail_service/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 邮件额度商品 ==========
|
||||||
|
|
||||||
|
export const getMailGoodsList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/goods/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMailGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/goods/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMailGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/goods/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMailGoods = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/mail_service/goods/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
// ========== 通知渠道配置 ==========
|
||||||
|
|
||||||
|
/** 获取全部通知渠道配置列表(无分页) */
|
||||||
|
export const getNoticeChannelList = () => {
|
||||||
|
return http2.get('/api/v1/admin/notice_message/channel/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改通知渠道配置 */
|
||||||
|
export const updateNoticeChannel = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/notice_message/channel/update', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 通知模板管理 ==========
|
||||||
|
|
||||||
|
/** 获取全部通知模板列表(无分页) */
|
||||||
|
export const getNoticeTemplateList = () => {
|
||||||
|
return http2.get('/api/v1/admin/notice_message/template/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加通知模板 */
|
||||||
|
export const addNoticeTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/notice_message/template/add', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改通知模板 */
|
||||||
|
export const updateNoticeTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/notice_message/template/update', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除通知模板 */
|
||||||
|
export const deleteNoticeTemplate = (params) => {
|
||||||
|
return http2.delete('/api/v1/admin/notice_message/template/delete', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使用默认参数预览渲染模板 */
|
||||||
|
export const previewNoticeTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/notice_message/template/default_msg', 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,145 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
const formHeaders = { headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
|
|
||||||
|
// ========== 短信主控服务 ==========
|
||||||
|
|
||||||
|
export const getSmsServiceList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/create', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/update', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsService = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/delete', {
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDefaultSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/set_default', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信额度商品 ==========
|
||||||
|
|
||||||
|
export const getSmsGoodsList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/goods/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/goods/create', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/goods/update', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsGoods = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/goods/delete', {
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信签名管理 ==========
|
||||||
|
|
||||||
|
export const getSmsSignatureList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/signature/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSmsSignatureDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/signature/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsSignature = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/signature/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/submit', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approveSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/approve', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejectSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/reject', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信模板管理 ==========
|
||||||
|
|
||||||
|
export const getSmsTemplateList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSmsTemplateDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsTemplate = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/template/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/submit', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approveSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/approve', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejectSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/reject', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 推荐模板管理 ==========
|
||||||
|
|
||||||
|
export const getSmsRecommendedTemplateList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/recommended/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/recommended/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/recommended/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/template/recommended/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
@@ -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 = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/user/user/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**更新用户信息 */
|
||||||
|
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,123 @@
|
|||||||
|
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 setUserVmNetworkPrimary = (data) => http2.post(`${BASE}/network/set_primary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
|
export const resetUserVmMac = (params) => http2.post(`${BASE}/reset_mac`, null, { params })
|
||||||
|
export const disconnectUserVmNetwork = (data) => http2.post(`${BASE}/disconnect_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
|
export const connectUserVmNetwork = (data) => http2.post(`${BASE}/connect_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
|
|
||||||
|
// ========== 组网 ==========
|
||||||
|
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 getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { 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) {
|
export function deleteDomain(data) {
|
||||||
return request.post("/api/v1/admin/server/domain_withe/delete",{domain_id: id})
|
return request.post("/api/v1/admin/server/domain_withe/delete",data,{
|
||||||
|
|
||||||
|
headers:{
|
||||||
|
'Content-Type':'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除域名白名单
|
// 批量删除域名白名单
|
||||||
export async function batchDeleteDomain(ids) {
|
export async function batchDeleteDomain(ids) {
|
||||||
let promises = []
|
let promises = []
|
||||||
for (let id of ids) {
|
for (let id of ids) {
|
||||||
promises.push(deleteDomain(id))
|
promises.push(deleteDomain({domain_id:id}))
|
||||||
}
|
}
|
||||||
return await Promise.all(promises)
|
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 = () => {
|
export const getUserInfo = () => {
|
||||||
return request.get("/api/v1/users/info/info")
|
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 })
|
||||||
}
|
}
|
||||||
+93
-7
@@ -5,8 +5,15 @@ import request from "@/utils/request.js";
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function getTickerList(count, page, status) {
|
export function getTickerList(count, page, status, orderBy, order, userId, keyword, type) {
|
||||||
return request.get('/api/v1/admin/work_order/list', { count, page, status })
|
const params = { count, page }
|
||||||
|
if (status !== undefined && status !== '') params.status = status
|
||||||
|
if (orderBy) params.orderBy = orderBy
|
||||||
|
if (order) params.order = order
|
||||||
|
if (userId) params.user_id = userId
|
||||||
|
if (keyword) params.keyword = keyword
|
||||||
|
if (type) params.type = type
|
||||||
|
return request.get('/api/v1/admin/work_order/list', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待处理
|
// 待处理
|
||||||
@@ -29,19 +36,23 @@ export function getCompletedTicketList(count, page) {
|
|||||||
return getTickerList(count,page,3)
|
return getTickerList(count,page,3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取详情
|
// 获取工单详情
|
||||||
export function getTicketDetail(work_id) {
|
export function getTicketDetail(work_id) {
|
||||||
return request.get('/api/v1/admin/work_order/detail', { work_id })
|
return request.get('/api/v1/admin/work_order/detail', { work_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回复
|
// 回复工单
|
||||||
export function replyTicket(work_id, content, files) {
|
export function replyTicket(work_id, content, files) {
|
||||||
return request.post('/api/v1/admin/work_order/reply', { work_id, content, files })
|
return request.post('/api/v1/admin/work_order/reply', { work_id, content, files }, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭工单
|
// 关闭工单
|
||||||
export function closeTicket(work_id) {
|
export function closeTicket(work_id) {
|
||||||
return request.post('/api/v1/admin/work_order/close', { work_id })
|
return request.post('/api/v1/admin/work_order/close', { work_id }, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFile(file_id) {
|
export function getFile(file_id) {
|
||||||
@@ -67,4 +78,79 @@ export async function parseFilesToImages(files) {
|
|||||||
|
|
||||||
const fileIds = files.split(',')
|
const fileIds = files.split(',')
|
||||||
return await Promise.all(fileIds.map(async (id) => await getFileImage(id.trim())))
|
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: 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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取回复模板列表 */
|
||||||
|
export function getReplyTemplateList(params = {}) {
|
||||||
|
return request.get('/api/v1/admin/work_order/reply_template/list', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**创建回复模板 */
|
||||||
|
export function createReplyTemplate(data) {
|
||||||
|
return request.post('/api/v1/admin/work_order/reply_template/create', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**修改回复模板 */
|
||||||
|
export function updateReplyTemplate(data) {
|
||||||
|
return request.post('/api/v1/admin/work_order/reply_template/update', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**删除回复模板 */
|
||||||
|
export function deleteReplyTemplate(data) {
|
||||||
|
return request.delete('/api/v1/admin/work_order/reply_template/delete', {
|
||||||
|
data: 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,289 @@
|
|||||||
|
<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-select v-model="searchParams.is_admin" placeholder="全部身份" clearable style="width: 120px" @change="handleSearch">
|
||||||
|
<el-option label="管理员" :value="true" />
|
||||||
|
<el-option label="普通用户" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
<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: '',
|
||||||
|
is_admin: undefined,
|
||||||
|
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.is_admin = undefined
|
||||||
|
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: 520px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: linear-gradient(135deg, #f7f8fa 0%, #f0f2f5 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 14px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
margin-right: auto;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:hover > td) {
|
||||||
|
background-color: #f0f7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.current-row > td) {
|
||||||
|
background-color: var(--el-color-primary-light-9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.current-row td) {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-avatar) {
|
||||||
|
background: linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary));
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
<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>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-select v-model="sourceFilter" placeholder="上传来源" clearable size="default" style="width: 120px" @change="handleSourceChange">
|
||||||
|
<el-option label="管理员" :value="true" />
|
||||||
|
<el-option label="用户" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||||
|
上传新文件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</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)
|
||||||
|
const sourceFilter = ref(undefined)
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
visible.value = newVal
|
||||||
|
if (newVal) {
|
||||||
|
selectedId.value = props.currentCoverId
|
||||||
|
currentPage.value = 1
|
||||||
|
sourceFilter.value = undefined
|
||||||
|
fetchFileList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 visible 变化
|
||||||
|
watch(visible, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取文件列表
|
||||||
|
const fetchFileList = async () => {
|
||||||
|
if (!props.userId) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
fileList.value = [] // 清空列表
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = { page: currentPage.value, count: pageSize.value }
|
||||||
|
if (sourceFilter.value !== undefined && sourceFilter.value !== null && sourceFilter.value !== '') {
|
||||||
|
params.is_admin = sourceFilter.value
|
||||||
|
}
|
||||||
|
const res = await getFileList(params)
|
||||||
|
|
||||||
|
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 handleSourceChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
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
|
||||||
|
sourceFilter.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,699 @@
|
|||||||
|
<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;"
|
||||||
|
/>
|
||||||
|
<el-select v-model="sourceFilter" placeholder="上传来源" clearable style="width: 130px; margin-left: 12px;" @change="handleSourceChange">
|
||||||
|
<el-option label="管理员" :value="true" />
|
||||||
|
<el-option label="用户" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
</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 sourceFilter = ref(undefined)
|
||||||
|
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 = ''
|
||||||
|
sourceFilter.value = undefined
|
||||||
|
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 params = { page: currentPage.value, count: pageSize.value }
|
||||||
|
if (sourceFilter.value !== undefined && sourceFilter.value !== null && sourceFilter.value !== '') {
|
||||||
|
params.is_admin = sourceFilter.value
|
||||||
|
}
|
||||||
|
const res = await getFileList(params)
|
||||||
|
|
||||||
|
// 如果有更新的请求发起,丢弃当前结果
|
||||||
|
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 handleSourceChange = () => {
|
||||||
|
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 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 = ''
|
||||||
|
sourceFilter.value = undefined
|
||||||
|
// 清理待上传文件的预览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;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,273 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="选择用户" width="860px" append-to-body @close="handleClose" class="user-selector-dialog">
|
||||||
|
<div class="uls-root">
|
||||||
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="uls-tabs">
|
||||||
|
<!-- ====== 选择用户 ====== -->
|
||||||
|
<el-tab-pane name="selectUser">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-lbl">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/></svg>
|
||||||
|
选择用户
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="uls-search">
|
||||||
|
<el-input v-model="searchParams.key" placeholder="搜索用户名 / 邮箱 / ID" clearable @keyup.enter="handleSearch" class="uls-search-input" :prefix-icon="Search" />
|
||||||
|
<el-select v-model="searchParams.is_admin" placeholder="全部身份" clearable style="width: 120px" @change="handleSearch">
|
||||||
|
<el-option label="管理员" :value="true" />
|
||||||
|
<el-option label="普通用户" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="handleSearch" :icon="Search" circle />
|
||||||
|
<el-button @click="handleReset" :icon="Refresh" circle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已选提示 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div class="uls-selected-bar" v-if="selectedUser">
|
||||||
|
<div class="uls-sel-left">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#67c23a"/></svg>
|
||||||
|
<span>已选择:</span>
|
||||||
|
<el-avatar :size="24" :src="selectedUser.cover" class="sel-avatar">{{ selectedUser.user_name?.charAt(0)?.toUpperCase() || 'U' }}</el-avatar>
|
||||||
|
<b>{{ selectedUser.user_name }}</b>
|
||||||
|
<span class="sel-id">#{{ selectedUser.user_id }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" link type="danger" @click="selectedUser = null">取消选择</el-button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- 用户卡片列表 -->
|
||||||
|
<div class="uls-grid" v-loading="loading">
|
||||||
|
<div
|
||||||
|
v-for="user in userList" :key="user.user_id"
|
||||||
|
class="uls-card" :class="{ active: selectedUser?.user_id === user.user_id }"
|
||||||
|
@click="handleCurrentChange(user)"
|
||||||
|
>
|
||||||
|
<div class="uls-card-check" v-if="selectedUser?.user_id === user.user_id">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#fff"/></svg>
|
||||||
|
</div>
|
||||||
|
<el-avatar :size="42" :src="user.cover" class="uls-avatar">{{ user.user_name?.charAt(0)?.toUpperCase() || 'U' }}</el-avatar>
|
||||||
|
<div class="uls-card-body">
|
||||||
|
<div class="uls-card-name">
|
||||||
|
{{ user.user_name }}
|
||||||
|
<el-tag v-if="user.is_admin" type="warning" size="small" effect="dark" round class="admin-tag">管理员</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="uls-card-meta">
|
||||||
|
<span class="meta-id">#{{ user.user_id }}</span>
|
||||||
|
<span v-if="user.email" class="meta-email">{{ user.email }}</span>
|
||||||
|
<span v-if="user.phone" class="meta-phone">{{ user.phone }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-if="userList.length === 0 && !loading" description="暂无用户数据" :image-size="80" />
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="uls-pagination" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="searchParams.page"
|
||||||
|
v-model:page-size="searchParams.count"
|
||||||
|
:page-sizes="[12, 24, 48]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
background small
|
||||||
|
@size-change="s => { searchParams.count = s; searchParams.page = 1; fetchUserList() }"
|
||||||
|
@current-change="p => { searchParams.page = p; fetchUserList() }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- ====== 添加用户 ====== -->
|
||||||
|
<el-tab-pane name="addUser">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-lbl">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/></svg>
|
||||||
|
添加用户
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="uls-add-section">
|
||||||
|
<el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-width="90px" class="uls-add-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" :icon="Plus">立即创建</el-button>
|
||||||
|
<el-button @click="resetAddForm" :icon="Refresh">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="uls-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'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
currentUserId: { type: [String, Number], default: '' }
|
||||||
|
})
|
||||||
|
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: '', is_admin: undefined, page: 1, count: 12 })
|
||||||
|
|
||||||
|
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' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (v) => {
|
||||||
|
visible.value = v
|
||||||
|
if (v) { activeTab.value = 'selectUser'; selectedUser.value = null; searchParams.page = 1; fetchUserList() }
|
||||||
|
})
|
||||||
|
watch(visible, (v) => emit('update:modelValue', v))
|
||||||
|
|
||||||
|
const fetchUserList = async () => {
|
||||||
|
loading.value = true; userList.value = []
|
||||||
|
try {
|
||||||
|
const params = { page: searchParams.page, count: searchParams.count, key: searchParams.key || '' }
|
||||||
|
if (searchParams.is_admin !== undefined && searchParams.is_admin !== null && searchParams.is_admin !== '') params.is_admin = searchParams.is_admin
|
||||||
|
const res = await getUserList(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
userList.value = res.data.data?.data || []
|
||||||
|
total.value = res.data.data?.all_count || 0
|
||||||
|
if (props.currentUserId) { const u = userList.value.find(u => u.user_id === props.currentUserId); if (u) selectedUser.value = u }
|
||||||
|
}
|
||||||
|
} catch { 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.is_admin = undefined; searchParams.page = 1; fetchUserList() }
|
||||||
|
const handleCurrentChange = (row) => { selectedUser.value = (selectedUser.value?.user_id === row.user_id) ? null : row }
|
||||||
|
const switchToAdd = () => { activeTab.value = 'addUser' }
|
||||||
|
|
||||||
|
const handleAddUser = async () => {
|
||||||
|
if (!addFormRef.value) return
|
||||||
|
await addFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
addLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('user_name', addForm.user_name); fd.append('email', addForm.email)
|
||||||
|
if (addForm.phone) fd.append('phone', addForm.phone)
|
||||||
|
fd.append('password', addForm.password)
|
||||||
|
const res = await createTask(fd)
|
||||||
|
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 { ElMessage.error('创建失败') }
|
||||||
|
finally { addLoading.value = false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAddForm = () => { Object.assign(addForm, { user_name: '', email: '', phone: '', password: '', confirmPassword: '' }); addFormRef.value?.resetFields() }
|
||||||
|
const handleClose = () => { visible.value = false; selectedUser.value = null; userList.value = []; searchParams.key = ''; searchParams.is_admin = undefined; searchParams.page = 1; total.value = 0; resetAddForm() }
|
||||||
|
const handleConfirm = () => { if (selectedUser.value) { emit('confirm', selectedUser.value); handleClose() } else ElMessage.warning('请选择一个用户') }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.uls-root { min-height: 460px; }
|
||||||
|
|
||||||
|
/* 标签页 */
|
||||||
|
.tab-lbl { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
:deep(.el-tabs__item) { font-size: 15px; padding: 0 20px; }
|
||||||
|
:deep(.el-tabs__item.is-active) { font-weight: 600; }
|
||||||
|
|
||||||
|
/* 搜索栏 */
|
||||||
|
.uls-search { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
|
||||||
|
.uls-search-input { flex: 1; max-width: 320px; }
|
||||||
|
|
||||||
|
/* 已选提示条 */
|
||||||
|
.uls-selected-bar { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; margin-bottom: 12px; background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%); border: 1px solid #c2e7b0; border-radius: 8px; }
|
||||||
|
.uls-sel-left { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #1d2129; }
|
||||||
|
.uls-sel-left b { font-weight: 600; }
|
||||||
|
.sel-id { color: #86909c; font-size: 12px; }
|
||||||
|
.sel-avatar { flex-shrink: 0; font-size: 11px; background: linear-gradient(135deg, #67c23a, #85ce61); color: #fff; }
|
||||||
|
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: all .25s ease; }
|
||||||
|
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(-6px); }
|
||||||
|
|
||||||
|
/* 卡片网格 */
|
||||||
|
.uls-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; max-height: 340px; overflow-y: auto; padding: 2px; }
|
||||||
|
|
||||||
|
.uls-card { position: relative; display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 2px solid #ebeef5; border-radius: 10px; cursor: pointer; transition: all .2s ease; background: #fff; }
|
||||||
|
.uls-card:hover { border-color: #b3d8ff; background: #f5faff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(64,158,255,.1); }
|
||||||
|
.uls-card.active { border-color: #409eff; background: #ecf5ff; }
|
||||||
|
|
||||||
|
.uls-card-check { position: absolute; top: -1px; right: -1px; width: 24px; height: 24px; background: #409eff; border-radius: 0 8px 0 8px; display: flex; align-items: center; justify-content: center; }
|
||||||
|
|
||||||
|
.uls-avatar { flex-shrink: 0; font-size: 16px; font-weight: 700; background: linear-gradient(135deg, #c6e2ff, #409eff); color: #fff; }
|
||||||
|
.uls-card.active .uls-avatar { background: linear-gradient(135deg, #409eff, #337ecc); }
|
||||||
|
|
||||||
|
.uls-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||||
|
.uls-card-name { font-size: 14px; font-weight: 600; color: #1d2129; display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.admin-tag { transform: scale(.85); transform-origin: left center; }
|
||||||
|
|
||||||
|
.uls-card-meta { display: flex; flex-wrap: wrap; gap: 4px 8px; font-size: 11px; color: #a8abb2; line-height: 1.3; }
|
||||||
|
.meta-id { color: #86909c; font-weight: 500; }
|
||||||
|
.meta-email, .meta-phone { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px; }
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.uls-pagination { margin-top: 14px; display: flex; justify-content: flex-end; }
|
||||||
|
|
||||||
|
/* 添加用户 */
|
||||||
|
.uls-add-section { padding: 24px 40px; }
|
||||||
|
.uls-add-form { max-width: 460px; margin: 0 auto; }
|
||||||
|
|
||||||
|
/* 底部 */
|
||||||
|
.uls-footer { display: flex; justify-content: flex-end; gap: 10px; }
|
||||||
|
|
||||||
|
/* 滚动条美化 */
|
||||||
|
.uls-grid::-webkit-scrollbar { width: 5px; }
|
||||||
|
.uls-grid::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 4px; }
|
||||||
|
.uls-grid::-webkit-scrollbar-thumb:hover { background: #c0c4cc; }
|
||||||
|
</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,149 @@
|
|||||||
|
<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
|
||||||
|
})
|
||||||
|
const code = res?.data?.code
|
||||||
|
if (code === 200 || code === 201 || (code >= 200 && code < 300)) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
showCreate.value = false
|
||||||
|
Object.assign(createForm, { name: '', direction: 'in', lock: false, drop_all: false })
|
||||||
|
await loadList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.data?.message || res?.data?.error || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.response?.data?.message || e?.response?.data?.error || e?.message || '创建失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} 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>
|
<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">
|
<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>
|
</div>
|
||||||
<el-scrollbar>
|
<el-scrollbar class="sidebar-scrollbar">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
class="sidebar-menu"
|
class="sidebar-menu"
|
||||||
background-color="#ffffff"
|
background-color="transparent"
|
||||||
text-color="#333333"
|
text-color="#34495e"
|
||||||
active-text-color="#1890ff"
|
active-text-color="#2c3e50"
|
||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
|
:collapse="isCollapsed"
|
||||||
|
:collapse-transition="false"
|
||||||
router
|
router
|
||||||
>
|
>
|
||||||
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
|
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
<!-- 收缩按钮 -->
|
||||||
|
<div class="collapse-btn" @click="toggleCollapse">
|
||||||
|
<el-icon :size="18">
|
||||||
|
<Fold v-if="!isCollapsed" />
|
||||||
|
<Expand v-else />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主区域 -->
|
<!-- 主区域 -->
|
||||||
@@ -25,23 +38,23 @@
|
|||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="navbar-left">
|
<div class="navbar-left">
|
||||||
|
<!-- 移动端菜单按钮 -->
|
||||||
|
<el-button type="text" class="mobile-menu-btn" @click="toggleMobileMenu">
|
||||||
|
<el-icon :size="22"><Menu /></el-icon>
|
||||||
|
</el-button>
|
||||||
<breadcrumb />
|
<breadcrumb />
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-right">
|
<div class="navbar-right">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<el-tooltip content="全屏" placement="bottom">
|
<GlobalSearch />
|
||||||
<el-button type="text" class="header-btn" @click="toggleFullScreen">
|
|
||||||
<el-icon :size="18"><full-screen /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
<el-avatar :size="32" :src="userStore.getUserAvatar() || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'" />
|
||||||
<span class="username">{{ userStore.userInfo.user_name }}</span>
|
<span class="username hidden-mobile">{{ userStore.userInfo.user_name }}</span>
|
||||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
<el-icon class="el-icon--right hidden-mobile"><arrow-down /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -81,18 +94,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import SidebarMenuItem from './SidebarMenuItem.vue'
|
import SidebarMenuItem from './SidebarMenuItem.vue'
|
||||||
import Breadcrumb from './Breadcrumb.vue'
|
import Breadcrumb from './Breadcrumb.vue'
|
||||||
import TagsView from './TagsView.vue'
|
import TagsView from './TagsView.vue'
|
||||||
|
import GlobalSearch from './GlobalSearch.vue'
|
||||||
import { menus as menuConfig } from '@/config/menus'
|
import { menus as menuConfig } from '@/config/menus'
|
||||||
import {
|
import {
|
||||||
FullScreen,
|
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
User,
|
User,
|
||||||
Key,
|
Key,
|
||||||
SwitchButton
|
SwitchButton,
|
||||||
|
Fold,
|
||||||
|
Expand,
|
||||||
|
Menu
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
@@ -105,20 +121,44 @@ const router = useRouter()
|
|||||||
// 侧边栏菜单数据
|
// 侧边栏菜单数据
|
||||||
const menus = ref(menuConfig)
|
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(() => {
|
const activeMenu = computed(() => {
|
||||||
return route.path
|
return route.path
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换全屏
|
// 切换侧边栏收缩
|
||||||
const toggleFullScreen = () => {
|
const toggleCollapse = () => {
|
||||||
if (!document.fullscreenElement) {
|
isCollapsed.value = !isCollapsed.value
|
||||||
document.documentElement.requestFullscreen()
|
// 保存状态到localStorage
|
||||||
} else {
|
localStorage.setItem('sidebarCollapsed', isCollapsed.value)
|
||||||
if (document.exitFullscreen) {
|
}
|
||||||
document.exitFullscreen()
|
|
||||||
}
|
// 切换移动端菜单
|
||||||
}
|
const toggleMobileMenu = () => {
|
||||||
|
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭移动端菜单
|
||||||
|
const closeMobileMenu = () => {
|
||||||
|
isMobileMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
@@ -129,9 +169,35 @@ const handleLogout = () => {
|
|||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('tokenExpire')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
userStore.clearUserInfo()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}).catch(() => {})
|
}).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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -141,44 +207,95 @@ const handleLogout = () => {
|
|||||||
width: 100%;
|
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 {
|
.sidebar {
|
||||||
width: 240px;
|
width: 260px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border-right: 1px solid #e1e8ed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 20;
|
z-index: 999;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
height: 60px;
|
height: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
justify-content: center;
|
||||||
color: #333;
|
padding: 0 20px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
overflow: hidden;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo-img {
|
||||||
width: 32px;
|
height: 50px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img-mini {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin-right: 10px;
|
width: 32px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.sidebar-scrollbar {
|
||||||
font-size: 18px;
|
flex: 1;
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu {
|
.sidebar-menu {
|
||||||
border-right: none;
|
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 +305,56 @@ const handleLogout = () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #f0f2f5;
|
background-color: #f0f2f5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部导航栏样式 */
|
/* 顶部导航栏样式 */
|
||||||
.navbar {
|
.navbar {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 0 15px;
|
padding: 0 20px;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
border-bottom: 1px solid #e1e8ed;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-left {
|
.navbar-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-right {
|
.navbar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
padding: 0 10px;
|
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-btn {
|
.header-btn {
|
||||||
height: 40px;
|
height: 36px;
|
||||||
width: 40px;
|
width: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #606266;
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-btn:hover {
|
.header-btn:hover {
|
||||||
background-color: #f5f7fa;
|
background-color: #f8f9fa;
|
||||||
border-radius: 4px;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
@@ -239,16 +363,31 @@ const handleLogout = () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container:hover {
|
.avatar-container:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.025);
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
margin: 0 8px;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avatar-container .el-icon--right) {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container:hover :deep(.el-icon--right) {
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域样式 */
|
/* 内容区域样式 */
|
||||||
@@ -271,12 +410,353 @@ const handleLogout = () => {
|
|||||||
opacity: 0;
|
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) {
|
:deep(.el-dropdown-menu__item) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-dropdown-menu__item i) {
|
:deep(.el-dropdown-menu__item i) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item:hover) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item:hover i) {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item.is-divided) {
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏滚动条样式优化 */
|
||||||
|
: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>
|
</style>
|
||||||
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-icon {
|
.breadcrumb-icon {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__item) {
|
:deep(.el-breadcrumb__item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner) {
|
:deep(.el-breadcrumb__inner) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner a) {
|
:deep(.el-breadcrumb__inner a) {
|
||||||
color: #606266;
|
color: #7f8c8d;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
transition: color 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__inner a:hover) {
|
:deep(.el-breadcrumb__inner a:hover) {
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__inner.is-link) {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:last-child .breadcrumb-icon) {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__separator) {
|
:deep(.el-breadcrumb__separator) {
|
||||||
margin: 0 8px;
|
margin: 0 10px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:first-child .el-breadcrumb__inner) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__item:first-child .breadcrumb-icon) {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
<template>
|
||||||
|
<div class="global-search">
|
||||||
|
<el-tooltip content="全局搜索" placement="bottom">
|
||||||
|
<el-button type="text" class="header-btn" @click="openSearch">
|
||||||
|
<el-icon :size="18"><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:show-close="false"
|
||||||
|
:append-to-body="true"
|
||||||
|
class="search-dialog"
|
||||||
|
width="680px"
|
||||||
|
top="12vh"
|
||||||
|
@opened="focusInput"
|
||||||
|
>
|
||||||
|
<div class="search-header">
|
||||||
|
<el-icon :size="20" class="search-prefix"><Search /></el-icon>
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
v-model="keyword"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索用户、订单、工单、用户商品..."
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
@keydown.escape="visible = false"
|
||||||
|
/>
|
||||||
|
<span v-if="keyword" class="search-clear" @click="clearSearch">
|
||||||
|
<el-icon :size="16"><CircleClose /></el-icon>
|
||||||
|
</span>
|
||||||
|
<span class="search-shortcut">ESC</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasSearched" class="search-body">
|
||||||
|
<el-tabs v-model="activeTab" class="search-tabs">
|
||||||
|
<el-tab-pane name="user">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label">用户 <em v-if="results.user.total > 0">{{ results.user.total }}</em></span>
|
||||||
|
</template>
|
||||||
|
<div class="result-list" v-loading="results.user.loading">
|
||||||
|
<div v-if="results.user.list.length === 0 && !results.user.loading" class="empty-tip">未找到相关用户</div>
|
||||||
|
<div
|
||||||
|
v-for="item in results.user.list"
|
||||||
|
:key="item.user_id"
|
||||||
|
class="result-item"
|
||||||
|
@click="goToUser(item)"
|
||||||
|
>
|
||||||
|
<el-avatar :size="32" :src="item.cover || ''" class="result-avatar">
|
||||||
|
{{ (item.user_name || '')[0] }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="result-info">
|
||||||
|
<span class="result-title" v-html="highlight(item.user_name)"></span>
|
||||||
|
<span class="result-desc">ID: {{ item.user_id }} · {{ item.phone || item.email || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon class="result-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-pagination" v-if="results.user.total > pageSize">
|
||||||
|
<el-pagination small layout="prev, pager, next" :total="results.user.total" :page-size="pageSize" v-model:current-page="results.user.page" @current-change="(p) => { results.user.page = p; searchUsers(keyword.trim()) }" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane name="order">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label">订单 <em v-if="results.order.total > 0">{{ results.order.total }}</em></span>
|
||||||
|
</template>
|
||||||
|
<div class="result-list" v-loading="results.order.loading">
|
||||||
|
<div v-if="results.order.list.length === 0 && !results.order.loading" class="empty-tip">未找到相关订单</div>
|
||||||
|
<div
|
||||||
|
v-for="item in results.order.list"
|
||||||
|
:key="item.id"
|
||||||
|
class="result-item"
|
||||||
|
@click="goToOrder(item)"
|
||||||
|
>
|
||||||
|
<div class="result-icon order-icon">
|
||||||
|
<el-icon :size="18"><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="result-info">
|
||||||
|
<span class="result-title" v-html="highlight(item.name || ('#' + item.id))"></span>
|
||||||
|
<span class="result-desc">用户ID: {{ item.userId }} · ¥{{ (item.price / 100).toFixed(2) }} · {{ item.type }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" :type="orderStatusType(item.state)">{{ orderStatusText(item.state) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-pagination" v-if="results.order.total > pageSize">
|
||||||
|
<el-pagination small layout="prev, pager, next" :total="results.order.total" :page-size="pageSize" v-model:current-page="results.order.page" @current-change="(p) => { results.order.page = p; searchOrders(keyword.trim()) }" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane name="ticket">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label">工单 <em v-if="results.ticket.total > 0">{{ results.ticket.total }}</em></span>
|
||||||
|
</template>
|
||||||
|
<div class="result-list" v-loading="results.ticket.loading">
|
||||||
|
<div v-if="results.ticket.list.length === 0 && !results.ticket.loading" class="empty-tip">未找到相关工单</div>
|
||||||
|
<div
|
||||||
|
v-for="item in results.ticket.list"
|
||||||
|
:key="item.work_id"
|
||||||
|
class="result-item"
|
||||||
|
@click="goToTicket(item)"
|
||||||
|
>
|
||||||
|
<div class="result-icon ticket-icon">
|
||||||
|
<el-icon :size="18"><ChatDotSquare /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="result-info">
|
||||||
|
<span class="result-title" v-html="highlight(item.name)"></span>
|
||||||
|
<span class="result-desc">{{ item.user?.userName || ('用户' + item.user?.userId) }} · {{ formatTime(item.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" :type="ticketStatusType(item.status)">{{ ticketStatusText(item.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-pagination" v-if="results.ticket.total > pageSize">
|
||||||
|
<el-pagination small layout="prev, pager, next" :total="results.ticket.total" :page-size="pageSize" v-model:current-page="results.ticket.page" @current-change="(p) => { results.ticket.page = p; searchTickets(keyword.trim()) }" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane name="goods">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label">用户商品 <em v-if="results.goods.total > 0">{{ results.goods.total }}</em></span>
|
||||||
|
</template>
|
||||||
|
<div class="result-list" v-loading="results.goods.loading">
|
||||||
|
<div v-if="results.goods.list.length === 0 && !results.goods.loading" class="empty-tip">未找到相关用户商品</div>
|
||||||
|
<div
|
||||||
|
v-for="item in results.goods.list"
|
||||||
|
:key="item.id"
|
||||||
|
class="result-item"
|
||||||
|
@click="goToGoods(item)"
|
||||||
|
>
|
||||||
|
<div class="result-icon goods-icon">
|
||||||
|
<el-icon :size="18"><Box /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="result-info">
|
||||||
|
<span class="result-title" v-html="highlight(item.good?.name || item.tag || ('商品#' + item.id))"></span>
|
||||||
|
<span class="result-desc">用户: {{ item.user?.UserName || item.userId }} · 到期: {{ formatTime(item.expireTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon class="result-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-pagination" v-if="results.goods.total > pageSize">
|
||||||
|
<el-pagination small layout="prev, pager, next" :total="results.goods.total" :page-size="pageSize" v-model:current-page="results.goods.page" @current-change="(p) => { results.goods.page = p; searchGoods(keyword.trim()) }" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="search-placeholder">
|
||||||
|
<el-icon :size="48" class="placeholder-icon"><Search /></el-icon>
|
||||||
|
<p>输入关键词后按回车搜索</p>
|
||||||
|
<div class="search-tips">
|
||||||
|
<span>支持搜索:用户名/手机号、订单号、工单标题、商品名称</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Search, CircleClose, ArrowRight, Document, ChatDotSquare, Box } from '@element-plus/icons-vue'
|
||||||
|
import { getUserList } from '@/api/admin/user.js'
|
||||||
|
import { getOrderList } from '@/api/admin/order.js'
|
||||||
|
import { getTickerList } from '@/api/ticket.js'
|
||||||
|
import { getUserGoodsList } from '@/api/admin/userVm.js'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const keyword = ref('')
|
||||||
|
const activeTab = ref('user')
|
||||||
|
const hasSearched = ref(false)
|
||||||
|
const searchInput = ref(null)
|
||||||
|
|
||||||
|
const pageSize = 10
|
||||||
|
|
||||||
|
const results = reactive({
|
||||||
|
user: { list: [], total: 0, loading: false, page: 1 },
|
||||||
|
order: { list: [], total: 0, loading: false, page: 1 },
|
||||||
|
ticket: { list: [], total: 0, loading: false, page: 1 },
|
||||||
|
goods: { list: [], total: 0, loading: false, page: 1 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
searchInput.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
hasSearched.value = false
|
||||||
|
searchInput.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
const key = keyword.value.trim()
|
||||||
|
if (!key) return
|
||||||
|
hasSearched.value = true
|
||||||
|
results.user.page = 1
|
||||||
|
results.order.page = 1
|
||||||
|
results.ticket.page = 1
|
||||||
|
results.goods.page = 1
|
||||||
|
searchUsers(key)
|
||||||
|
searchOrders(key)
|
||||||
|
searchTickets(key)
|
||||||
|
searchGoods(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchUsers = async (key) => {
|
||||||
|
results.user.loading = true
|
||||||
|
results.user.list = []
|
||||||
|
try {
|
||||||
|
const res = await getUserList({ page: results.user.page, count: pageSize, key })
|
||||||
|
if (res.data?.code === 200) {
|
||||||
|
results.user.list = res.data.data?.data || []
|
||||||
|
results.user.total = res.data.data?.all_count || results.user.list.length
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
results.user.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchOrders = async (key) => {
|
||||||
|
results.order.loading = true
|
||||||
|
results.order.list = []
|
||||||
|
try {
|
||||||
|
const res = await getOrderList({ page: results.order.page, count: pageSize, keyword: key })
|
||||||
|
if (res.data?.code === 200) {
|
||||||
|
results.order.list = res.data.data?.list || []
|
||||||
|
results.order.total = res.data.data?.all_count || results.order.list.length
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
results.order.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTickets = async (key) => {
|
||||||
|
results.ticket.loading = true
|
||||||
|
results.ticket.list = []
|
||||||
|
try {
|
||||||
|
const res = await getTickerList(pageSize, results.ticket.page, '', '', '', '', key)
|
||||||
|
if (res?.code === 200) {
|
||||||
|
results.ticket.list = res.data?.data || []
|
||||||
|
results.ticket.total = res.data?.all_count || results.ticket.list.length
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
results.ticket.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchGoods = async (key) => {
|
||||||
|
results.goods.loading = true
|
||||||
|
results.goods.list = []
|
||||||
|
try {
|
||||||
|
const res = await getUserGoodsList({ page: results.goods.page, count: pageSize, keyword: key })
|
||||||
|
if (res.data?.code === 200) {
|
||||||
|
results.goods.list = res.data.data?.data || []
|
||||||
|
results.goods.total = res.data.data?.all_count || results.goods.list.length
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
results.goods.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlight = (text) => {
|
||||||
|
if (!text || !keyword.value) return text
|
||||||
|
const key = keyword.value.trim()
|
||||||
|
if (!key) return text
|
||||||
|
const regex = new RegExp(`(${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
|
||||||
|
return String(text).replace(regex, '<mark>$1</mark>')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
if (!time) return '—'
|
||||||
|
const d = new Date(time)
|
||||||
|
if (isNaN(d.getTime())) return '—'
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToUser = (item) => {
|
||||||
|
visible.value = false
|
||||||
|
router.push({ path: '/user/detail', query: { user_id: item.user_id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToOrder = (item) => {
|
||||||
|
visible.value = false
|
||||||
|
router.push({ path: '/order/list', query: { keyword: keyword.value } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTicket = (item) => {
|
||||||
|
visible.value = false
|
||||||
|
router.push({ path: '/ticket/detail', query: { id: item.work_id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToGoods = (item) => {
|
||||||
|
visible.value = false
|
||||||
|
const tag = (item.tag || item.good?.tag || '').toLowerCase()
|
||||||
|
if (tag === '云服务器') {
|
||||||
|
router.push({ path: '/user-goods/vm-detail', query: { id: item.id } })
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'UserGoodsDetail', params: { id: item.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderStatusText = (status) => {
|
||||||
|
const map = { 0: '待支付', 1: '已完成', 2: '已取消', 3: '已退款' }
|
||||||
|
return map[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderStatusType = (status) => {
|
||||||
|
const map = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger' }
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketStatusText = (status) => {
|
||||||
|
const map = { 0: '待处理', 1: '处理中', 2: '已回复', 3: '已关闭' }
|
||||||
|
return map[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketStatusType = (status) => {
|
||||||
|
const map = { 0: 'danger', 1: 'warning', 2: 'success', 3: 'info' }
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
e.preventDefault()
|
||||||
|
openSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.global-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn {
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search-dialog .el-dialog__header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dialog .el-dialog__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dialog .el-dialog {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e8ecf0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-prefix {
|
||||||
|
color: #909399;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2c3e50;
|
||||||
|
background: transparent;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #a8abb2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear:hover {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-shortcut {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a8abb2;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-body {
|
||||||
|
max-height: 460px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tabs {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tabs .el-tabs__header {
|
||||||
|
padding: 0 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tabs .el-tabs__content {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label em {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-list {
|
||||||
|
padding: 8px 12px;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-icon {
|
||||||
|
background: #fdf6ec;
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-icon {
|
||||||
|
background: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-icon {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c3e50;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title :deep(mark) {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #e6a23c;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 12px 12px;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px 20px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
color: #dcdfe6;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-placeholder p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tips {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a8abb2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-sub-menu v-if="hasChildren" :index="menu.path">
|
<el-sub-menu v-if="hasChildren" :index="menu.path">
|
||||||
<template #title>
|
<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" />
|
<component :is="menu.icon || menu.meta?.icon" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>{{ menu.title || menu.meta?.title }}</span>
|
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
<sidebar-menu-item
|
<sidebar-menu-item
|
||||||
v-for="child in menu.children"
|
v-for="child in menu.children"
|
||||||
:key="child.path"
|
:key="child.path"
|
||||||
:menu="child"
|
:menu="child"
|
||||||
|
:level="level + 1"
|
||||||
/>
|
/>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-menu-item v-else :index="menu.path">
|
<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" />
|
<component :is="menu.icon || menu.meta?.icon" />
|
||||||
</el-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>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -29,6 +32,10 @@ const props = defineProps({
|
|||||||
menu: {
|
menu: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -39,65 +46,45 @@ const hasChildren = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-menu-item, :deep(.el-sub-menu__title) {
|
/* 菜单图标样式 */
|
||||||
height: 50px;
|
.menu-icon {
|
||||||
line-height: 50px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-sub-menu .el-menu-item) {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
padding-left: 55px !important;
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666666;
|
color: #7f8c8d;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 激活菜单项特效 */
|
/* 菜单标题 */
|
||||||
.el-menu-item.is-active {
|
.menu-title {
|
||||||
position: relative;
|
font-size: inherit;
|
||||||
background-color: #e6f7ff !important;
|
letter-spacing: 0.2px;
|
||||||
color: #1890ff !important;
|
white-space: nowrap;
|
||||||
font-weight: 600;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item.is-active::before {
|
/* 图标交互状态 */
|
||||||
content: '';
|
.el-menu-item .menu-icon,
|
||||||
position: absolute;
|
:deep(.el-sub-menu__title .menu-icon) {
|
||||||
top: 0;
|
color: #7f8c8d !important;
|
||||||
left: 0;
|
transition: color 0.2s ease;
|
||||||
width: 3px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
.el-menu-item:hover .menu-icon,
|
||||||
color: #1890ff !important;
|
:deep(.el-sub-menu__title:hover .menu-icon) {
|
||||||
font-weight: 600;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修复图标颜色 */
|
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .menu-icon) {
|
||||||
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
color: #2c3e50 !important;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+246
-140
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tags-view-container">
|
<div class="tags-view-container"
|
||||||
<div class="tags-view-wrapper">
|
@mouseenter="hovered = true" @mouseleave="hovered = false">
|
||||||
|
<div class="tags-view-wrapper" ref="scrollWrapperRef"
|
||||||
|
@wheel.prevent="handleWheel"
|
||||||
|
@scroll="onScroll">
|
||||||
<div class="tags-view-scroll">
|
<div class="tags-view-scroll">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tag in visitedViews"
|
v-for="tag in visitedViews"
|
||||||
@@ -23,6 +26,10 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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 { useRoute, useRouter } from 'vue-router'
|
||||||
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const tagsViewStore = useTagsViewStore()
|
||||||
|
|
||||||
|
const visitedViews = computed(() => tagsViewStore.visitedViews)
|
||||||
|
const affixTags = computed(() => tagsViewStore.affixTags)
|
||||||
|
|
||||||
// 固定标签
|
|
||||||
const affixTags = ref([])
|
|
||||||
// 访问过的标签
|
|
||||||
const visitedViews = ref([])
|
|
||||||
// 右键菜单
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const top = ref(0)
|
const top = ref(0)
|
||||||
const left = ref(0)
|
const left = ref(0)
|
||||||
const selectedTag = ref({})
|
const selectedTag = ref({})
|
||||||
|
|
||||||
// 初始化标签
|
|
||||||
const initTags = () => {
|
const initTags = () => {
|
||||||
// 如果当前路由不在访问过的标签中,添加它
|
|
||||||
if (route.name) {
|
if (route.name) {
|
||||||
addVisitedView(route)
|
tagsViewStore.addVisitedView(route)
|
||||||
}
|
}
|
||||||
// 添加固定标签(仪表盘)
|
|
||||||
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
||||||
if (dashboardRoute) {
|
if (dashboardRoute) {
|
||||||
affixTags.value.push(dashboardRoute)
|
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
|
||||||
addVisitedView(dashboardRoute)
|
tagsViewStore.affixTags.push(dashboardRoute)
|
||||||
|
}
|
||||||
|
tagsViewStore.addVisitedView(dashboardRoute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加访问过的标签
|
|
||||||
const addVisitedView = (view) => {
|
|
||||||
if (visitedViews.value.some(v => v.path === view.path)) return
|
|
||||||
|
|
||||||
// 过滤404和登录页
|
|
||||||
if (view.name === 'NotFound' || view.name === 'Login') return
|
|
||||||
|
|
||||||
visitedViews.value.push(
|
|
||||||
Object.assign({}, view, {
|
|
||||||
title: view.meta.title || 'unknown'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新选中的标签
|
|
||||||
const refreshSelectedTag = (view) => {
|
const refreshSelectedTag = (view) => {
|
||||||
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
|
|
||||||
// 然后立即再将路由重定向回原路由,实现刷新效果
|
|
||||||
const { fullPath } = view
|
const { fullPath } = view
|
||||||
router.replace('/redirect' + fullPath)
|
router.replace('/redirect' + fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭选中的标签
|
|
||||||
const closeSelectedTag = (view) => {
|
const closeSelectedTag = (view) => {
|
||||||
// 从访问过的标签中移除
|
tagsViewStore.delVisitedView(view).then((visitedViews) => {
|
||||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
if (isActive(view)) {
|
||||||
if (index > -1) {
|
toLastView(visitedViews, view)
|
||||||
visitedViews.value.splice(index, 1)
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// 如果关闭的是当前标签,则跳转到下一个标签
|
|
||||||
if (isActive(view)) {
|
|
||||||
toLastView(visitedViews.value, view)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭其他标签
|
|
||||||
const closeOthersTags = () => {
|
const closeOthersTags = () => {
|
||||||
// 保留固定标签和当前选中的标签
|
router.push(selectedTag.value)
|
||||||
visitedViews.value = visitedViews.value.filter(v => {
|
tagsViewStore.delOthersViews(selectedTag.value)
|
||||||
return isAffix(v) || v.path === selectedTag.value.path
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭左侧标签
|
|
||||||
const closeLeftTags = () => {
|
const closeLeftTags = () => {
|
||||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
|
||||||
if (selectedIndex === -1) return
|
if (!visitedViews.find(i => i.path === route.path)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
// 保留固定标签和右侧标签
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
|
||||||
return isAffix(v) || i >= selectedIndex
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭右侧标签
|
|
||||||
const closeRightTags = () => {
|
const closeRightTags = () => {
|
||||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
|
||||||
if (selectedIndex === -1) return
|
if (!visitedViews.find(i => i.path === route.path)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
// 保留固定标签和左侧标签
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
|
||||||
return isAffix(v) || i <= selectedIndex
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isActive(selectedTag.value)) {
|
|
||||||
router.push(selectedTag.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所有标签
|
|
||||||
const closeAllTags = () => {
|
const closeAllTags = () => {
|
||||||
// 仅保留固定标签
|
tagsViewStore.delAllViews().then((visitedViews) => {
|
||||||
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
|
toLastView(visitedViews)
|
||||||
|
})
|
||||||
// 跳转到第一个标签或首页
|
|
||||||
toLastView(visitedViews.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到最后一个标签或首页
|
|
||||||
const toLastView = (visitedViews, view) => {
|
const toLastView = (visitedViews, view) => {
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
if (latestView) {
|
if (latestView) {
|
||||||
router.push(latestView)
|
router.push(latestView)
|
||||||
} else {
|
} else {
|
||||||
// 如果没有标签,则跳转到首页
|
|
||||||
if (view && view.name === 'Dashboard') {
|
if (view && view.name === 'Dashboard') {
|
||||||
// 如果当前是首页,则刷新页面
|
|
||||||
router.push('/redirect' + '/dashboard')
|
router.push('/redirect' + '/dashboard')
|
||||||
} else {
|
} else {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
@@ -190,17 +146,14 @@ const toLastView = (visitedViews, view) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是当前激活的标签
|
|
||||||
const isActive = (tag) => {
|
const isActive = (tag) => {
|
||||||
return tag.path === route.path
|
return tag.path === route.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是固定标签
|
|
||||||
const isAffix = (tag) => {
|
const isAffix = (tag) => {
|
||||||
return affixTags.value.some(t => t.path === tag.path)
|
return tag.meta && tag.meta.affix
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开右键菜单
|
|
||||||
const openMenu = (e, tag) => {
|
const openMenu = (e, tag) => {
|
||||||
const menuMinWidth = 125
|
const menuMinWidth = 125
|
||||||
const offsetLeft = e.clientX
|
const offsetLeft = e.clientX
|
||||||
@@ -214,30 +167,112 @@ const openMenu = (e, tag) => {
|
|||||||
selectedTag.value = 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 = () => {
|
const closeMenu = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化,添加标签
|
|
||||||
watch(route, (newRoute) => {
|
watch(route, (newRoute) => {
|
||||||
if (newRoute.name) {
|
if (newRoute.name) {
|
||||||
addVisitedView(newRoute)
|
tagsViewStore.addVisitedView(newRoute)
|
||||||
}
|
}
|
||||||
|
nextTick(scrollToActiveTag)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击其他区域关闭右键菜单
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
refreshState()
|
||||||
|
scrollToActiveTag()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initTags()
|
initTags()
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
nextTick(() => { refreshState(); scrollToActiveTag() })
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('click', handleClickOutside)
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -245,21 +280,24 @@ onBeforeUnmount(() => {
|
|||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #e1e8ed;
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标签滚动区域 */
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
overflow-x: scroll;
|
||||||
display: flex;
|
overflow-y: hidden;
|
||||||
align-items: center;
|
scrollbar-width: none;
|
||||||
padding: 0 16px;
|
-ms-overflow-style: none;
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-view-wrapper::-webkit-scrollbar {
|
.tags-view-wrapper::-webkit-scrollbar {
|
||||||
@@ -267,44 +305,94 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tags-view-scroll {
|
.tags-view-scroll {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag, .active-tag {
|
|
||||||
height: 28px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
height: 100%;
|
||||||
margin-right: 5px;
|
padding: 0 8px;
|
||||||
border-radius: 2px;
|
gap: 4px;
|
||||||
font-size: 12px;
|
}
|
||||||
color: #333333;
|
|
||||||
background-color: #f4f4f5;
|
/* 底部自定义滚动条(在容器上,不在滚动区域内) */
|
||||||
|
.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;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: all 0.2s ease;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
color: #7f8c8d;
|
||||||
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:hover {
|
.tag:hover {
|
||||||
color: #1890ff;
|
color: #34495e;
|
||||||
background-color: #e6f7ff;
|
background-color: #e8ecf0;
|
||||||
border-color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag {
|
.active-tag {
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
background-color: #e6f7ff;
|
background-color: #ffffff;
|
||||||
border-color: #1890ff;
|
border: 1px solid #e1e8ed;
|
||||||
font-weight: 600;
|
border-bottom: 2px solid #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-icon {
|
.tag-icon {
|
||||||
margin-right: 4px;
|
margin-right: 6px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
|
color: #95a5a6;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover .tag-icon {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-tag .tag-icon {
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-title {
|
.tag-title {
|
||||||
@@ -315,36 +403,47 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-close {
|
.tag-close {
|
||||||
margin-left: 5px;
|
margin-left: 8px;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 0;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s ease;
|
||||||
|
color: #95a5a6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-close:hover {
|
||||||
|
color: #e74c3c;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:hover .tag-close {
|
.tag:hover .tag-close {
|
||||||
color: #666;
|
color: #7f8c8d;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag .tag-close {
|
.active-tag .tag-close {
|
||||||
color: #1890ff;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-tag:hover .tag-close {
|
.active-tag .tag-close:hover {
|
||||||
background-color: rgba(24, 144, 255, 0.1);
|
color: #e74c3c;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右键菜单 */
|
||||||
.contextmenu {
|
.contextmenu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 6px 0;
|
padding: 4px 0;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li {
|
.contextmenu li {
|
||||||
@@ -352,15 +451,22 @@ onBeforeUnmount(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #34495e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li:hover {
|
.contextmenu li:hover {
|
||||||
background-color: #f5f7fa;
|
background-color: #f8f9fa;
|
||||||
color: #1890ff;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu li .el-icon {
|
.contextmenu li .el-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
</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
|
||||||
|
}
|
||||||
+192
-18
@@ -5,10 +5,101 @@ export const menus = [
|
|||||||
icon: 'DataBoard'
|
icon: 'DataBoard'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : '/ticket',
|
path: '/ticket',
|
||||||
title: '工单处理',
|
title: '工单管理',
|
||||||
icon: 'DataBoard'
|
icon: 'Tickets',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/ticket/list',
|
||||||
|
title: '工单列表'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ticket/types',
|
||||||
|
title: '工单类型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ticket/templates',
|
||||||
|
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',
|
path: '/acs',
|
||||||
@@ -33,39 +124,122 @@ export const menus = [
|
|||||||
{ path: '/acs/images/categories', title: '镜像分类' }
|
{ path: '/acs/images/categories', title: '镜像分类' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/acs/nodes', title: '节点管理'
|
path: '/acs/nodes',
|
||||||
|
title: '节点管理'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/acs/guacamole',
|
path: '/acs/guacamole',
|
||||||
title: '远程桌面网关管理',
|
title: '远程桌面网关管理'
|
||||||
icon: 'Monitor'
|
},
|
||||||
},{
|
{
|
||||||
path: '/audit',
|
path: '/audit',
|
||||||
title: '站点审计',
|
title: '站点审计',
|
||||||
icon: 'Monitor',
|
|
||||||
children: [
|
children: [
|
||||||
{ path: '/audit/all', title: '所有站点' },
|
{ path: '/audit/all', title: '所有站点' },
|
||||||
{ path: '/audit/violation', title: '违规站点' }
|
{ path: '/audit/violation', title: '违规站点' }
|
||||||
]
|
]
|
||||||
},{
|
},
|
||||||
path:'/setting',
|
{
|
||||||
title:'全局设置管理',
|
path: '/setting',
|
||||||
icon:'Setting',
|
title: '全局设置管理',
|
||||||
children:[
|
children: [
|
||||||
{path:'/setting/global',title:'全局设置'}
|
{ path: '/setting/global', title: '全局设置' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/sms',
|
||||||
|
title: '短信平台管理',
|
||||||
|
icon: 'ChatDotRound',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/sms/service',
|
||||||
|
title: '主控服务管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/goods',
|
||||||
|
title: '额度商品管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/signature',
|
||||||
|
title: '签名管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/template',
|
||||||
|
title: '模板管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/mail',
|
||||||
|
title: '邮箱平台管理',
|
||||||
|
icon: 'Message',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/mail/service',
|
||||||
|
title: '主控服务管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '虚拟化平台管理',
|
||||||
|
icon: 'Platform',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/virtualization/kvm-service',
|
||||||
|
title: '主控服务管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/virtualization/host-group-mapping',
|
||||||
|
title: '宿主机组映射管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/virtualization/vnc-command',
|
||||||
|
title: 'VNC指令管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
title: '系统管理',
|
title: '系统管理',
|
||||||
icon: 'Setting',
|
icon: 'Setting',
|
||||||
children: [
|
children: [
|
||||||
// { path: '/system/users', title: '用户管理' },
|
{
|
||||||
// { path: '/system/operation-log', title: '操作日志' },
|
path: '/system/permission',
|
||||||
{ path: '/system/domain-whitelist', title: '域名白名单' }
|
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/notice-channel',
|
||||||
|
title: '通知管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system/menu',
|
||||||
|
title: '菜单管理',
|
||||||
|
children: [
|
||||||
|
{ path: '/system/menu-manage', title: '菜单列表' },
|
||||||
|
{ path: '/system/menu-permission', title: '菜单权限' }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+525
-34
@@ -39,9 +39,45 @@ const routes = [
|
|||||||
title: '工单管理',
|
title: '工单管理',
|
||||||
icon: 'Tickets'
|
icon: 'Tickets'
|
||||||
},
|
},
|
||||||
component: () => import('../views/ticket/TicketChat.vue'),
|
redirect: '/ticket/list',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'TicketList',
|
||||||
|
component: () => import('../views/ticket/TicketList.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '工单列表'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail',
|
||||||
|
name: 'TicketDetail',
|
||||||
|
component: () => import('../views/ticket/TicketDetail.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '工单详情',
|
||||||
|
hidden: true,
|
||||||
|
activeMenu: '/ticket/list'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'types',
|
||||||
|
name: 'TicketTypes',
|
||||||
|
component: () => import('../views/ticket/TicketTypes.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '工单类型管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'templates',
|
||||||
|
name: 'TicketTemplates',
|
||||||
|
component: () => import('../views/ticket/TicketTemplates.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '回复模板管理'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ACS管理路由
|
// ACS管理路由
|
||||||
{
|
{
|
||||||
path: 'acs',
|
path: 'acs',
|
||||||
@@ -130,7 +166,19 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '节点管理'
|
title: '节点管理'
|
||||||
}
|
}
|
||||||
},{
|
},
|
||||||
|
{
|
||||||
|
path: 'nodes/form',
|
||||||
|
name: 'ServerForm',
|
||||||
|
component: () => import('@/views/acs/nodes/ServerForm.vue'),
|
||||||
|
meta: { title: '服务器表单', activeMenu: '/acs/nodes', hidden: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'images/form',
|
||||||
|
name: 'ImageForm',
|
||||||
|
component: () => import('@/views/acs/images/ImageForm.vue'),
|
||||||
|
meta: { title: '镜像表单', activeMenu: '/acs/images/vm', hidden: true }
|
||||||
|
}, {
|
||||||
path: 'guacamole',
|
path: 'guacamole',
|
||||||
name: 'Guacamole',
|
name: 'Guacamole',
|
||||||
component: () => import('../views/acs/guacamole/Guacamole.vue'),
|
component: () => import('../views/acs/guacamole/Guacamole.vue'),
|
||||||
@@ -140,6 +188,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',
|
path: 'system',
|
||||||
name: 'System',
|
name: 'System',
|
||||||
@@ -147,28 +384,282 @@ const routes = [
|
|||||||
title: '系统管理',
|
title: '系统管理',
|
||||||
icon: 'Setting'
|
icon: 'Setting'
|
||||||
},
|
},
|
||||||
redirect: '/system/domain-whitelist',
|
redirect: '/system/permission/route',
|
||||||
children: [
|
children: [
|
||||||
// 注释掉的用户管理和操作日志路由,与菜单配置保持一致
|
{
|
||||||
// {
|
path: 'permission/route',
|
||||||
// path: 'users',
|
name: 'PermissionRoute',
|
||||||
// name: 'Users',
|
component: () => import('../views/system/PermissionRoute.vue'),
|
||||||
// component: () => import('../views/system/Users.vue'),
|
meta: {
|
||||||
// meta: {
|
title: '路由权限'
|
||||||
// title: '用户管理'
|
}
|
||||||
// }
|
},
|
||||||
// },
|
{
|
||||||
// {
|
path: 'permission/admin',
|
||||||
// path: 'operation-log',
|
name: 'PermissionAdmin',
|
||||||
// name: 'OperationLog',
|
component: () => import('../views/system/PermissionAdmin.vue'),
|
||||||
// component: OperationLog,
|
meta: {
|
||||||
// meta: { title: '操作日志' }
|
title: '管理员权限'
|
||||||
// },
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'file',
|
||||||
|
name: 'SystemFile',
|
||||||
|
component: () => import('../views/system/SystemFile.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '文件管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'domain-whitelist',
|
path: 'domain-whitelist',
|
||||||
name: 'DomainWhitelist',
|
name: 'DomainWhitelist',
|
||||||
component: () => import('../views/system/DomainWhitelist.vue'),
|
component: () => import('../views/system/DomainWhitelist.vue'),
|
||||||
meta: { title: '域名白名单' }
|
meta: { title: '域名白名单' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'setting-manage',
|
||||||
|
name: 'SettingManage',
|
||||||
|
component: () => import('../views/system/SettingManage.vue'),
|
||||||
|
meta: { title: '配置管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'notice-channel',
|
||||||
|
name: 'NoticeChannel',
|
||||||
|
component: () => import('../views/system/NoticeChannel.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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 短信平台管理路由
|
||||||
|
{
|
||||||
|
path: 'sms',
|
||||||
|
name: 'Sms',
|
||||||
|
meta: {
|
||||||
|
title: '短信平台管理',
|
||||||
|
icon: 'ChatDotRound'
|
||||||
|
},
|
||||||
|
redirect: '/sms/service',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'service',
|
||||||
|
name: 'SmsService',
|
||||||
|
component: () => import('../views/sms/SmsService.vue'),
|
||||||
|
meta: { title: '主控服务管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods',
|
||||||
|
name: 'SmsGoods',
|
||||||
|
component: () => import('../views/sms/SmsGoods.vue'),
|
||||||
|
meta: { title: '额度商品管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signature',
|
||||||
|
name: 'SmsSignature',
|
||||||
|
component: () => import('../views/sms/SmsSignature.vue'),
|
||||||
|
meta: { title: '签名管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'template',
|
||||||
|
name: 'SmsTemplateMgr',
|
||||||
|
component: () => import('../views/sms/SmsTemplate.vue'),
|
||||||
|
meta: { title: '模板管理' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 邮箱平台管理路由
|
||||||
|
{
|
||||||
|
path: 'mail',
|
||||||
|
name: 'Mail',
|
||||||
|
meta: {
|
||||||
|
title: '邮箱平台管理',
|
||||||
|
icon: 'Message'
|
||||||
|
},
|
||||||
|
redirect: '/mail/service',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'service',
|
||||||
|
name: 'MailService',
|
||||||
|
component: () => import('../views/mail/MailService.vue'),
|
||||||
|
meta: { title: '主控服务管理' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -269,21 +760,21 @@ const routes = [
|
|||||||
title: '容器详情',
|
title: '容器详情',
|
||||||
hidden: true
|
hidden: true
|
||||||
}
|
}
|
||||||
},{
|
}, {
|
||||||
path:'servers/container/console',
|
path: 'servers/container/console',
|
||||||
name:'ContainerConsole',
|
name: 'ContainerConsole',
|
||||||
component:()=>import('../views/acs/nodes/containerConsole.vue'),
|
component: () => import('../views/acs/nodes/containerConsole.vue'),
|
||||||
meta:{
|
meta: {
|
||||||
title:'终端容器',
|
title: '终端容器',
|
||||||
hidden:true
|
hidden: true
|
||||||
}
|
}
|
||||||
},{
|
}, {
|
||||||
path:'servers/container/files',
|
path: 'servers/container/files',
|
||||||
name:'ContainerFiles',
|
name: 'ContainerFiles',
|
||||||
component:()=>import('../views/acs/nodes/containFile.vue'),
|
component: () => import('../views/acs/nodes/containFile.vue'),
|
||||||
meta:{
|
meta: {
|
||||||
title:'容器文件管理',
|
title: '容器文件管理',
|
||||||
hidden:true
|
hidden: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -317,7 +808,7 @@ const router = createRouter({
|
|||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// 设置页面标题
|
// 设置页面标题
|
||||||
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
|
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
|
||||||
|
|
||||||
// 这里可以添加登录验证逻辑
|
// 这里可以添加登录验证逻辑
|
||||||
const isAuthenticated = localStorage.getItem('token')
|
const isAuthenticated = localStorage.getItem('token')
|
||||||
if (to.path !== '/login' && !isAuthenticated) {
|
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',() => {
|
export const useUserStore = defineStore('userStore',() => {
|
||||||
|
|
||||||
let userInfo = ref({})
|
// 初始化时从localStorage读取用户信息
|
||||||
|
const savedUserInfo = localStorage.getItem('userInfo')
|
||||||
|
let userInfo = ref(savedUserInfo ? JSON.parse(savedUserInfo) : {})
|
||||||
|
|
||||||
function setUserInfo(u){
|
function setUserInfo(u){
|
||||||
userInfo.value = 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;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.hidden-xs {
|
.hidden-xs {
|
||||||
display: none !important;
|
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) {
|
@media (min-width: 768px) and (max-width: 992px) {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import {http2} from "@/utils/request.js";
|
|||||||
|
|
||||||
/**获取所有站点 */
|
/**获取所有站点 */
|
||||||
export const getSiteList = (data) => {
|
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 = () => {
|
export const auditSite = () => {
|
||||||
return http2.get(`/v1/admin/audit/start`)
|
return http2.get(`/acs/v1/admin/audit/start`)
|
||||||
}
|
}
|
||||||
/**删除违规网页审计 传入参数: web_key 站点名*/
|
/**删除违规网页审计 传入参数: web_key 站点名*/
|
||||||
export const delAudit = (data) => {
|
export const delAudit = (data) => {
|
||||||
return http2.post(`/v1/admin/audit/delete`,data,{
|
return http2.post(`/acs/v1/admin/audit/delete`,data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -19,5 +19,5 @@ export const delAudit = (data) => {
|
|||||||
}
|
}
|
||||||
/**获取违规网页审计列表 */
|
/**获取违规网页审计列表 */
|
||||||
export const getAuditList = (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";
|
import {http2} from "@/utils/request.js";
|
||||||
export const getFileList = (data) => {
|
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) => {
|
export const readFile = (data) => {
|
||||||
return http2.post(`/v1/file/read`,data, {
|
return http2.post(`/acs/v1/file/read`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ export const readFile = (data) => {
|
|||||||
}
|
}
|
||||||
/*删除文件或文件夹 */
|
/*删除文件或文件夹 */
|
||||||
export const deleteFile = (data) => {
|
export const deleteFile = (data) => {
|
||||||
return http2.post(`/v1/file/delete`,data, {
|
return http2.post(`/acs/v1/file/delete`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ export const deleteFile = (data) => {
|
|||||||
}
|
}
|
||||||
/*写入文件 */
|
/*写入文件 */
|
||||||
export const writeFile = (data) => {
|
export const writeFile = (data) => {
|
||||||
return http2.post(`/v1/file/write`,data, {
|
return http2.post(`/acs/v1/file/write`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export const writeFile = (data) => {
|
|||||||
}
|
}
|
||||||
/*创建文件夹 */
|
/*创建文件夹 */
|
||||||
export const createFolder = (data) => {
|
export const createFolder = (data) => {
|
||||||
return http2.post(`/v1/file/mkdir`,data, {
|
return http2.post(`/acs/v1/file/mkdir`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export const createFolder = (data) => {
|
|||||||
}
|
}
|
||||||
/**上传文件 */
|
/**上传文件 */
|
||||||
export const uploadFile = (data) => {
|
export const uploadFile = (data) => {
|
||||||
return http2.post(`/v1/file/upload_file`,data, {
|
return http2.post(`/acs/v1/file/upload_file`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export const uploadFile = (data) => {
|
|||||||
}
|
}
|
||||||
/**下载文件链接 */
|
/**下载文件链接 */
|
||||||
export const downloadFile = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export const downloadFile = (data) => {
|
|||||||
}
|
}
|
||||||
/**压缩文件 */
|
/**压缩文件 */
|
||||||
export const compressFile = (data) => {
|
export const compressFile = (data) => {
|
||||||
return http2.post(`/v1/file/zip_file`,data, {
|
return http2.post(`/acs/v1/file/zip_file`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ export const compressFile = (data) => {
|
|||||||
}
|
}
|
||||||
/**解压文件 */
|
/**解压文件 */
|
||||||
export const decompressFile = (data) => {
|
export const decompressFile = (data) => {
|
||||||
return http2.post(`/v1/file/unzip_file`,data, {
|
return http2.post(`/acs/v1/file/unzip_file`,data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import {http2} from "@/utils/request.js";
|
|||||||
|
|
||||||
/**获取 guacamole 列表 */
|
/**获取 guacamole 列表 */
|
||||||
export const getGuacamoleList = data => {
|
export const getGuacamoleList = data => {
|
||||||
return http2.get(`/v1/admin/server/get_guacamole_list`);
|
return http2.get(`/acs/v1/admin/server/get_guacamole_list`);
|
||||||
};
|
};
|
||||||
/**获取服务器 guacamole 信息 */
|
/**获取服务器 guacamole 信息 */
|
||||||
export const getGuacamoleInfo = data => {
|
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*/
|
/**新增 guacamole 参数 url:string,username:string,password:string*/
|
||||||
export const addGuacamoleInfo = data => {
|
export const addGuacamoleInfo = data => {
|
||||||
return http2.post(`/v1/admin/server/add_guacamole`, data,{
|
return http2.post(`/acs/v1/admin/server/add_guacamole`, data,{
|
||||||
headers:{
|
headers:{
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export const addGuacamoleInfo = data => {
|
|||||||
};
|
};
|
||||||
/**修改guacamole 参数 id:string,url:string,username:string,password:string*/
|
/**修改guacamole 参数 id:string,url:string,username:string,password:string*/
|
||||||
export const updateGuacamoleInfo = data => {
|
export const updateGuacamoleInfo = data => {
|
||||||
return http2.post(`/v1/admin/server/edit_guacamole`, data,{
|
return http2.post(`/acs/v1/admin/server/edit_guacamole`, data,{
|
||||||
headers:{
|
headers:{
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export const updateGuacamoleInfo = data => {
|
|||||||
};
|
};
|
||||||
/**删除guacamole 参数 id:string */
|
/**删除guacamole 参数 id:string */
|
||||||
export const deleteGuacamoleInfo = data => {
|
export const deleteGuacamoleInfo = data => {
|
||||||
return http2.post(`/v1/admin/server/delete_guacamole`, data,{
|
return http2.post(`/acs/v1/admin/server/delete_guacamole`, data,{
|
||||||
headers:{
|
headers:{
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -1,15 +1,15 @@
|
|||||||
import {http2} from "@/utils/request.js";
|
import {http2} from "@/utils/request.js";
|
||||||
/**获取消息列表 */
|
/**获取消息列表 */
|
||||||
export const getMessageList = (data) => {
|
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) => {
|
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) => {
|
export const addMessage = (data) => {
|
||||||
return http2.post(`/v1/messages/add_message`, data,{
|
return http2.post(`/acs/v1/messages/add_message`, data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**删除消息 */
|
/**删除消息 */
|
||||||
export const deleteMessage = (data) => {
|
export const deleteMessage = (data) => {
|
||||||
return http2.post(`/v1/messages/delete_message`, data,{
|
return http2.post(`/acs/v1/messages/delete_message`, data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**修改消息 */
|
/**修改消息 */
|
||||||
export const editMessage = (data) => {
|
export const editMessage = (data) => {
|
||||||
return http2.post(`/v1/messages/update_message`, data,{
|
return http2.post(`/acs/v1/messages/update_message`, data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,11 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**获取附件列表 */
|
/**获取附件列表 */
|
||||||
export const getFileList = (data) => {
|
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) => {
|
export const uploadFile = (data) => {
|
||||||
return http2.post(`/v1/attachment/add_attachment`, data,{
|
return http2.post(`/acs/v1/attachment/add_attachment`, data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -45,20 +45,20 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**删除附件 */
|
/**删除附件 */
|
||||||
export const deleteFile = (data) => {
|
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) => {
|
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) => {
|
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) => {
|
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)=> {
|
export const compressAndConvertFileToBase64=async(file)=> {
|
||||||
|
|||||||
+15
-12
@@ -1,17 +1,20 @@
|
|||||||
import {http2} from "@/utils/request.js";
|
import {http2} from "@/utils/request.js";
|
||||||
/**获取镜像列表 */
|
/**获取镜像列表 */
|
||||||
export const getMirrorList = data => {
|
export const getMirrorList = data => {
|
||||||
return http2.get(`/v1/image/list?server_id=${data}`);
|
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 => {
|
export const getUserMirrorList = data => {
|
||||||
return http2.get(
|
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 => {
|
export const uploadMirror = data => {
|
||||||
return http2.post("/v1/image/pull", data, {
|
return http2.post("/acs/v1/image/pull", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -19,7 +22,7 @@ export const uploadMirror = data => {
|
|||||||
};
|
};
|
||||||
/**编辑镜像 */
|
/**编辑镜像 */
|
||||||
export const editMirror = data => {
|
export const editMirror = data => {
|
||||||
return http2.post("/v1/image/update", data, {
|
return http2.post("/acs/v1/image/update", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -27,7 +30,7 @@ export const editMirror = data => {
|
|||||||
};
|
};
|
||||||
/**删除镜像 */
|
/**删除镜像 */
|
||||||
export const delMirror = data => {
|
export const delMirror = data => {
|
||||||
return http2.post("/v1/image/delete", data, {
|
return http2.post("/acs/v1/image/delete", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -35,11 +38,11 @@ export const delMirror = data => {
|
|||||||
};
|
};
|
||||||
/**镜像同步 */
|
/**镜像同步 */
|
||||||
export const syncMirror = 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 => {
|
export const pullMirror = data => {
|
||||||
return http2.post(`/v1/image/repull`, data, {
|
return http2.post(`/acs/v1/image/repull`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -50,12 +53,12 @@ export const pullMirror = data => {
|
|||||||
export const Mirrorinfo = data => {
|
export const Mirrorinfo = data => {
|
||||||
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
||||||
return http2.get(
|
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 => {
|
export const addVirtualMirror = data => {
|
||||||
return http2.post("/v1/image/create", data, {
|
return http2.post("/acs/v1/image/create", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -64,11 +67,11 @@ export const addVirtualMirror = data => {
|
|||||||
|
|
||||||
|
|
||||||
export const getImageTypeList = (server_id) => {
|
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) => {
|
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,
|
server_id,
|
||||||
class_name,
|
class_name,
|
||||||
class_ico
|
class_ico
|
||||||
@@ -80,7 +83,7 @@ export const createImageType = (server_id,class_name,class_ico) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateImageType = (class_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_id,
|
||||||
class_name,
|
class_name,
|
||||||
class_ico
|
class_ico
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {http2} from "@/utils/request.js";
|
import {http2} from "@/utils/request.js";
|
||||||
/**获取订单列表 */
|
/**获取订单列表 */
|
||||||
export const getOrderList = (data) => {
|
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) => {
|
export const editOrder = (data) => {
|
||||||
return http2.post('/v1/admin/trades/update_trades',data,{
|
return http2.post('/acs/v1/admin/trades/update_trades',data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**删除订单 */
|
/**删除订单 */
|
||||||
export const deleteOrder = (data) => {
|
export const deleteOrder = (data) => {
|
||||||
return http2.post('/v1/admin/trades/delete_trade',data,{
|
return http2.post('/acs/v1/admin/trades/delete_trade',data,{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,5 @@ headers: {
|
|||||||
}
|
}
|
||||||
/**用户获取订单列表 */
|
/**用户获取订单列表 */
|
||||||
export const getUserOrderList = (data) => {
|
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验证码 */
|
// /**email验证码 */
|
||||||
// export const ask_update_user_email = data => {
|
// 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: {
|
// headers: {
|
||||||
// "Content-Type": "multipart/form-data"
|
// "Content-Type": "multipart/form-data"
|
||||||
// }
|
// }
|
||||||
@@ -15,7 +15,7 @@ export const get_pay_code = data => {
|
|||||||
// };
|
// };
|
||||||
/**获取容器订单金额 */
|
/**获取容器订单金额 */
|
||||||
export const procedure_get_price = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export const procedure_get_price = data => {
|
|||||||
|
|
||||||
/**获取虚拟机订单金额 */
|
/**获取虚拟机订单金额 */
|
||||||
export const procedure_vir_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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"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") => {
|
export const getServer = (page, count, key, type = "dockerContainer") => {
|
||||||
return http2.get(
|
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 => {
|
export const addServer = data => {
|
||||||
return http2.post("/v1/admin/server/add_server", data, {
|
return http2.post("/acs/v1/admin/server/add_server", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export const addServer = data => {
|
|||||||
};
|
};
|
||||||
/**编辑服务器 */
|
/**编辑服务器 */
|
||||||
export const editServer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export const editServer = data => {
|
|||||||
};
|
};
|
||||||
/**删除服务器 */
|
/**删除服务器 */
|
||||||
export const deleteServer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export const deleteServer = data => {
|
|||||||
};
|
};
|
||||||
/**查询指定服务器 */
|
/**查询指定服务器 */
|
||||||
export const selectServer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -42,12 +42,12 @@ export const selectServer = data => {
|
|||||||
/**获取服务器套餐列表*/
|
/**获取服务器套餐列表*/
|
||||||
export const getServerPlan = data => {
|
export const getServerPlan = data => {
|
||||||
return http2.get(
|
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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export const selectServerPlan = data => {
|
|||||||
};
|
};
|
||||||
/**新增容器 */
|
/**新增容器 */
|
||||||
export const addContainer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export const addContainer = data => {
|
|||||||
};
|
};
|
||||||
/**删除容器网络 */
|
/**删除容器网络 */
|
||||||
export const deleteContainerNetwork = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ export const deleteContainerNetwork = data => {
|
|||||||
|
|
||||||
/**修改套餐信息 */
|
/**修改套餐信息 */
|
||||||
export const editServerPlan = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export const editServerPlan = data => {
|
|||||||
};
|
};
|
||||||
/**新增套餐 */
|
/**新增套餐 */
|
||||||
export const addServerPlan = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export const addServerPlan = data => {
|
|||||||
};
|
};
|
||||||
/**删除套餐 */
|
/**删除套餐 */
|
||||||
export const deleteServerPlan = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -97,19 +97,19 @@ export const deleteServerPlan = data => {
|
|||||||
/**获取容器列表 */
|
/**获取容器列表 */
|
||||||
export const getContainer = data => {
|
export const getContainer = data => {
|
||||||
return http2.get(
|
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 => {
|
export const getInstanceList = data => {
|
||||||
return http2.get(
|
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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -117,11 +117,11 @@ export const getOneContainer = data => {
|
|||||||
};
|
};
|
||||||
/**查询指定虚拟机信息(管理员查询) */
|
/**查询指定虚拟机信息(管理员查询) */
|
||||||
export const getVmAdminContainer = id => {
|
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 => {
|
export const pauseContainer = data => {
|
||||||
return http2.post("/v1/admin/container/pause_container", data, {
|
return http2.post("/acs/v1/admin/container/pause_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ export const pauseContainer = data => {
|
|||||||
};
|
};
|
||||||
// 暂停虚拟机
|
// 暂停虚拟机
|
||||||
export const pauseInstance = (data, id) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ export const pauseInstance = (data, id) => {
|
|||||||
};
|
};
|
||||||
/**恢复虚拟机 */
|
/**恢复虚拟机 */
|
||||||
export const unpauseInstance = (id, data = "") => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ export const unpauseInstance = (id, data = "") => {
|
|||||||
|
|
||||||
// 解除暂停
|
// 解除暂停
|
||||||
export const unpauseContainer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ export const unpauseContainer = data => {
|
|||||||
};
|
};
|
||||||
/**获取容器状态 */
|
/**获取容器状态 */
|
||||||
export const getContainerStatus = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -162,15 +162,15 @@ export const getContainerStatus = data => {
|
|||||||
};
|
};
|
||||||
/**获取虚拟机状态 */
|
/**获取虚拟机状态 */
|
||||||
export const getInstanceStatus = id => {
|
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 => {
|
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 => {
|
export const openContainer = data => {
|
||||||
return http2.post("/v1/admin/container/open_container", data, {
|
return http2.post("/acs/v1/admin/container/open_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ export const openContainer = data => {
|
|||||||
};
|
};
|
||||||
/**开通虚拟机 */
|
/**开通虚拟机 */
|
||||||
export const openInstance = (id, 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ export const openInstance = (id, data = "") => {
|
|||||||
};
|
};
|
||||||
/**启动容器 */
|
/**启动容器 */
|
||||||
export const startContainer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -194,11 +194,11 @@ export const startContainer = data => {
|
|||||||
};
|
};
|
||||||
/**启动虚拟机 */
|
/**启动虚拟机 */
|
||||||
export const startInstance = 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 => {
|
export const reinstallC = data => {
|
||||||
return http2.post("/v1/admin/container/reinstall_container", data, {
|
return http2.post("/acs/v1/admin/container/reinstall_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ export const reinstallC = data => {
|
|||||||
};
|
};
|
||||||
/**重装虚拟机 */
|
/**重装虚拟机 */
|
||||||
export const reinstallI = (data, id) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ export const reinstallI = (data, id) => {
|
|||||||
|
|
||||||
/**获取容器日志 */
|
/**获取容器日志 */
|
||||||
export const getContainerLog = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -224,13 +224,13 @@ export const getContainerLog = data => {
|
|||||||
/**获取虚拟机操作日志 */
|
/**获取虚拟机操作日志 */
|
||||||
export const getInstanceLog = (id, data) => {
|
export const getInstanceLog = (id, data) => {
|
||||||
return http2.get(
|
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 => {
|
export const restartContainer = data => {
|
||||||
return http2.post("/v1/admin/container/reboot_container", data, {
|
return http2.post("/acs/v1/admin/container/reboot_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -238,12 +238,12 @@ export const restartContainer = data => {
|
|||||||
};
|
};
|
||||||
/**重启虚拟机 */
|
/**重启虚拟机 */
|
||||||
export const restartInstance = 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 => {
|
export const stopContainer = data => {
|
||||||
return http2.post("/v1/admin/container/stop_container", data, {
|
return http2.post("/acs/v1/admin/container/stop_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -251,12 +251,12 @@ export const stopContainer = data => {
|
|||||||
};
|
};
|
||||||
/**停止虚拟机 */
|
/**停止虚拟机 */
|
||||||
export const stopInstance = 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 => {
|
export const deleteContainer = data => {
|
||||||
return http2.post("/v1/admin/container/delete_container", data, {
|
return http2.post("/acs/v1/admin/container/delete_container", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ export const deleteContainer = data => {
|
|||||||
};
|
};
|
||||||
/**删除虚拟机 */
|
/**删除虚拟机 */
|
||||||
export const deleteInstance = (id, 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ export const deleteInstance = (id, data = "") => {
|
|||||||
};
|
};
|
||||||
/**清除容器流量 */
|
/**清除容器流量 */
|
||||||
export const clearContainerTraffic = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ export const clearContainerTraffic = data => {
|
|||||||
};
|
};
|
||||||
/**连接控制台 */
|
/**连接控制台 */
|
||||||
export const connectConsole = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ export const connectConsole = data => {
|
|||||||
};
|
};
|
||||||
/**新增虚拟机 (管理员) */
|
/**新增虚拟机 (管理员) */
|
||||||
export const addInstance = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -296,21 +296,21 @@ export const addInstance = data => {
|
|||||||
};
|
};
|
||||||
/**获取虚拟机控制台 */
|
/**获取虚拟机控制台 */
|
||||||
export const getInstanceConsole = 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 => {
|
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 => {
|
export const getInstanceVolumeList = data => {
|
||||||
return http2.get(
|
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 => {
|
export const addVolume = data => {
|
||||||
return http2.post("/v1/admin/volume/add_volume", data, {
|
return http2.post("/acs/v1/admin/volume/add_volume", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ export const addVolume = data => {
|
|||||||
};
|
};
|
||||||
/**修改卷大小 */
|
/**修改卷大小 */
|
||||||
export const updateVolume = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -326,7 +326,7 @@ export const updateVolume = data => {
|
|||||||
};
|
};
|
||||||
/**删除数据卷 */
|
/**删除数据卷 */
|
||||||
export const deleteVolume = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ export const deleteVolume = data => {
|
|||||||
/**获取容器网络信息 */
|
/**获取容器网络信息 */
|
||||||
export const getNetworkList = data => {
|
export const getNetworkList = data => {
|
||||||
return http2.get(
|
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)
|
if (data.internal_port !== undefined)
|
||||||
params.append("internal_port", data.internal_port.toString());
|
params.append("internal_port", data.internal_port.toString());
|
||||||
return http2.get(
|
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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ export const addNetwork = data => {
|
|||||||
};
|
};
|
||||||
/**创建端口 */
|
/**创建端口 */
|
||||||
export const addPort = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -369,12 +369,12 @@ export const addPort = data => {
|
|||||||
/**获取浮动ip列表 */
|
/**获取浮动ip列表 */
|
||||||
export const getFloatingIpList = data => {
|
export const getFloatingIpList = data => {
|
||||||
return http2.get(
|
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 */
|
/**新增浮动ip */
|
||||||
export const addFloatingIp = data => {
|
export const addFloatingIp = data => {
|
||||||
return http2.post("/v1/admin/floating_ip/add", data, {
|
return http2.post("/acs/v1/admin/floating_ip/add", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ export const addFloatingIp = data => {
|
|||||||
};
|
};
|
||||||
/**批量添加浮动ip */
|
/**批量添加浮动ip */
|
||||||
export const addFloatingIpBatch = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ export const addFloatingIpBatch = data => {
|
|||||||
};
|
};
|
||||||
/**删除浮动ip */
|
/**删除浮动ip */
|
||||||
export const delFloatingIp = data => {
|
export const delFloatingIp = data => {
|
||||||
return http2.post("/v1/admin/floating_ip/delete", data, {
|
return http2.post("/acs/v1/admin/floating_ip/delete", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -400,13 +400,13 @@ export const delFloatingIp = data => {
|
|||||||
/**获取单个用户操作日志 */
|
/**获取单个用户操作日志 */
|
||||||
export const getUserLog = data => {
|
export const getUserLog = data => {
|
||||||
return http2.get(
|
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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -415,28 +415,28 @@ export const editAvatar = data => {
|
|||||||
|
|
||||||
/**获取服务器硬盘信息 */
|
/**获取服务器硬盘信息 */
|
||||||
export const getDiskInfo = 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 => {
|
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 => {
|
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 => {
|
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 = () => {
|
export const getVersion = () => {
|
||||||
return http2.get(`/v1/admin/version`);
|
return http2.get(`/acs/v1/admin/version`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 管理员删除https网络
|
// 管理员删除https网络
|
||||||
export const AdminDelHttps = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -445,7 +445,7 @@ export const AdminDelHttps = data => {
|
|||||||
|
|
||||||
// 管理员添加https网络
|
// 管理员添加https网络
|
||||||
export const AdminAddHttps = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -453,11 +453,11 @@ export const AdminAddHttps = data => {
|
|||||||
};
|
};
|
||||||
/**获取指定端口信息 */
|
/**获取指定端口信息 */
|
||||||
export const getPortInfo = 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 => {
|
export const addVolumeMount = data => {
|
||||||
return http2.post("/v1/admin/volume/add_volume", data, {
|
return http2.post("/acs/v1/admin/volume/add_volume", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -466,17 +466,17 @@ export const addVolumeMount = data => {
|
|||||||
|
|
||||||
/**进入救援系统 */
|
/**进入救援系统 */
|
||||||
export const rescueInstance = id => {
|
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 => {
|
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) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -484,7 +484,7 @@ export const changeInstancePassword = (id, data) => {
|
|||||||
};
|
};
|
||||||
/**修改虚拟机密码(用户) */
|
/**修改虚拟机密码(用户) */
|
||||||
export const changeInstancePasswordUser = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ export const changeInstancePasswordUser = (id, data) => {
|
|||||||
|
|
||||||
/**删除端口 */
|
/**删除端口 */
|
||||||
export const deletePort = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {http2} from "@/utils/request.js";
|
import {http2} from "@/utils/request.js";
|
||||||
/**获取全局配置 */
|
/**获取全局配置 */
|
||||||
export const getSetting = () => {
|
export const getSetting = () => {
|
||||||
return http2.get('/v1/admin/settings/get_settings')
|
return http2.get('/acs/v1/admin/settings/get_settings')
|
||||||
}
|
}
|
||||||
/**变更设置 */
|
/**变更设置 */
|
||||||
export const updateSetting = (data) => {
|
export const updateSetting = (data) => {
|
||||||
return http2.post('/v1/admin/settings/update_settings', data, {
|
return http2.post('/acs/v1/admin/settings/update_settings', data, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export const updateSetting = (data) => {
|
|||||||
}
|
}
|
||||||
/**新增设置 */
|
/**新增设置 */
|
||||||
export const addSetting = (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: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ export const addSetting = (data) => {
|
|||||||
}
|
}
|
||||||
/**删除设置 */
|
/**删除设置 */
|
||||||
export const deleteSetting = (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: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@@ -29,12 +29,12 @@ export const deleteSetting = (data) => {
|
|||||||
}
|
}
|
||||||
/**获取单项配置 */
|
/**获取单项配置 */
|
||||||
export const getOneSetting = (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) => {
|
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(',');
|
const namesParam = data.join(',');
|
||||||
// 将处理后的namesParam放入URL中
|
// 将处理后的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";
|
import {http2} from "@/utils/request.js";
|
||||||
/**获取用户列表 */
|
/**获取用户列表 */
|
||||||
export const ask_update_user_email11 = data => {
|
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验证码 */
|
/**email验证码 */
|
||||||
export const ask_update_user_email = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export const ask_update_user_email = data => {
|
|||||||
};
|
};
|
||||||
/**email修改 */
|
/**email修改 */
|
||||||
export const update_user_email = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ export const update_user_email = data => {
|
|||||||
};
|
};
|
||||||
/**phone验证码 */
|
/**phone验证码 */
|
||||||
export const ask_update_user_phone = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export const ask_update_user_phone = data => {
|
|||||||
};
|
};
|
||||||
/**phone修改 */
|
/**phone修改 */
|
||||||
export const update_user_phone = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export const update_user_phone = data => {
|
|||||||
};
|
};
|
||||||
/**密码修改 */
|
/**密码修改 */
|
||||||
export const update_user_password = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-67
@@ -4,12 +4,12 @@ import {http2} from "@/utils/request.js";
|
|||||||
|
|
||||||
// 获取图像验证码
|
// 获取图像验证码
|
||||||
export const Captch = data => {
|
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 => {
|
export const getLogin = data => {
|
||||||
return http2.post("/v1/user/login", data, {
|
return http2.post("/acs/v1/user/login", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -24,12 +24,12 @@ export const getLogin = data => {
|
|||||||
/**获取用户列表 */
|
/**获取用户列表 */
|
||||||
export const getUserList = data => {
|
export const getUserList = data => {
|
||||||
return http2.get(
|
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 => {
|
export const addUser = data => {
|
||||||
return http2.post("/v1/admin/users/add_user", data, {
|
return http2.post("/acs/v1/admin/users/add_user", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export const addUser = data => {
|
|||||||
};
|
};
|
||||||
/**编辑用户信息 */
|
/**编辑用户信息 */
|
||||||
export const editUser = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ export const editUser = data => {
|
|||||||
};
|
};
|
||||||
/**修改用户密码 */
|
/**修改用户密码 */
|
||||||
export const editPassword = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export const editPassword = data => {
|
|||||||
};
|
};
|
||||||
/**删除用户 */
|
/**删除用户 */
|
||||||
export const deleteUser = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ export const deleteUser = data => {
|
|||||||
};
|
};
|
||||||
/**查询单个用户 */
|
/**查询单个用户 */
|
||||||
export const userDetail = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export const userDetail = data => {
|
|||||||
};
|
};
|
||||||
/**修改用户余额 */
|
/**修改用户余额 */
|
||||||
export const editBalance = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -79,12 +79,12 @@ export const editBalance = data => {
|
|||||||
export const getUserServer = (data = {}) => {
|
export const getUserServer = (data = {}) => {
|
||||||
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
const serverType = data.server_type || "dockerContainer"; // 设置默认值
|
||||||
return http2.get(
|
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 => {
|
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) {
|
if (data.key) {
|
||||||
url += `&key=${data.key}`;
|
url += `&key=${data.key}`;
|
||||||
}
|
}
|
||||||
@@ -95,35 +95,35 @@ export const getVirtualList = data => {
|
|||||||
};
|
};
|
||||||
/**用户获取服务器套餐 */
|
/**用户获取服务器套餐 */
|
||||||
export const getUserPackage = 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 => {
|
export const getUserContainer = data => {
|
||||||
return http2.get(
|
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 => {
|
export const getUserContainerD = data => {
|
||||||
return http2.get(
|
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 = () => {
|
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 = () => {
|
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 = () => {
|
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 => {
|
export const realName = data => {
|
||||||
return http2.post("/v1/external/real_name", data, {
|
return http2.post("/acs/v1/external/real_name", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ export const realName = data => {
|
|||||||
};
|
};
|
||||||
/**发送手机验证码 */
|
/**发送手机验证码 */
|
||||||
export const sendCode = data => {
|
export const sendCode = data => {
|
||||||
return http2.post("/v1/external/send_message", data, {
|
return http2.post("/acs/v1/external/send_message", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export const sendCode = data => {
|
|||||||
};
|
};
|
||||||
/**发送邮箱验证码 */
|
/**发送邮箱验证码 */
|
||||||
export const sendEmailCode = data => {
|
export const sendEmailCode = data => {
|
||||||
return http2.post("/v1/external/send_email", data, {
|
return http2.post("/acs/v1/external/send_email", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ export const sendEmailCode = data => {
|
|||||||
};
|
};
|
||||||
/**用户注册 */
|
/**用户注册 */
|
||||||
export const register = data => {
|
export const register = data => {
|
||||||
return http2.post("/v1/user/register", data, {
|
return http2.post("/acs/v1/user/register", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ export const register = data => {
|
|||||||
};
|
};
|
||||||
/**手机号修改校证码 */
|
/**手机号修改校证码 */
|
||||||
export const CodePhone = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ export const CodePhone = data => {
|
|||||||
};
|
};
|
||||||
/**修改手机号码 */
|
/**修改手机号码 */
|
||||||
export const SetPhone = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ export const SetPhone = data => {
|
|||||||
};
|
};
|
||||||
/**邮箱修改校证码 */
|
/**邮箱修改校证码 */
|
||||||
export const CodeEmail = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ export const CodeEmail = data => {
|
|||||||
};
|
};
|
||||||
/**修改邮箱 */
|
/**修改邮箱 */
|
||||||
export const SetEmail = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ export const SetEmail = data => {
|
|||||||
};
|
};
|
||||||
/**上传头像 */
|
/**上传头像 */
|
||||||
export const uploadAvatar = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ export const uploadAvatar = data => {
|
|||||||
};
|
};
|
||||||
/**手机号忘记密码 */
|
/**手机号忘记密码 */
|
||||||
export const forgetphone = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ export const forgetphone = data => {
|
|||||||
|
|
||||||
/**邮箱忘记密码 */
|
/**邮箱忘记密码 */
|
||||||
export const forgetemail = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ export const forgetemail = data => {
|
|||||||
};
|
};
|
||||||
/**管理员全局搜索 */
|
/**管理员全局搜索 */
|
||||||
export const Find = data => {
|
export const Find = data => {
|
||||||
return http2.post("/v1/admin/search", data, {
|
return http2.post("/acs/v1/admin/search", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ export const Find = data => {
|
|||||||
};
|
};
|
||||||
// 管理员删除容器网络
|
// 管理员删除容器网络
|
||||||
export const delContainer = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ export const delContainer = data => {
|
|||||||
};
|
};
|
||||||
// 删除端口
|
// 删除端口
|
||||||
export const delPort = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ export const delPort = data => {
|
|||||||
};
|
};
|
||||||
// 自定义容器价格
|
// 自定义容器价格
|
||||||
export const Containerpay = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ export const Containerpay = data => {
|
|||||||
};
|
};
|
||||||
// 修改虚拟机续费价格
|
// 修改虚拟机续费价格
|
||||||
export const Containerpaytime = (data, id) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ export const Containerpaytime = (data, id) => {
|
|||||||
};
|
};
|
||||||
// 自定义容器到期时间
|
// 自定义容器到期时间
|
||||||
export const Containertime = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ export const Containertime = data => {
|
|||||||
};
|
};
|
||||||
// 修改虚拟机续到期时间
|
// 修改虚拟机续到期时间
|
||||||
export const Containertimetime = (data, id) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ export const Containertimetime = (data, id) => {
|
|||||||
};
|
};
|
||||||
// 修改虚拟机信息
|
// 修改虚拟机信息
|
||||||
export const editContainer = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ export const editContainer = (data, id) => {
|
|||||||
/** 容器操作 */
|
/** 容器操作 */
|
||||||
export const startUserContainer = (type, id) => {
|
export const startUserContainer = (type, id) => {
|
||||||
return http2.post(
|
return http2.post(
|
||||||
"/v1/user/container/" + type,
|
"/acs/v1/user/container/" + type,
|
||||||
{
|
{
|
||||||
container_id: id
|
container_id: id
|
||||||
},
|
},
|
||||||
@@ -293,7 +293,7 @@ export const startUserContainer = (type, id) => {
|
|||||||
};
|
};
|
||||||
/**用户容器退款 */
|
/**用户容器退款 */
|
||||||
export const backUserContainer = data => {
|
export const backUserContainer = data => {
|
||||||
return http2.post("/v1/user/container/delete", data, {
|
return http2.post("/acs/v1/user/container/delete", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ export const backUserContainer = data => {
|
|||||||
};
|
};
|
||||||
/**重装容器 */
|
/**重装容器 */
|
||||||
export const reinContainer = data => {
|
export const reinContainer = data => {
|
||||||
return http2.post("/v1/user/container/reinstall", data, {
|
return http2.post("/acs/v1/user/container/reinstall", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ export const reinContainer = data => {
|
|||||||
};
|
};
|
||||||
/**重装虚拟机 */
|
/**重装虚拟机 */
|
||||||
export const reinVmContainer = (id, 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ export const reinVmContainer = (id, data) => {
|
|||||||
/** 容器操作 */
|
/** 容器操作 */
|
||||||
export const startAdminContainer = (type, id) => {
|
export const startAdminContainer = (type, id) => {
|
||||||
return http2.post(
|
return http2.post(
|
||||||
"/v1/admin/container/" + type,
|
"/acs/v1/admin/container/" + type,
|
||||||
{
|
{
|
||||||
container_id: id
|
container_id: id
|
||||||
},
|
},
|
||||||
@@ -331,7 +331,7 @@ export const startAdminContainer = (type, id) => {
|
|||||||
};
|
};
|
||||||
/** 容器操作 */
|
/** 容器操作 */
|
||||||
export const procedureUpdateContainerRenew = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -339,15 +339,15 @@ export const procedureUpdateContainerRenew = data => {
|
|||||||
};
|
};
|
||||||
/**获取容器完整信息 */
|
/**获取容器完整信息 */
|
||||||
export const getContainerDetail = id => {
|
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 => {
|
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 => {
|
export const containerLog = data => {
|
||||||
return http2.post("/v1/user/container/logs", data, {
|
return http2.post("/acs/v1/user/container/logs", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -356,12 +356,12 @@ export const containerLog = data => {
|
|||||||
/**虚拟机操作日志 */
|
/**虚拟机操作日志 */
|
||||||
export const vmLog = data => {
|
export const vmLog = data => {
|
||||||
return http2.get(
|
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 => {
|
export const getContainerStatus = data => {
|
||||||
return http2.post(`/v1/user/container/status`, data, {
|
return http2.post(`/acs/v1/user/container/status`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -369,11 +369,11 @@ export const getContainerStatus = data => {
|
|||||||
};
|
};
|
||||||
/**获取虚拟机状态 */
|
/**获取虚拟机状态 */
|
||||||
export const getVmStatus = id => {
|
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 => {
|
export const getContainerLog = data => {
|
||||||
return http2.post(`/v1/user/container/run_logs`, data, {
|
return http2.post(`/acs/v1/user/container/run_logs`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ export const getContainerLog = data => {
|
|||||||
};
|
};
|
||||||
/**获取容器购买网络订单 */
|
/**获取容器购买网络订单 */
|
||||||
export const getContainerList = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,7 @@ export const getContainerList = data => {
|
|||||||
};
|
};
|
||||||
/**计算容器网络价格 */
|
/**计算容器网络价格 */
|
||||||
export const getContainerPrice = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -397,33 +397,33 @@ export const getContainerPrice = data => {
|
|||||||
};
|
};
|
||||||
/** 启动虚拟机 */
|
/** 启动虚拟机 */
|
||||||
export const start_vm = id => {
|
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 => {
|
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 => {
|
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 => {
|
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 => {
|
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 => {
|
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
|
// ******************************* new
|
||||||
/** 提交充值订单 */
|
/** 提交充值订单 */
|
||||||
export const user_update_container_recharge = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -431,7 +431,7 @@ export const user_update_container_recharge = data => {
|
|||||||
};
|
};
|
||||||
/** 提交容器订单 */
|
/** 提交容器订单 */
|
||||||
export const user_update_plan_info = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -439,7 +439,7 @@ export const user_update_plan_info = data => {
|
|||||||
};
|
};
|
||||||
/** 提交虚拟机订单 */
|
/** 提交虚拟机订单 */
|
||||||
export const user_update_vm_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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -447,11 +447,11 @@ export const user_update_vm_info = data => {
|
|||||||
};
|
};
|
||||||
/**获取订单简略信息 */
|
/**获取订单简略信息 */
|
||||||
export const getOrderDetail = id => {
|
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 => {
|
export const pay_request = data => {
|
||||||
return http2.post("/v1/external/pay", data, {
|
return http2.post("/acs/v1/external/pay", data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -459,7 +459,7 @@ export const pay_request = data => {
|
|||||||
};
|
};
|
||||||
/**用户删除容器网络 */
|
/**用户删除容器网络 */
|
||||||
export const deleteConNet = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -468,7 +468,7 @@ export const deleteConNet = data => {
|
|||||||
|
|
||||||
// 添加https
|
// 添加https
|
||||||
export const additionHttp = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -477,7 +477,7 @@ export const additionHttp = data => {
|
|||||||
|
|
||||||
// 删除https
|
// 删除https
|
||||||
export const DelHttp = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -485,7 +485,7 @@ export const DelHttp = data => {
|
|||||||
};
|
};
|
||||||
/**获取新增虚拟机端口价格 */
|
/**获取新增虚拟机端口价格 */
|
||||||
export const getVmPortPrice = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ export const getVmPortPrice = data => {
|
|||||||
};
|
};
|
||||||
/**提交新增虚拟机端口订单 */
|
/**提交新增虚拟机端口订单 */
|
||||||
export const addVmPort = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-22
@@ -3,7 +3,7 @@ import {http2} from "@/utils/request.js";
|
|||||||
|
|
||||||
/**获取虚拟机列表 */
|
/**获取虚拟机列表 */
|
||||||
export const getVirtualList = data => {
|
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) {
|
if (data.key) {
|
||||||
url += `&key=${data.key}`;
|
url += `&key=${data.key}`;
|
||||||
}
|
}
|
||||||
@@ -18,12 +18,12 @@ export const getVirtualList = data => {
|
|||||||
|
|
||||||
/**新增虚拟机 */
|
/**新增虚拟机 */
|
||||||
export const addVirtual = 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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -32,18 +32,18 @@ export const migrate_disk = data => {
|
|||||||
|
|
||||||
/**获取虚拟机访问控制列表 */
|
/**获取虚拟机访问控制列表 */
|
||||||
export const getVirtualAccessList = 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);
|
return http2.get(url);
|
||||||
};
|
};
|
||||||
/**获取虚拟机访问控制列表(用户) */
|
/**获取虚拟机访问控制列表(用户) */
|
||||||
export const getUserAccessList = data => {
|
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);
|
return http2.get(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**创建访问控制 */
|
/**创建访问控制 */
|
||||||
export const createAccessControl = data => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ export const createAccessControl = data => {
|
|||||||
};
|
};
|
||||||
/**创建访问控制(用户) */
|
/**创建访问控制(用户) */
|
||||||
export const createUserAccessControl = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ export const createUserAccessControl = data => {
|
|||||||
};
|
};
|
||||||
/**删除访问控制 */
|
/**删除访问控制 */
|
||||||
export const deleteAccessControl = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ export const deleteAccessControl = data => {
|
|||||||
};
|
};
|
||||||
/**删除访问控制(用户) */
|
/**删除访问控制(用户) */
|
||||||
export const deleteUserAccessControl = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -76,17 +76,17 @@ export const deleteUserAccessControl = data => {
|
|||||||
|
|
||||||
/**获取虚拟机快照列表 */
|
/**获取虚拟机快照列表 */
|
||||||
export const getSnapshotList = 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);
|
return http2.get(url);
|
||||||
};
|
};
|
||||||
/**获取虚拟机快照列表(用户) */
|
/**获取虚拟机快照列表(用户) */
|
||||||
export const getUserSnapshotList = data => {
|
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);
|
return http2.get(url);
|
||||||
};
|
};
|
||||||
/**创建虚拟机快照 */
|
/**创建虚拟机快照 */
|
||||||
export const createSnapshot = (data, id) => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export const createSnapshot = (data, id) => {
|
|||||||
};
|
};
|
||||||
/**创建虚拟机快照(用户) */
|
/**创建虚拟机快照(用户) */
|
||||||
export const createUserSnapshot = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ export const createUserSnapshot = (data, id) => {
|
|||||||
|
|
||||||
/**删除虚拟机快照 */
|
/**删除虚拟机快照 */
|
||||||
export const deleteSnapshot = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ export const deleteSnapshot = (data, id) => {
|
|||||||
|
|
||||||
/**恢复虚拟机快照 */
|
/**恢复虚拟机快照 */
|
||||||
export const recoverSnapshot = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ export const recoverSnapshot = (data, id) => {
|
|||||||
};
|
};
|
||||||
/**恢复虚拟机快照(用户) */
|
/**恢复虚拟机快照(用户) */
|
||||||
export const recoverUserSnapshot = (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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -129,24 +129,24 @@ export const recoverUserSnapshot = (data, id) => {
|
|||||||
/**获取实时监控 */
|
/**获取实时监控 */
|
||||||
export const getVirtualLog = data => {
|
export const getVirtualLog = data => {
|
||||||
return http2.get(
|
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 => {
|
export const getUserVirtualLog = data => {
|
||||||
return http2.get(
|
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 => {
|
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 => {
|
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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ export const submitSnapshotOrder = data => {
|
|||||||
};
|
};
|
||||||
// 获取购买虚拟机数据卷价格
|
// 获取购买虚拟机数据卷价格
|
||||||
export const getVolumePrice = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ export const getVolumePrice = data => {
|
|||||||
|
|
||||||
// 提交虚拟机数据卷订单
|
// 提交虚拟机数据卷订单
|
||||||
export const submitVolumeOrder = 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: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"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 axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
import {getRefreshToken,refreshAccessToken} from "@/api/login.js";
|
||||||
// 基础URL
|
import { baseUrl, acsBaseUrl, noAuthUrls as noAuthUrlList, requestTimeout, acsRequestTimeout, TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js'
|
||||||
const baseUrl = 'https://apiservertest.s1f.ren'
|
|
||||||
// const baseUrl = 'https://cloudapi.007yjs.com'
|
|
||||||
|
|
||||||
// 检查URL是否需要认证
|
// 检查URL是否需要认证
|
||||||
const urlNeedAuth = (url) => {
|
const urlNeedAuth = (url) => {
|
||||||
// 这里可以添加不需要认证的URL列表
|
return !noAuthUrlList.some(noAuthUrl => url.includes(noAuthUrl))
|
||||||
const noAuthUrls = ['/v1/user/login', '/v1/user/check/get_code_img', '/v1/user/register']
|
|
||||||
return !noAuthUrls.some(noAuthUrl => url.includes(noAuthUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查token是否过期
|
// 检查token是否过期
|
||||||
const isTokenExpired = () => {
|
const isTokenExpired = () => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
|
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
|
||||||
if (!token) return true
|
if (!token) return true
|
||||||
|
|
||||||
// 这里可以添加token过期检查逻辑,如果有JWT可以解析它
|
// 检查过期时间
|
||||||
// 简单实现,仅检查token是否存在
|
if (expire) {
|
||||||
return false
|
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 {
|
class Request {
|
||||||
@@ -37,7 +115,7 @@ class Request {
|
|||||||
(config) => {
|
(config) => {
|
||||||
// 在发送请求之前做些什么
|
// 在发送请求之前做些什么
|
||||||
// 例如:添加 token
|
// 例如:添加 token
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
@@ -106,33 +184,78 @@ class Request {
|
|||||||
// 创建默认实例
|
// 创建默认实例
|
||||||
const request = new Request({
|
const request = new Request({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
timeout: 50000,
|
timeout: requestTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const mainUrl = baseUrl + "/acs"
|
export const mainUrl = baseUrl + '/acs'
|
||||||
|
export const baseURL = baseUrl
|
||||||
|
|
||||||
export const http2 = axios.create({
|
export const http2 = axios.create({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
timeout: 30000,
|
timeout: acsRequestTimeout,
|
||||||
headers: {},
|
headers: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
http2.interceptors.request.use(config => {
|
http2.interceptors.request.use(async config => {
|
||||||
const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
if(urlNeedAuth(config.url) && isTokenExpired()){
|
|
||||||
if (token){
|
// 检查是否需要认证
|
||||||
localStorage.removeItem('token');
|
if (urlNeedAuth(config.url)) {
|
||||||
ElMessage.warning('登陆过期,请重新登陆')
|
// 检查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}`;
|
// 不需要认证的请求,不添加token
|
||||||
|
|
||||||
config.url = '/acs' + config.url
|
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -146,7 +269,7 @@ http2.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
const { status } = error.response;
|
const { status } = error.response;
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
ElMessage.warning('登陆过期,请重新登陆')
|
ElMessage.warning('登陆过期,请重新登陆')
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
|
|||||||
+206
-1
@@ -1,4 +1,209 @@
|
|||||||
export const FileName = (data) =>{
|
export const FileName = (data) =>{
|
||||||
let name = data.split("/").pop()
|
let name = data.split("/").pop()
|
||||||
return name
|
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 = () => {
|
const handleLogin = () => {
|
||||||
loginFormRef.value?.validate(async valid =>{
|
loginFormRef.value?.validate(async valid =>{
|
||||||
window.localStorage.removeItem('token')
|
window.localStorage.removeItem('token')
|
||||||
|
window.localStorage.removeItem('tokenExpire')
|
||||||
|
window.localStorage.removeItem('userInfo')
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
let resp = await userLogin(loginForm.username, loginForm.password)
|
let resp = await userLogin(loginForm.username, loginForm.password)
|
||||||
|
console.log("login:",resp)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
if(resp.code === 200){
|
if(resp.code === 200){
|
||||||
|
// 保存token和过期时间
|
||||||
window.localStorage.setItem('token',resp.data.token)
|
window.localStorage.setItem('token', resp.data.token)
|
||||||
|
if (resp.data.expire) {
|
||||||
|
window.localStorage.setItem('tokenExpire', resp.data.expire.toString())
|
||||||
|
}
|
||||||
|
|
||||||
let userInfo = await getUserInfo()
|
let userInfo = await getUserInfo()
|
||||||
if(userInfo.data.is_admin){
|
if(userInfo.data.is_admin){
|
||||||
|
// 保存用户信息到localStorage
|
||||||
|
window.localStorage.setItem('userInfo', JSON.stringify(userInfo.data))
|
||||||
await router.push('/dashboard')
|
await router.push('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('你不是管理员,不能登陆到后台控制面板')
|
ElMessage.warning('你不是管理员,不能登陆到后台控制面板')
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="guacamole-container">
|
<div class="guacamole-container">
|
||||||
<!-- 页面标题和操作按钮 -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="left">
|
|
||||||
<h2 class="title">远程桌面网关管理</h2>
|
|
||||||
<el-tag type="info" effect="plain" class="count-tag">共 {{ guacamoleStats.total }} 个配置</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">添加配置</el-button>
|
|
||||||
<el-button @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<div class="stats-panel">
|
<div class="stats-panel">
|
||||||
<div class="stat-card total-card">
|
<div class="stat-card total-card">
|
||||||
@@ -37,79 +25,91 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索和筛选 -->
|
<el-card class="main-container" shadow="never">
|
||||||
<div class="filter-section">
|
<!-- 搜索和筛选 -->
|
||||||
<el-input
|
<div class="filter-section">
|
||||||
v-model="filterForm.url"
|
<div class="filter-content">
|
||||||
placeholder="搜索 Guacamole URL"
|
<el-form :inline="true" :model="filterForm" class="search-form">
|
||||||
prefix-icon="Search"
|
<el-form-item>
|
||||||
clearable
|
<el-input
|
||||||
@keyup.enter="handleSearch"
|
v-model="filterForm.url"
|
||||||
class="search-input"
|
placeholder="搜索 Guacamole URL"
|
||||||
/>
|
prefix-icon="Search"
|
||||||
<div class="filter-actions">
|
clearable
|
||||||
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
@keyup.enter="handleSearch"
|
||||||
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
|
style="width: 300px"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
<!-- Guacamole 配置列表 -->
|
<el-button type="primary" @click="handleSearch">
|
||||||
<div class="table-container">
|
<el-icon><search /></el-icon>搜索
|
||||||
<el-table
|
</el-button>
|
||||||
v-loading="loading"
|
<el-button @click="resetFilter">
|
||||||
:data="guacamoleData"
|
<el-icon><refresh /></el-icon>重置
|
||||||
border
|
</el-button>
|
||||||
stripe
|
</el-form-item>
|
||||||
style="width: 100%"
|
</el-form>
|
||||||
table-layout="auto"
|
<div class="action-bar">
|
||||||
class="guacamole-table"
|
<el-button type="primary" @click="handleAdd">
|
||||||
>
|
<el-icon><plus /></el-icon>添加配置
|
||||||
<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-button>
|
</el-button>
|
||||||
</template>
|
<el-button @click="handleRefresh">
|
||||||
</el-table-column>
|
<el-icon><refresh /></el-icon>刷新
|
||||||
<el-table-column label="创建时间" width="180" align="center">
|
</el-button>
|
||||||
<template #default="scope">
|
</div>
|
||||||
{{ formatDate(scope.row.created_at) }}
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- Guacamole 配置列表 -->
|
||||||
<div class="pagination-container">
|
<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
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -119,9 +119,10 @@
|
|||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
background
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑配置对话框 -->
|
<!-- 添加/编辑配置对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -510,42 +511,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.guacamole-container {
|
.guacamole-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面标题样式 */
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count-tag {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片 */
|
/* 统计卡片 */
|
||||||
@@ -558,18 +524,18 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #e1e8ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
@@ -608,60 +574,64 @@ onMounted(async () => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索和筛选部分 */
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
justify-content: space-between;
|
||||||
margin-bottom: 24px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
padding: 16px 20px;
|
||||||
padding: 16px;
|
gap: 20px;
|
||||||
border-radius: 8px;
|
flex-wrap: wrap;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格容器 */
|
.search-form :deep(.el-form-item) {
|
||||||
.table-container {
|
margin-bottom: 0;
|
||||||
background: white;
|
margin-right: 12px;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guacamole-table {
|
.action-bar {
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 操作按钮 */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 8px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分页 */
|
.table-section {
|
||||||
.pagination-container {
|
padding: 0;
|
||||||
display: flex;
|
}
|
||||||
justify-content: flex-end;
|
|
||||||
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单样式 */
|
/* 表单样式 */
|
||||||
@@ -676,14 +646,6 @@ onMounted(async () => {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px dashed #ebeef5;
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框底部 */
|
/* 对话框底部 */
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -696,6 +658,37 @@ onMounted(async () => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: none;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table td) {
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table tr:hover > td) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
@@ -708,17 +701,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header .actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -727,13 +709,18 @@ onMounted(async () => {
|
|||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.filter-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-form {
|
||||||
max-width: none;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,61 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-images-container" v-loading="loading">
|
<div class="container-images-container" v-loading="loading">
|
||||||
<div class="page-header">
|
<el-card class="main-container" shadow="never">
|
||||||
<h2>容器镜像</h2>
|
<!-- 搜索和操作栏 -->
|
||||||
<div class="header-actions">
|
<div class="filter-section">
|
||||||
<el-button type="primary" @click="handleRefresh">
|
<div class="filter-content">
|
||||||
<el-icon><refresh /></el-icon>刷新
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
</el-button>
|
<el-form-item label="服务器">
|
||||||
</div>
|
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
||||||
</div>
|
<el-option
|
||||||
|
v-for="server in serverList"
|
||||||
<!-- 服务器选择和搜索区域 -->
|
:key="server.server_id"
|
||||||
<el-card class="search-card">
|
:label="server.name"
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
:value="server.server_id"
|
||||||
<el-form-item label="服务器">
|
/>
|
||||||
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
|
</el-select>
|
||||||
<el-option
|
</el-form-item>
|
||||||
v-for="server in serverList"
|
<el-form-item label="镜像名称">
|
||||||
:key="server.server_id"
|
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||||
:label="server.name"
|
</el-form-item>
|
||||||
:value="server.server_id"
|
<el-form-item>
|
||||||
/>
|
<el-button type="primary" @click="handleSearch">
|
||||||
</el-select>
|
<el-icon><search /></el-icon>搜索
|
||||||
</el-form-item>
|
</el-button>
|
||||||
<el-form-item label="镜像名称">
|
<el-button @click="resetSearch">
|
||||||
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
|
<el-icon><refresh /></el-icon>重置
|
||||||
</el-form-item>
|
</el-button>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch">
|
</el-form>
|
||||||
<el-icon><search /></el-icon>搜索
|
<div class="action-bar">
|
||||||
</el-button>
|
<el-button type="primary" @click="handleRefresh">
|
||||||
<el-button @click="resetSearch">
|
<el-icon><refresh /></el-icon>刷新
|
||||||
<el-icon><refresh /></el-icon>重置
|
</el-button>
|
||||||
</el-button>
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
</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
|
<el-table
|
||||||
:data="currentMirrorList"
|
:data="currentMirrorList"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="image_id"
|
row-key="image_id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column label="镜像信息" min-width="250">
|
<el-table-column label="镜像信息" min-width="250">
|
||||||
@@ -65,7 +63,7 @@
|
|||||||
<div class="image-info-content">
|
<div class="image-info-content">
|
||||||
<div class="image-name-row">
|
<div class="image-name-row">
|
||||||
<span class="table-image-name">{{ scope.row.name }}</span>
|
<span class="table-image-name">{{ scope.row.name }}</span>
|
||||||
<el-tag>{{ scope.row.tag || '无标签' }}</el-tag>
|
<el-tag size="small">{{ scope.row.tag || '无标签' }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
|
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,17 +102,17 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
<el-pagination
|
||||||
<el-pagination
|
v-model:current-page="currentPage"
|
||||||
v-model:current-page="currentPage"
|
:page-size="10"
|
||||||
:page-size="10"
|
:total="total"
|
||||||
:total="total"
|
layout="prev, pager, next"
|
||||||
layout="prev, pager, next"
|
@current-change="handleCurrentPageChange"
|
||||||
@current-change="handleCurrentPageChange"
|
background
|
||||||
/>
|
class="pagination"
|
||||||
</div>
|
/>
|
||||||
</el-card>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 镜像详情对话框 -->
|
<!-- 镜像详情对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -201,12 +199,6 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- <el-form-item label="分类ID" prop="class_id">
|
|
||||||
<el-input v-model="form.class_id" placeholder="请输入分类ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分类名称" prop="class_name">
|
|
||||||
<el-input v-model="form.class_name" placeholder="请输入分类名称" />
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item label="图标">
|
<el-form-item label="图标">
|
||||||
<div class="image-icon-upload">
|
<div class="image-icon-upload">
|
||||||
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
|
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
|
||||||
@@ -674,7 +666,7 @@ const toLoad = async (data) => {
|
|||||||
})
|
})
|
||||||
form.server_id = data
|
form.server_id = data
|
||||||
nowserver_id.value = 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 => {
|
planlist.value = res.data.data.map(item => {
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -756,7 +748,7 @@ const fetchCategoryList = async (serverId) => {
|
|||||||
// 编辑镜像
|
// 编辑镜像
|
||||||
const handleEdit = async (data) => {
|
const handleEdit = async (data) => {
|
||||||
try {
|
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) {
|
if (res.data && res.data.data) {
|
||||||
planlist.value = res.data.data.map(item => {
|
planlist.value = res.data.data.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -882,7 +874,7 @@ const getit = async () => {
|
|||||||
|
|
||||||
// 选择图片
|
// 选择图片
|
||||||
const picPagin = reactive({
|
const picPagin = reactive({
|
||||||
count: 50,
|
count: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
key: '',
|
key: '',
|
||||||
user_type: 1
|
user_type: 1
|
||||||
@@ -986,65 +978,65 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container-images-container {
|
.container-images-container {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
padding: 16px 20px;
|
||||||
background: #fff;
|
gap: 20px;
|
||||||
padding: 16px 24px;
|
flex-wrap: wrap;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-section {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-header {
|
.server-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 16px 24px;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-header h3 {
|
.server-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1055,7 +1047,7 @@ onMounted(() => {
|
|||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
background-color: #409EFF;
|
background-color: #409EFF;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@@ -1066,12 +1058,6 @@ onMounted(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-cell {
|
.image-info-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1118,11 +1104,12 @@ onMounted(() => {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情对话框样式 */
|
/* 详情对话框样式 */
|
||||||
@@ -1263,75 +1250,34 @@ onMounted(() => {
|
|||||||
background-color: #ecf5ff;
|
background-color: #ecf5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话框样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-dialog__header) {
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__footer) {
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f5f7fa;
|
background: #f8f9fa !important;
|
||||||
color: #606266;
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row:hover) {
|
:deep(.el-table td) {
|
||||||
background-color: #ecf5ff !important;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link) {
|
:deep(.el-table tr:hover > td) {
|
||||||
padding: 4px 8px;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link):hover {
|
:deep(.el-card__body) {
|
||||||
background-color: #f0f2f5;
|
padding: 0;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.server-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-cell {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-info-content {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-image-logo {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,107 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-categories-container" v-loading="loading">
|
<div class="image-categories-container" v-loading="loading">
|
||||||
<!-- 页面标题 -->
|
<el-card class="main-container" shadow="never">
|
||||||
<div class="page-header">
|
<!-- 搜索和操作栏 -->
|
||||||
<h2>镜像分类管理</h2>
|
<div class="filter-section">
|
||||||
<p class="page-description">管理不同服务器下的镜像分类</p>
|
<div class="filter-content">
|
||||||
</div>
|
<el-form :inline="true" class="search-form">
|
||||||
|
<el-form-item label="服务器">
|
||||||
<!-- 操作栏 -->
|
<el-select
|
||||||
<el-card class="search-card">
|
v-model="selectedServer"
|
||||||
<el-form :inline="true" class="search-form">
|
placeholder="请选择服务器"
|
||||||
<el-form-item label="服务器">
|
clearable
|
||||||
<el-select
|
@change="handleServerChange"
|
||||||
v-model="selectedServer"
|
style="width: 220px"
|
||||||
placeholder="请选择服务器"
|
>
|
||||||
clearable
|
<el-option
|
||||||
@change="handleServerChange"
|
v-for="item in serverList"
|
||||||
style="width: 220px"
|
:key="item.server_id"
|
||||||
>
|
:label="item.name"
|
||||||
<el-option
|
:value="item.server_id"
|
||||||
v-for="item in serverList"
|
/>
|
||||||
:key="item.server_id"
|
</el-select>
|
||||||
:label="item.name"
|
</el-form-item>
|
||||||
:value="item.server_id"
|
<el-form-item label="分类名称">
|
||||||
/>
|
<el-input
|
||||||
</el-select>
|
v-model="searchKey"
|
||||||
</el-form-item>
|
placeholder="搜索分类名称"
|
||||||
<el-form-item label="分类名称">
|
clearable
|
||||||
<el-input
|
@input="handleSearch"
|
||||||
v-model="searchKey"
|
style="width: 200px"
|
||||||
placeholder="搜索分类名称"
|
/>
|
||||||
clearable
|
</el-form-item>
|
||||||
@input="handleSearch"
|
<el-form-item>
|
||||||
/>
|
<el-button type="primary" @click="handleSearch">
|
||||||
</el-form-item>
|
<el-icon><search /></el-icon>搜索
|
||||||
<el-form-item>
|
</el-button>
|
||||||
<el-button
|
</el-form-item>
|
||||||
type="primary"
|
</el-form>
|
||||||
@click="handleAddCategory"
|
<div class="action-bar">
|
||||||
: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-button
|
<el-button
|
||||||
type="success"
|
type="primary"
|
||||||
link
|
@click="handleAddCategory"
|
||||||
@click="handleEditCategory(scope.row)"
|
:disabled="!selectedServer"
|
||||||
>
|
>
|
||||||
<el-icon><edit /></el-icon>编辑
|
<el-icon><plus /></el-icon>添加分类
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
</el-table>
|
</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
|
<el-pagination
|
||||||
background
|
background
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
layout="total, sizes, prev, pager, next"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -264,7 +262,7 @@ const categoryRules = {
|
|||||||
// 素材库相关
|
// 素材库相关
|
||||||
const picSwitch = ref(false)
|
const picSwitch = ref(false)
|
||||||
const picPagin = reactive({
|
const picPagin = reactive({
|
||||||
count: 50,
|
count: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
key: '',
|
key: '',
|
||||||
user_type: 1
|
user_type: 1
|
||||||
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.image-categories-container {
|
.image-categories-container {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
margin-bottom: 24px;
|
align-items: center;
|
||||||
background: #fff;
|
padding: 16px 20px;
|
||||||
padding: 16px 24px;
|
gap: 20px;
|
||||||
border-radius: 8px;
|
flex-wrap: wrap;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #303133;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-description {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 0;
|
||||||
border-radius: 8px;
|
margin-right: 12px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 16px;
|
|
||||||
padding: 0 16px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图片上传区域样式 */
|
/* 图片上传区域样式 */
|
||||||
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
|
|||||||
background-color: #ecf5ff;
|
background-color: #ecf5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话框样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-dialog__header) {
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__footer) {
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
overflow: hidden;
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table th) {
|
:deep(.el-table th) {
|
||||||
background-color: #f5f7fa;
|
background: #f8f9fa !important;
|
||||||
color: #606266;
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
color: #2c3e50;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row:hover) {
|
:deep(.el-table td) {
|
||||||
background-color: #ecf5ff !important;
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link) {
|
:deep(.el-table tr:hover > td) {
|
||||||
padding: 4px 8px;
|
background-color: #f8f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--link):hover {
|
:deep(.el-card__body) {
|
||||||
background-color: #f0f2f5;
|
padding: 0;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.image-icon-upload {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-buttons {
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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>
|
<template>
|
||||||
<div class="image-requests-container">
|
<div class="image-requests-container">
|
||||||
<div class="page-header">
|
<el-card class="main-container" shadow="never">
|
||||||
<h2>申请镜像</h2>
|
<!-- 搜索和操作栏 -->
|
||||||
<div class="header-actions">
|
<div class="filter-section">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<div class="filter-content">
|
||||||
<el-icon><plus /></el-icon>申请镜像
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
</el-button>
|
<el-form-item label="镜像名称">
|
||||||
<el-button @click="handleRefresh">
|
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
|
||||||
<el-icon><refresh /></el-icon>刷新
|
</el-form-item>
|
||||||
</el-button>
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<el-alert
|
<el-alert
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
class="info-alert"
|
class="info-alert"
|
||||||
>
|
style="margin: 20px 20px 0; width: auto;"
|
||||||
<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-table-column prop="id" label="申请ID" width="150" align="center" />
|
<template #title>
|
||||||
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
|
申请的镜像需要经过安全审核,审核通过后可在创建云电脑或容器时使用,审核结果将通过站内信通知。
|
||||||
<el-table-column prop="type" label="类型" width="120" align="center">
|
</template>
|
||||||
<template #default="scope">
|
</el-alert>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 数据表格 -->
|
||||||
<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
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -106,6 +106,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -589,41 +591,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.image-requests-container {
|
.image-requests-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-alert {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,12 +656,16 @@ onMounted(() => {
|
|||||||
/* 环境变量配置样式 */
|
/* 环境变量配置样式 */
|
||||||
.env-vars-container {
|
.env-vars-container {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-vars-header {
|
.env-vars-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-vars-item {
|
.env-vars-item {
|
||||||
@@ -665,17 +688,23 @@ onMounted(() => {
|
|||||||
|
|
||||||
.add-env-btn {
|
.add-env-btn {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 端口配置样式 */
|
/* 端口配置样式 */
|
||||||
.ports-container {
|
.ports-container {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ports-header {
|
.ports-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ports-item {
|
.ports-item {
|
||||||
@@ -702,6 +731,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
.add-port-btn {
|
.add-port-btn {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情样式 */
|
/* 详情样式 */
|
||||||
@@ -710,11 +741,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.request-reason {
|
.request-reason {
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f9fa;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-comment {
|
.review-comment {
|
||||||
@@ -724,5 +757,37 @@ onMounted(() => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
border-left: 4px solid #67c23a;
|
border-left: 4px solid #67c23a;
|
||||||
|
color: #606266;
|
||||||
}
|
}
|
||||||
</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>
|
<template>
|
||||||
<div class="announcements-container">
|
<div class="announcements-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>官方公告</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布公告
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
<el-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">
|
<div class="table-section">
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-table
|
||||||
<el-form-item label="公告标题">
|
v-loading="loading"
|
||||||
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable />
|
:data="tableData"
|
||||||
</el-form-item>
|
style="width: 100%"
|
||||||
<el-form-item label="发布时间">
|
row-key="id"
|
||||||
<el-date-picker
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
v-model="searchForm.dateRange"
|
>
|
||||||
type="daterange"
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
range-separator="至"
|
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||||
start-placeholder="开始日期"
|
<el-table-column prop="publisher" label="发布人" width="120" />
|
||||||
end-placeholder="结束日期"
|
<el-table-column prop="publishTime" label="发布时间" width="180" />
|
||||||
format="YYYY-MM-DD"
|
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
|
||||||
value-format="YYYY-MM-DD"
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
clearable
|
<template #default="scope">
|
||||||
/>
|
<el-tag :type="getStatusType(scope.row.status)">
|
||||||
</el-form-item>
|
{{ getStatusText(scope.row.status) }}
|
||||||
<el-form-item label="状态">
|
</el-tag>
|
||||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
</template>
|
||||||
<el-option label="已发布" value="published" />
|
</el-table-column>
|
||||||
<el-option label="草稿" value="draft" />
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<el-option label="已下线" value="offline" />
|
<template #default="scope">
|
||||||
</el-select>
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
</el-form-item>
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
<el-form-item>
|
<el-button
|
||||||
<el-button type="primary" @click="handleSearch">
|
type="warning"
|
||||||
<el-icon><search /></el-icon>搜索
|
link
|
||||||
</el-button>
|
@click="handleChangeStatus(scope.row)"
|
||||||
<el-button @click="resetSearch">
|
v-if="scope.row.status !== 'offline'"
|
||||||
<el-icon><refresh /></el-icon>重置
|
>下线</el-button>
|
||||||
</el-button>
|
<el-button
|
||||||
</el-form-item>
|
type="danger"
|
||||||
</el-form>
|
link
|
||||||
</el-card>
|
@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
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -102,6 +96,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -365,32 +361,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.announcements-container {
|
.announcements-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,4 +442,35 @@ onMounted(() => {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
</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>
|
<template>
|
||||||
<div class="news-container">
|
<div class="news-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>新闻咨询</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布新闻
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
<el-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">
|
<div class="table-section">
|
||||||
<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">
|
|
||||||
<el-table
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
:data="newsData"
|
:data="newsData"
|
||||||
border
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
>
|
>
|
||||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
|
||||||
@@ -68,33 +71,27 @@
|
|||||||
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
|
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
|
||||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click="handleView(scope.row)">
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
<el-icon><view /></el-icon>查看
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
</el-button>
|
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||||
<el-button type="primary" link @click="handleEdit(scope.row)">
|
|
||||||
<el-icon><edit /></el-icon>编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" link @click="handleDelete(scope.row)">
|
|
||||||
<el-icon><delete /></el-icon>删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
<el-pagination
|
||||||
<el-pagination
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:page-size="pagination.pageSize"
|
||||||
v-model:page-size="pagination.pageSize"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:total="pagination.total"
|
||||||
:total="pagination.total"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
||||||
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
||||||
@current-change="handleCurrentChange"
|
background
|
||||||
/>
|
class="pagination"
|
||||||
</div>
|
/>
|
||||||
</el-card>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 新闻详情对话框 -->
|
<!-- 新闻详情对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -400,36 +397,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.news-container {
|
.news-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-list {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
.table-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,4 +506,35 @@ onMounted(() => {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: #E6A23C;
|
color: #E6A23C;
|
||||||
}
|
}
|
||||||
</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>
|
<template>
|
||||||
<div class="policies-container">
|
<div class="policies-container">
|
||||||
<div class="page-header">
|
<!-- 主容器 -->
|
||||||
<h2>官方政策</h2>
|
<el-card class="main-container" shadow="never">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<!-- 搜索和操作栏 -->
|
||||||
<el-icon><plus /></el-icon>发布政策
|
<div class="filter-section">
|
||||||
</el-button>
|
<div class="filter-content">
|
||||||
</div>
|
<el-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">
|
<div class="table-section">
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-table
|
||||||
<el-form-item label="政策标题">
|
v-loading="loading"
|
||||||
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable />
|
:data="tableData"
|
||||||
</el-form-item>
|
style="width: 100%"
|
||||||
<el-form-item label="政策类型">
|
row-key="id"
|
||||||
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable>
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
<el-option label="服务条款" value="terms" />
|
>
|
||||||
<el-option label="定价政策" value="pricing" />
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
<el-option label="隐私政策" value="privacy" />
|
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
|
||||||
<el-option label="合规政策" value="compliance" />
|
<el-table-column prop="type" label="政策类型" width="120" align="center">
|
||||||
<el-option label="其他" value="other" />
|
<template #default="scope">
|
||||||
</el-select>
|
<el-tag :type="getPolicyTypeTag(scope.row.type)">
|
||||||
</el-form-item>
|
{{ getPolicyTypeText(scope.row.type) }}
|
||||||
<el-form-item label="发布时间">
|
</el-tag>
|
||||||
<el-date-picker
|
</template>
|
||||||
v-model="searchForm.dateRange"
|
</el-table-column>
|
||||||
type="daterange"
|
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
|
||||||
range-separator="至"
|
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
|
||||||
start-placeholder="开始日期"
|
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
|
||||||
end-placeholder="结束日期"
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
format="YYYY-MM-DD"
|
<template #default="scope">
|
||||||
value-format="YYYY-MM-DD"
|
<el-tag :type="getStatusType(scope.row.status)">
|
||||||
clearable
|
{{ getStatusText(scope.row.status) }}
|
||||||
/>
|
</el-tag>
|
||||||
</el-form-item>
|
</template>
|
||||||
<el-form-item>
|
</el-table-column>
|
||||||
<el-button type="primary" @click="handleSearch">
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
<el-icon><search /></el-icon>搜索
|
<template #default="scope">
|
||||||
</el-button>
|
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
|
||||||
<el-button @click="resetSearch">
|
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
<el-icon><refresh /></el-icon>重置
|
<el-button
|
||||||
</el-button>
|
type="warning"
|
||||||
</el-form-item>
|
link
|
||||||
</el-form>
|
@click="handleChangeStatus(scope.row)"
|
||||||
</el-card>
|
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
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="pagination.currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
@@ -111,6 +105,8 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -435,32 +431,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.policies-container {
|
.policies-container {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.main-container {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
gap: 20px;
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.search-form {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.search-form :deep(.el-form-item) {
|
||||||
margin-top: 20px;
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
background: #fafbfc;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,4 +517,35 @@ onMounted(() => {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
</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
-779
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user