108 Commits

Author SHA1 Message Date
shiran 7394afb83f chore(admin): 站点图标改用 logo.ico,与 user_dashboard 统一
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 1m17s
缘由:
用户 2026-05-11 要求三个前端项目使用同一 logo 图标,源文件位于
ApiServer-Web-user_dashboard_pc/public/logo.ico。
admin 原本只有 public/logo.svg(491 字节),需要新增 logo.ico 并切换 link。

修改:
- public/logo.ico (new):
  从 user_dashboard_pc/public/logo.ico 拷贝同源副本(267,837 字节)。
- index.html L5:
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
  → <link rel="icon" type="image/x-icon" href="/logo.ico" />
  原 logo.svg 保留未删(用户未要求清理,避免越权)。

预期:
- 浏览器标签页 favicon 与 user_dashboard、home 三端统一。
- Vite 静态资源走 public/ 路径,重启 dev / 强制刷新即可生效。

测试:
- 仅文件 + link 改动,lint 通过,未做运行时复现。请用户重启 vite dev / 强制刷新观察。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:33:50 +08:00
shiran c43d1978a8 feat: 虚拟机流量精细化控制接入(接口新增,待联调)
Build and Deploy Vue3 / build (push) Successful in 1m37s
Build and Deploy Vue3 / deploy (push) Successful in 1m16s
1. userVm.js/kvmService.js 新增 traffic_policy 系列 API(GET/update/add_fixed/add_temporary)
2. UserVmList.vue/VmManage.vue 创建表单新增 traffic_max、traffic_exhausted_rx/tx_mbps 三个可选字段
3. UserVmDetail.vue/VmDetail.vue 修改带宽表单新增耗尽限速字段,并各增加流量策略 Tab(展示+修改策略+增加固定/临时流量)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 15:10:44 +08:00
shiran 475c62aefc feat: 添加用户ID和订单ID链接跳转功能
Build and Deploy Vue3 / build (push) Successful in 1m30s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s
- 在团购活动页面中为用户ID添加点击跳转到用户详情的功能
- 在团购管理页面中为用户ID添加点击跳转到用户详情的功能
- 在审核相关页面中为容器ID添加点击跳转到容器详情的功能
- 在营销相关页面中为用户ID和订单ID添加点击跳转功能
- 在订单列表页面中为用户ID和商品ID添加点击跳转功能
- 在商品列表页面中为用户和商品添加点击跳转功能
- 在工单列表页面中为用户名添加点击跳转到用户详情的功能
- 在用户虚拟机列表中为商品和用户添加点击跳转功能
- 在用户余额页面中为支付订单ID添加点击跳转到订单详情的功能
- 统一使用el-link组件实现可点击的链接样式
- 添加useRouter依赖并创建router实例用于页面跳转
2026-04-24 18:06:29 +08:00
shiran c0daa6ed11 docs(readme): 更新文档说明
Build and Deploy Vue3 / build (push) Successful in 1m29s
Build and Deploy Vue3 / deploy (push) Successful in 1m14s
- 添加项目使用指南
- 完善API接口描述
- 修正错误的配置示例
2026-04-23 17:43:31 +08:00
lin 2e073c2b87 fix: 优化顶部标签页的滚动条
Build and Deploy Vue3 / build (push) Successful in 1m32s
Build and Deploy Vue3 / deploy (push) Successful in 1m13s
2026-04-21 15:16:32 +08:00
lin 13248468d3 fix: 用户虚拟机详情跳转参数问题
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 1m17s
2026-04-21 14:13:31 +08:00
lin ab7a8d5cfa fix: 用户余额管理页面添加返回按钮
Build and Deploy Vue3 / build (push) Successful in 1m24s
Build and Deploy Vue3 / deploy (push) Successful in 1m13s
2026-04-20 16:08:11 +08:00
lin 64d40cbbbf fix: 修改头像预览问题
Build and Deploy Vue3 / build (push) Successful in 1m25s
Build and Deploy Vue3 / deploy (push) Successful in 1m18s
2026-04-20 16:04:47 +08:00
lin d72a4f804e fix: 右侧记录栏添加已购商品
Build and Deploy Vue3 / build (push) Successful in 2m21s
Build and Deploy Vue3 / deploy (push) Successful in 1m12s
2026-04-20 11:32:14 +08:00
lin 8b2251ef97 feat: 增加菜单管理
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s
2026-04-18 16:24:57 +08:00
lin 2916c04ba5 fix: 参数范围值修改
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 1m28s
2026-04-17 19:27:25 +08:00
lin c7245cec67 fix: 用户商品模块
Build and Deploy Vue3 / build (push) Successful in 1m34s
Build and Deploy Vue3 / deploy (push) Successful in 1m10s
2026-04-16 15:43:08 +08:00
lin 985412c3bc fix: 虚拟机模块
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 3m5s
2026-04-16 13:22:39 +08:00
lin f53f63e679 fix: 虚拟机详情监控模块
Build and Deploy Vue3 / build (push) Successful in 1m30s
Build and Deploy Vue3 / deploy (push) Successful in 17m15s
2026-04-15 18:38:08 +08:00
lin cae1f847e4 fix: 网络模块
Build and Deploy Vue3 / build (push) Successful in 1m59s
Build and Deploy Vue3 / deploy (push) Successful in 1m21s
2026-04-15 16:46:28 +08:00
lin 5428f01cdf fix: 虚拟化平台模块
Build and Deploy Vue3 / build (push) Successful in 2m40s
Build and Deploy Vue3 / deploy (push) Has been cancelled
2026-04-15 16:41:06 +08:00
lin 7652b290b0 fix: 虚拟机模块
Build and Deploy Vue3 / build (push) Successful in 1m58s
Build and Deploy Vue3 / deploy (push) Successful in 1m13s
2026-04-15 16:35:41 +08:00
lin cf188bb94a fix: 数据迁移模块
Build and Deploy Vue3 / build (push) Successful in 1m30s
Build and Deploy Vue3 / deploy (push) Successful in 1m5s
2026-04-15 16:18:15 +08:00
lin b3ed406f84 fix: 提交修改
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 1m9s
2026-04-15 16:02:36 +08:00
lin 2f06aa9f5f style: 优化布局和交互(Loading/空状态/骨架屏)
Build and Deploy Vue3 / build (push) Successful in 1m51s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s
2026-04-07 16:51:12 +08:00
lin f0e89695f4 fix: 修改新增用户商品的配置项逻辑
Build and Deploy Vue3 / build (push) Successful in 4m9s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s
2026-04-06 18:44:11 +08:00
lin c07e09c151 feat: 添加用户虚拟机商品管理
Build and Deploy Vue3 / build (push) Successful in 1m40s
Build and Deploy Vue3 / deploy (push) Successful in 1m8s
2026-03-31 15:13:04 +08:00
lin 71d3605f4f fix: 虚拟机添加强制操作
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s
2026-03-26 17:20:00 +08:00
lin b7e806cc80 fix: 创建数据卷的系统卷展示选择镜像
Build and Deploy Vue3 / build (push) Successful in 6m0s
Build and Deploy Vue3 / deploy (push) Successful in 58s
2026-03-26 17:06:12 +08:00
lin 1a4587f893 fix: 重构虚拟机内网外网参数设置选择网络
Build and Deploy Vue3 / build (push) Successful in 1m28s
Build and Deploy Vue3 / deploy (push) Successful in 1m1s
2026-03-26 16:36:25 +08:00
lin 40a5e486a6 feat: 对接用户组网管理
Build and Deploy Vue3 / build (push) Successful in 1m43s
Build and Deploy Vue3 / deploy (push) Successful in 1m7s
2026-03-24 18:57:52 +08:00
lin 3357566b02 fex: 样式修改
Build and Deploy Vue3 / build (push) Successful in 1m39s
Build and Deploy Vue3 / deploy (push) Successful in 1m0s
2026-03-21 19:07:04 +08:00
lin 25d782b050 feat: 将页面添加分页
Build and Deploy Vue3 / build (push) Successful in 1m35s
Build and Deploy Vue3 / deploy (push) Successful in 1m5s
2026-03-21 17:37:06 +08:00
lin 9edb59d16e fix: 修改内存的基础单位为kb
Build and Deploy Vue3 / build (push) Successful in 2m38s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s
2026-03-21 15:25:38 +08:00
lin cf19956b88 feat: 对接虚拟化平台管理
Build and Deploy Vue3 / build (push) Successful in 1m22s
Build and Deploy Vue3 / deploy (push) Successful in 1m2s
2026-03-19 18:13:24 +08:00
lin cd16ec17ae feat: 新增移动端配置信息
Build and Deploy Vue3 / build (push) Successful in 1m33s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s
2026-03-17 18:40:12 +08:00
lin f4dbf17ce9 feat: 对接主控服务接口
Build and Deploy Vue3 / build (push) Successful in 2m29s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s
2026-03-14 15:45:07 +08:00
lin 25975c8b29 feat: 对接宿主机组映射管理
Build and Deploy Vue3 / build (push) Successful in 1m20s
Build and Deploy Vue3 / deploy (push) Successful in 1m0s
2026-03-13 17:33:02 +08:00
lin d650bfeb61 style: 对商品管理页面进行名称样式修改
Build and Deploy Vue3 / build (push) Successful in 1m15s
Build and Deploy Vue3 / deploy (push) Successful in 54s
2026-03-13 14:07:09 +08:00
lin 3e751d4c42 style: 修改值字段的文字展示样式
Build and Deploy Vue3 / build (push) Successful in 1m18s
Build and Deploy Vue3 / deploy (push) Successful in 56s
2026-03-11 18:58:01 +08:00
lin 193db5735f feat: 相近字排序
Build and Deploy Vue3 / build (push) Successful in 1m21s
Build and Deploy Vue3 / deploy (push) Successful in 56s
2026-03-11 16:55:17 +08:00
lin 09a83f4985 feat: 添加编辑配置是否开放接口接口
Build and Deploy Vue3 / build (push) Successful in 2m52s
Build and Deploy Vue3 / deploy (push) Successful in 55s
2026-03-11 14:55:27 +08:00
lin 20790cf029 style: 对list的类型值添加点击打开编辑弹窗效果
Build and Deploy Vue3 / build (push) Successful in 2m53s
Build and Deploy Vue3 / deploy (push) Successful in 56s
2026-03-11 12:48:21 +08:00
lin 86f3835e51 style: 表格值字段的list类型只展示文件数,添加note列
Build and Deploy Vue3 / build (push) Successful in 1m45s
Build and Deploy Vue3 / deploy (push) Successful in 1m2s
2026-03-11 10:27:29 +08:00
lin a2a7644a9f fix: 设置动态编辑配置图片
Build and Deploy Vue3 / build (push) Successful in 2m47s
Build and Deploy Vue3 / deploy (push) Successful in 55s
2026-03-10 19:25:43 +08:00
lin 3ca956d9f0 feat: 添加配置管理的批量删除,复选框
Build and Deploy Vue3 / build (push) Successful in 2m0s
Build and Deploy Vue3 / deploy (push) Successful in 53s
2026-03-10 16:23:14 +08:00
lin 5d16589e54 fix: 对象存储图片,没有jpg后缀不用做扩展名检测,不然图片不渲染
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 53s
2026-03-10 15:20:35 +08:00
lin 255bd9e832 fix: 修改编辑配置对接信息
Build and Deploy Vue3 / build (push) Successful in 1m38s
Build and Deploy Vue3 / deploy (push) Successful in 56s
2026-03-10 14:30:07 +08:00
lin 2e82ff8a34 fix: 新增配置添加确定对接函数
Build and Deploy Vue3 / build (push) Successful in 1m37s
Build and Deploy Vue3 / deploy (push) Successful in 55s
2026-03-10 14:20:47 +08:00
lin c100c37a32 style: 配置管理弄成树状视图
Build and Deploy Vue3 / build (push) Successful in 1m24s
Build and Deploy Vue3 / deploy (push) Successful in 57s
2026-03-10 13:56:14 +08:00
lin cdd8f86b92 feat: 管理员 配置信息类型新增file,file_list,string_list类型
Build and Deploy Vue3 / build (push) Successful in 1m34s
Build and Deploy Vue3 / deploy (push) Successful in 1m0s
2026-03-10 13:08:49 +08:00
lin fe29a8b3d0 feat: 添加新增套餐是否展示在首页参数
Build and Deploy Vue3 / build (push) Successful in 2m26s
Build and Deploy Vue3 / deploy (push) Successful in 55s
2026-03-10 10:49:59 +08:00
lin 2f38932878 feate:添加创建参数的必选参数
Build and Deploy Vue3 / build (push) Successful in 1m20s
Build and Deploy Vue3 / deploy (push) Successful in 1m24s
2026-02-05 17:00:39 +08:00
lin fdc9db9a9c fix:商品套餐添加固定价格
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s
2026-02-05 15:15:13 +08:00
lin 4d45cf535e fix:修改编辑参数值的展示
Build and Deploy Vue3 / build (push) Successful in 1m18s
Build and Deploy Vue3 / deploy (push) Successful in 1m27s
2026-02-05 12:54:49 +08:00
lin 9d8f23262b fix:修改用户选择展示的用户名称参数对接
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 1m25s
2026-02-04 16:04:22 +08:00
lin e96e9c4a7e feate:编辑套餐添加
Build and Deploy Vue3 / build (push) Successful in 2m56s
Build and Deploy Vue3 / deploy (push) Successful in 1m25s
2026-02-04 10:49:24 +08:00
lin 5a31de64b3 fix:修改json格式参数展示
Build and Deploy Vue3 / build (push) Successful in 1m17s
Build and Deploy Vue3 / deploy (push) Successful in 1m23s
2026-02-02 18:22:58 +08:00
lin b4260fedb8 fix:修改产品价格
Build and Deploy Vue3 / build (push) Successful in 4m46s
Build and Deploy Vue3 / deploy (push) Successful in 1m21s
2026-01-30 16:45:21 +08:00
lin 793a96a44f fix:修改视图样式
Build and Deploy Vue3 / build (push) Successful in 3m43s
Build and Deploy Vue3 / deploy (push) Successful in 1m24s
2026-01-29 18:13:25 +08:00
lin 043be60f4f feate:添加分组标签
Build and Deploy Vue3 / build (push) Successful in 4m59s
Build and Deploy Vue3 / deploy (push) Successful in 1m23s
2026-01-29 17:43:45 +08:00
lin 127d54eaa6 feate:新增套餐管理
Build and Deploy Vue3 / build (push) Successful in 1m11s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s
2026-01-29 15:18:08 +08:00
lin ead7c5bba5 fix:工单样式兼容移动端
Build and Deploy Vue3 / build (push) Successful in 1m12s
Build and Deploy Vue3 / deploy (push) Successful in 1m19s
2026-01-23 14:24:29 +08:00
lin 5b5e0f62ec Merge branch 'master' of https://gitea.s1f.ren/lin/ApiServer-Web-admin_dashboard_pc
Build and Deploy Vue3 / build (push) Successful in 1m12s
Build and Deploy Vue3 / deploy (push) Successful in 1m17s
2026-01-23 13:21:02 +08:00
lin 9105503850 fix:修改仪表盘工单 2026-01-23 13:20:49 +08:00
shiran 20260e221c 更新 .gitea/workflows/build-service-server.yaml
Build and Deploy Vue3 / build (push) Successful in 1m13s
Build and Deploy Vue3 / deploy (push) Successful in 1m21s
2026-01-23 13:00:08 +08:00
shiran a270f58500 更新 .gitea/workflows/build-test-server.yaml
Build and Deploy Vue3 / deploy (push) Blocked by required conditions
Build and Deploy Vue3 / build (push) Has been cancelled
2026-01-23 12:59:40 +08:00
lin 7992ee9902 fix:修改用户列表的更多中的相关操作
Build and Deploy Vue3 / build (push) Successful in 1m15s
Build and Deploy Vue3 / deploy (push) Failing after 35s
2026-01-23 12:33:26 +08:00
lin 084aeebf13 fix:用户组列表成员弹窗限制高度
Build and Deploy Vue3 / build (push) Successful in 4m33s
Build and Deploy Vue3 / deploy (push) Successful in 1m19s
2026-01-21 10:57:06 +08:00
lin 1e79005440 fix:修改侧边栏样式
Build and Deploy Vue3 / build (push) Successful in 9m50s
Build and Deploy Vue3 / deploy (push) Successful in 1m31s
2026-01-20 18:25:38 +08:00
lin a6d4d70221 fix:修改样式
Build and Deploy Vue3 / build (push) Successful in 1m16s
Build and Deploy Vue3 / deploy (push) Successful in 1m22s
2026-01-20 17:59:31 +08:00
lin e3e70114fb fix:将侧边栏兼容移动端 2026-01-20 17:54:45 +08:00
lin 0b57581799 fix:修改对接订单列表参数
Build and Deploy Vue3 / build (push) Successful in 6m11s
Build and Deploy Vue3 / deploy (push) Failing after 1m35s
2026-01-19 18:32:24 +08:00
lin 36271b8bd0 fix:将填写弹窗修改为选择弹窗
Build and Deploy Vue3 / build (push) Successful in 6m17s
Build and Deploy Vue3 / deploy (push) Successful in 1m25s
2026-01-19 17:02:26 +08:00
lin cae89dd5ad fix:修改代金卷时间选择器
Build and Deploy Vue3 / build (push) Successful in 5m46s
Build and Deploy Vue3 / deploy (push) Failing after 1m41s
2026-01-08 18:40:32 +08:00
lin d3479fb0bb fix:修改参数值价格单位
Build and Deploy Vue3 / build (push) Successful in 9m19s
Build and Deploy Vue3 / deploy (push) Failing after 21s
2026-01-08 11:35:21 +08:00
lin 98cb0e1c8e fix:修改商品价格单元
Build and Deploy Vue3 / build (push) Successful in 1m11s
Build and Deploy Vue3 / deploy (push) Successful in 1m30s
2026-01-08 11:05:00 +08:00
lin 779359cec5 Merge branch 'master' of https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc
Build and Deploy Vue3 / build (push) Successful in 1m28s
Build and Deploy Vue3 / deploy (push) Failing after 1m41s
2026-01-08 10:50:32 +08:00
lin 60f141a0a9 fix:修改参数管理对接 2026-01-08 10:49:10 +08:00
wlkjyy fe1a118132 feat: 工单列表添加关键词搜索功能
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 1m31s
- 添加关键词搜索输入框,支持搜索工单标题/内容
- 300ms 防抖优化搜索性能
- 支持与用户筛选同时使用
2026-01-07 17:27:54 +08:00
wlkjyy 2ce2c1a31f feat: 工单系统优化 - 修复自动跳转问题并添加用户筛选功能
Build and Deploy Vue3 / build (push) Successful in 1m8s
Build and Deploy Vue3 / deploy (push) Successful in 1m28s
- 修复工单详情页定时刷新导致的自动跳转问题
- 添加用户搜索选择器,支持按用户筛选工单
- 优化用户搜索体验,使用对话框模式
- 修正API响应数据结构解析
2026-01-07 17:21:01 +08:00
lin 1655d86f6b fix:修改退款对接参数
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 1m28s
2026-01-05 15:28:32 +08:00
lin fcebebd216 feate:添加退款接口
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Failing after 1m41s
2026-01-05 15:19:48 +08:00
lin 5a93f4f8a8 feate:修改打包环境
Build and Deploy Vue3 / build (push) Successful in 5m30s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s
2025-12-31 19:16:52 +08:00
lin 4d10deef86 feate:对接删除队伍接口
Build and Deploy Vue3 / build (push) Successful in 5m22s
Build and Deploy Vue3 / deploy (push) Successful in 3m12s
2025-12-31 19:07:19 +08:00
lin cf7ac515f6 fix:修改备注弄成添加字段json
Build and Deploy Vue3 / build (push) Successful in 1m7s
Build and Deploy Vue3 / deploy (push) Successful in 3m31s
2025-12-31 18:52:30 +08:00
lin 4ef208a662 fix:队伍名称修改对接
Build and Deploy Vue3 / build (push) Successful in 1m9s
Build and Deploy Vue3 / deploy (push) Successful in 10m59s
2025-12-31 15:04:45 +08:00
lin f6dcec75d7 feate:添加拼单类型接口
Build and Deploy Vue3 / build (push) Successful in 1m6s
Build and Deploy Vue3 / deploy (push) Successful in 10m44s
2025-12-31 13:07:05 +08:00
lin 4cc684eca6 Merge branch 'master' of https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc
Build and Deploy Vue3 / build (push) Successful in 1m29s
Build and Deploy Vue3 / deploy (push) Successful in 6m21s
2025-12-30 14:22:52 +08:00
lin 00ea1845a7 添加拼团活动 2025-12-30 14:22:44 +08:00
wlkjyy 0c6166b3c7 feat: 工单详情页增强 - 支持图片粘贴拖拽和消息编辑功能
Build and Deploy Vue3 / build (push) Successful in 2m54s
Build and Deploy Vue3 / deploy (push) Successful in 11m17s
- 支持剪贴板粘贴图片和文件拖拽上传
- 添加消息编辑功能,支持修改内容和管理图片
- 编辑对话框支持显示、删除原有图片和添加新图片
- 修复消息更新API响应检查逻辑
- 优化图片文件ID管理,支持保留和删除原始图片
2025-12-17 17:03:52 +08:00
wlkjyy 978b18d5d5 feat: 工单系统优化 - 改为列表形式,添加排序、状态修改、图片粘贴拖拽等功能
Build and Deploy Vue3 / build (push) Successful in 2m52s
Build and Deploy Vue3 / deploy (push) Successful in 2m37s
2025-12-17 15:42:14 +08:00
wlkjyy 54f78e15fe feat: 优化工单和用户管理功能
Build and Deploy Vue3 / build (push) Successful in 1m12s
Build and Deploy Vue3 / deploy (push) Successful in 5m2s
- 工单模块改为列表形式,支持点击进入详情页回复
- 新增工单列表页面(TicketList.vue)和详情页面(TicketDetail.vue)
- 工单详情页支持图片上传、快捷回复、定时刷新
- 消息按ID排序,时间显示优化(今天/昨天/星期/完整日期)
- 定时刷新时不显示loading,且只在数据变化时更新UI
- 用户列表直接使用API返回的cover字段作为头像,减少HTTP请求
- 修复用户余额页面balance_type参数undefined问题
2025-12-16 11:29:52 +08:00
wlkjyy ab2df50c0d 优化用户和工单管理功能
Build and Deploy Vue3 / build (push) Successful in 3m1s
Build and Deploy Vue3 / deploy (push) Successful in 1m50s
- 优化用户列表页面,移除头像批量加载导致的大量detail请求
- 移除工单列表自动刷新功能,避免页面跳转问题
- 将用户余额管理整合到用户列表操作菜单中
- 重构用户余额管理页面,采用现代化企业扁平化设计
- 移除用户余额管理独立菜单项
- 优化页面交互体验和视觉效果
2025-12-15 20:34:02 +08:00
wlkjyy 6859753470 feat: 工单模块改为列表形式,点击回复进入详情页
Build and Deploy Vue3 / build (push) Successful in 1m13s
Build and Deploy Vue3 / deploy (push) Successful in 2m31s
2025-12-15 16:25:16 +08:00
shiran 32bb4502e7 更新 .gitea/workflows/build-service-server.yaml
Build and Deploy Vue3 / build (push) Successful in 7m51s
Build and Deploy Vue3 / deploy (push) Successful in 12m49s
2025-12-12 21:19:12 +08:00
lin 4a13048718 fix:修改商品列表分页展示
Build and Deploy Vue3 / build (push) Successful in 2m47s
Build and Deploy Vue3 / deploy (push) Successful in 1m43s
2025-12-12 16:02:25 +08:00
lin b56359e572 fix:对接商品列表分页
Build and Deploy Vue3 / build (push) Successful in 2m53s
Build and Deploy Vue3 / deploy (push) Successful in 6m6s
2025-12-11 17:52:52 +08:00
lin 41d6492daf fix:修改库存控制修改操作
Build and Deploy Vue3 / build (push) Successful in 3m4s
Build and Deploy Vue3 / deploy (push) Successful in 4m49s
2025-12-11 10:14:57 +08:00
lin 14fcac3a24 fix:修改库存参数
Build and Deploy Vue3 / build (push) Successful in 1m50s
Build and Deploy Vue3 / deploy (push) Successful in 5m42s
2025-12-11 09:59:58 +08:00
lin 0fc582bc8c Merge branch 'qian' 2025-12-10 20:19:32 +08:00
lin 0fe4ece1a9 fit:工单修改和商品关联修改 2025-12-10 20:17:13 +08:00
wlkjyy a09631551b xx 2025-12-08 15:24:48 +08:00
wlkjyy 777022632c Merge pull request 'refactor: extract image form to standalone page and implement tags view store' (#11) from qian into master
Build and Deploy Vue3 / build (push) Successful in 2m58s
Build and Deploy Vue3 / deploy (push) Successful in 3m43s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#11
2025-11-28 14:17:42 +08:00
wlkjyy 5ea4f2cfe3 Merge branch 'master' into qian 2025-11-28 14:17:29 +08:00
wlkjyy f7c3be1d30 refactor: extract image form to standalone page and implement tags view store
- Created ImageForm.vue as standalone page for add/edit image functionality
- Removed dialog-based image form from VmImages.vue
- Implemented tagsViewStore for global tab state management
- Added automatic tab closing on form cancel/back
- Fixed data persistence issue when switching between image edits
- Removed quick actions section from ImageForm
- Updated router configuration for new image form route
2025-11-28 14:15:29 +08:00
shiran 2a91cbb193 Merge pull request 'feat:添加admin相关接口' (#10) from qian into master
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 3m42s
Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#10
2025-11-17 16:53:17 +08:00
lin 067e0539ba feat:添加admin相关接口 2025-11-13 15:05:54 +08:00
lin 11cb40c86a fix:修改实例规格参数和镜像管理参数
Build and Deploy Vue3 / build (push) Successful in 2m57s
Build and Deploy Vue3 / deploy (push) Successful in 2m48s
2025-10-24 17:34:52 +08:00
lin fca272f8fa fix:xiug
Build and Deploy Vue3 / build (push) Successful in 1m6s
Build and Deploy Vue3 / deploy (push) Successful in 56s
2025-10-23 14:10:18 +08:00
lin d0c706f645 fix:修改获取镜像套餐参数
Build and Deploy Vue3 / build (push) Successful in 1m6s
Build and Deploy Vue3 / deploy (push) Successful in 1m48s
2025-10-15 16:37:13 +08:00
lin e8fa0d7c2c fix:修改镜像展示label
Build and Deploy Vue3 / build (push) Successful in 1m6s
Build and Deploy Vue3 / deploy (push) Successful in 57s
2025-10-07 15:45:18 +08:00
wlkjyy a97f6a7202 fix: 修复了创建虚拟机无法获取镜像列表的BUG
Build and Deploy Vue3 / build (push) Successful in 1m8s
Build and Deploy Vue3 / deploy (push) Successful in 7m52s
2025-10-07 00:05:24 +08:00
172 changed files with 97933 additions and 5404 deletions
+5
View File
@@ -17,7 +17,12 @@ store封装到src/store目录下。
注册侧边栏在/config/menus.js文件中。
新添加要求:
在遇到用户id需要填写和修改的弹窗将其修改为可预览样式
关于填写表单为推荐人id的需要使用组件AvatarSelector展示,如果是文件id或者是封面id 的也需要预览展示需要向头像列表组件一样,可以弄个文件组件/api/v1/admin/file/list这个是文件列表接口
规则:
1.只要涉及弹窗添加和修改xxxid类型的就需要生成一个弹窗组件并使用到页面中
## 1. 基础布局规范
```css
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL='https://apiservertest.s1f.ren'
+2 -2
View File
@@ -37,10 +37,10 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
runs-on: ninBo
steps:
- name: Download Artifact
uses: actions/download-artifact@v3
uses: https://gitea.s1f.ren/actions/download-artifact@v3
with:
name: vue3-build
+2 -2
View File
@@ -33,10 +33,10 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
runs-on: ninBo
steps:
- name: Download Artifact
uses: actions/download-artifact@v3
uses: https://gitea.s1f.ren/actions/download-artifact@v3
with:
name: vue3-build
+2
View File
@@ -1,3 +1,5 @@
# 管理员后台pc端
# 007UI 后台管理系统
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
View File
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="icon" type="image/x-icon" href="/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="007UI - 高端蓝色扁平化后台管理系统模板" />
<meta name="keywords" content="管理系统,后台,模板,Vue3,ElementPlus" />
+984 -931
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

+394 -12
View File
@@ -16,6 +16,7 @@ import {getUserInfo} from "@/api/login.js";
const userStore = useUserStore()
onMounted(async () => {
let resp = await getUserInfo()
console.log("用户信息:",resp)
userStore.setUserInfo(resp.data)
console.log(userStore.userInfo)
})
@@ -87,40 +88,421 @@ html, body {
background: #a8a8a8;
}
/* Element Plus样式优化 */
/* Element Plus全局配色优化 */
/* 按钮扁平化 */
.el-button {
font-weight: 400;
border-radius: 4px;
border-radius: 0 !important;
transition: all 0.2s ease;
font-weight: 500;
}
/* 主按钮 - 深蓝灰色 */
.el-button--primary {
background-color: #2c3e50 !important;
border-color: #2c3e50 !important;
color: #ffffff !important;
}
.el-button--primary:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
.el-button--primary:active {
background-color: #1a252f !important;
border-color: #1a252f !important;
}
/* 成功按钮 - 绿色系 */
.el-button--success {
background-color: #27ae60 !important;
border-color: #27ae60 !important;
color: #ffffff !important;
}
.el-button--success:hover {
background-color: #2ecc71 !important;
border-color: #2ecc71 !important;
}
.el-button--success:active {
background-color: #229954 !important;
border-color: #229954 !important;
}
/* 危险按钮 - 红色系 */
.el-button--danger {
background-color: #e74c3c !important;
border-color: #e74c3c !important;
color: #ffffff !important;
}
.el-button--danger:hover {
background-color: #ec7063 !important;
border-color: #ec7063 !important;
}
.el-button--danger:active {
background-color: #c0392b !important;
border-color: #c0392b !important;
}
/* 默认按钮 */
.el-button--default {
background-color: #ffffff !important;
border-color: #d5d9e0 !important;
color: #606266 !important;
}
.el-button--default:hover {
background-color: #f5f7fa !important;
border-color: #c0c4cc !important;
color: #606266 !important;
}
/* Link按钮 */
.el-button.is-link {
color: #3498db !important;
border: none !important;
padding: 0;
background: transparent !important;
}
.el-button.is-link:hover {
color: #2980b9 !important;
background: transparent !important;
}
.el-button--primary.is-link {
color: #3498db !important;
background: transparent !important;
}
.el-button--primary.is-link:hover {
color: #2980b9 !important;
background: transparent !important;
}
/* 输入框扁平化 */
.el-input__wrapper {
border-radius: 0 !important;
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
background-color: #ffffff !important;
transition: all 0.2s ease;
}
.el-input__wrapper:hover {
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
}
.el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px #2c3e50 inset !important;
}
/* 标签扁平化 */
.el-tag {
border-radius: 0 !important;
border: none !important;
font-weight: 500;
padding: 2px 10px;
}
/* 成功标签 */
.el-tag--success {
background-color: #d5f4e6 !important;
color: #27ae60 !important;
}
/* 危险标签 */
.el-tag--danger {
background-color: #fadbd8 !important;
color: #e74c3c !important;
}
/* 信息标签 */
.el-tag--info {
background-color: #ebf5fb !important;
color: #3498db !important;
}
/* 卡片扁平化 + 层次感 */
.el-card {
border-radius: 4px;
border-radius: 0 !important;
border: 1px solid #e1e8ed !important;
box-shadow: none !important;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.el-card[shadow="hover"]:hover {
border-color: #c0c4cc !important;
box-shadow: 0 2px 12px rgba(44, 62, 80, 0.08) !important;
}
/* 表格扁平化 */
.el-table {
border-radius: 0 !important;
border: none !important;
color: #2c3e50 !important;
}
.el-table__header {
background: #f8f9fa !important;
}
.el-table th {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed !important;
color: #2c3e50 !important;
font-weight: 600 !important;
font-size: 13px !important;
}
.el-table td {
border-bottom: 1px solid #f0f2f5 !important;
color: #34495e !important;
}
.el-table tr:hover > td {
background-color: #f8f9fa !important;
}
/* 分页扁平化 */
.el-pagination .el-pager li {
border-radius: 0 !important;
color: #606266 !important;
font-weight: 500;
}
.el-pagination .el-pager li.is-active {
background-color: #2c3e50 !important;
color: #ffffff !important;
}
.el-pagination .el-pager li:hover {
color: #2c3e50 !important;
}
.el-pagination button {
border-radius: 0 !important;
color: #606266 !important;
}
.el-pagination button:hover {
color: #2c3e50 !important;
}
.el-pagination .el-select .el-input__wrapper {
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
}
.el-pagination .el-input__inner {
color: #606266 !important;
}
/* 下拉菜单扁平化 */
.el-dropdown-menu {
border-radius: 0 !important;
border: 1px solid #e1e8ed !important;
background-color: #ffffff !important;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1) !important;
padding: 4px 0;
}
.el-dropdown-menu__item {
color: #34495e !important;
transition: all 0.2s ease;
padding: 8px 16px;
}
.el-dropdown-menu__item:hover {
background-color: #f8f9fa !important;
color: #2c3e50 !important;
}
.el-dropdown-menu__item.is-divided {
border-top: 1px solid #e1e8ed !important;
}
/* 选择框扁平化 */
.el-select .el-input__wrapper {
border-radius: 0 !important;
}
/* 文本域扁平化 */
.el-textarea__inner {
border-radius: 0 !important;
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
transition: all 0.2s ease;
}
.el-textarea__inner:hover {
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
}
.el-textarea__inner:focus {
box-shadow: 0 0 0 1px #2c3e50 inset !important;
}
/* 菜单扁平化 */
.el-menu {
border-right: none;
}
.el-table {
border-radius: 4px;
/* 表单标签 */
.el-form-item__label {
color: #2c3e50 !important;
font-weight: 500;
}
/* Dialog 扁平化样式 */
.el-overlay {
background-color: rgba(0, 0, 0, 0.5);
}
.el-overlay-dialog {
display: flex;
align-items: center;
justify-content: center;
}
.el-dialog {
border-radius: 8px;
border-radius: 0;
border: none;
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15);
background-color: #ffffff;
margin: 0;
padding: 0 !important;
}
.el-dialog__wrapper {
border: none;
}
.el-dialog__header {
margin: 0;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
font-weight: 500;
padding: 20px 24px;
border-bottom: 1px solid #e1e8ed;
background-color: #fafbfc;
font-weight: 600;
font-size: 16px;
color: #2c3e50;
}
.el-dialog__headerbtn {
top: 20px;
right: 24px;
width: 32px;
height: 32px;
}
.el-dialog__close {
color: #7f8c8d;
font-size: 18px;
transition: all 0.2s ease;
}
.el-dialog__close:hover {
color: #2c3e50;
background-color: #f8f9fa;
}
.el-dialog__body {
padding: 20px;
padding: 24px;
color: #34495e;
background-color: #ffffff;
}
.el-dialog__footer {
padding: 10px 20px 20px;
padding: 16px 24px;
border-top: 1px solid #e1e8ed;
background-color: #fafbfc;
}
/* Dialog 内表单组件样式 */
.el-dialog .el-input__wrapper {
border-radius: 0;
}
.el-dialog .el-select .el-input__wrapper {
border-radius: 0;
}
.el-dialog .el-textarea__inner {
border-radius: 0;
}
.el-dialog .el-form-item__label {
color: #2c3e50;
font-weight: 500;
}
.el-dialog .el-form-item {
margin-bottom: 20px;
}
/* Descriptions 描述列表增强 */
.el-descriptions {
--el-descriptions-item-bordered-label-background: #fafbfc;
}
.el-descriptions__label {
color: #606266 !important;
font-weight: 500 !important;
}
.el-descriptions__content {
color: #1d2129 !important;
}
/* Loading 遮罩增强 */
.el-loading-mask {
background-color: rgba(255, 255, 255, 0.85) !important;
}
.el-loading-spinner .circular {
width: 36px;
height: 36px;
}
.el-loading-spinner .el-loading-text {
color: #606266 !important;
font-size: 13px;
margin-top: 8px;
}
/* Message Box 增强 */
.el-message-box {
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15) !important;
}
.el-message-box__header {
padding: 16px 20px 12px !important;
}
.el-message-box__title {
font-weight: 600 !important;
color: #1d2129 !important;
}
.el-message-box__btns .el-button {
border-radius: 0 !important;
}
/* Alert 增强 */
.el-alert {
border-radius: 0 !important;
}
/* Tabs 增强 */
.el-tabs__item {
transition: color 0.2s ease !important;
}
.el-tabs__item.is-active {
font-weight: 600 !important;
}
/* Switch 开关增强 */
.el-switch {
--el-switch-on-color: #2c3e50;
}
/* 全局链接按钮悬浮下划线 */
.el-button.is-link:hover,
.el-button--primary.is-link:hover {
text-decoration: underline;
}
</style>
+69
View File
@@ -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'
}
})
}
+69
View File
@@ -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'
}
})
}
+9
View File
@@ -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'
}
})
}
+172
View File
@@ -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
+42
View File
@@ -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)
}
+25
View File
@@ -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)
}
+768
View File
@@ -0,0 +1,768 @@
import { http2 } from '@/utils/request.js'
/**
* ================================
* 主控服务管理 API
* ================================
*/
/** 获取 KVM 主控服务列表 */
export const getKvmServiceList = (params) => {
return http2.get('/api/v1/admin/server/host_service/list', { params })
}
/** 获取 KVM 主控服务详情 */
export const getKvmServiceDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/detail', { params })
}
/** 创建 KVM 主控服务 */
export const createKvmService = (data) => {
return http2.post('/api/v1/admin/server/host_service/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改 KVM 主控服务 */
export const updateKvmService = (id, data) => {
return http2.post(`/api/v1/admin/server/host_service/update?id=${id}`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除 KVM 主控服务 */
export const deleteKvmService = (params) => {
return http2.delete('/api/v1/admin/server/host_service/delete', { params })
}
/**
* ================================
* 宿主机组映射管理 API
* ================================
*/
/** 获取本地主机组列表 */
export const getHostGroupList = (params) => {
return http2.get('/api/v1/admin/server/host_service/host_group/list', { params })
}
/** 从远程同步主机组到本地 */
export const syncHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/host_group/sync', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 绑定主机组到商品组或商品 */
export const bindHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/host_group/bind', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改本地主机组信息 */
export const updateHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/host_group/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 根据主机组树自动生成 GoodGroup/Goods/Args */
export const generateGoodsByHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/host_group/generate_goods', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除本地主机组 */
export const deleteHostGroup = (params) => {
return http2.delete('/api/v1/admin/server/host_service/host_group/delete', { params })
}
/**
* ================================
* 主控服务接口 - 远程宿主机组管理
* ================================
*/
/** 获取远程主机组列表 */
export const getRemoteHostGroupList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host_group/list', { params })
}
/** 获取远程主机组详情 */
export const getRemoteHostGroupDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host_group/detail', { params })
}
/** 获取远程主机组树形结构 */
export const getRemoteHostGroupTree = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host_group/tree', { params })
}
/** 获取主机组最优主机配置信息 */
export const getOptimalHostInfo = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host_group/optimal_host', { params })
}
/** 创建远程主机组 */
export const createRemoteHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/host_group/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改远程主机组 */
export const updateRemoteHostGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/host_group/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除远程主机组 */
export const deleteRemoteHostGroup = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/host_group/delete', { params })
}
/**
* ================================
* 主控服务接口 - 宿主机管理
* ================================
*/
/** 获取宿主机列表 */
export const getRemoteHostList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host/list', { params })
}
/** 获取宿主机详情 */
export const getRemoteHostDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host/detail', { params })
}
/** 获取宿主机指标数据 */
export const getRemoteHostMetrics = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host/metrics', { params })
}
/** 查询历史指标(宿主机或虚拟机) */
export const getMetricsHistory = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/host/metrics_history', { params })
}
/** 新增宿主机 */
export const addRemoteHost = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/host/add', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改宿主机 */
export const updateRemoteHost = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/host/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除宿主机 */
export const deleteRemoteHost = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/host/delete', { params })
}
/** 创建宿主机注册令牌 */
export const createHostToken = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/host/create_token', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* ================================
* 主控服务接口 - 镜像管理
* ================================
*/
/** 获取镜像列表 */
export const getImageList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/image/list', { params })
}
/** 获取镜像详情 */
export const getImageDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/image/detail', { params })
}
/** 获取镜像在指定宿主机上的状态 */
export const getImageHostStatus = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/image/host_status', { params })
}
/** 创建镜像 */
export const createImage = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/image/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改镜像 */
export const updateImage = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/image/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除镜像 */
export const deleteImage = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/image/delete', { params })
}
/** 重新下载镜像 */
export const reloadImage = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/image/reload', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 向宿主机同步镜像 */
export const syncImageToHost = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/image/sync', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 指定宿主机重新下载指定镜像 */
export const reloadImageOnHost = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/image/reload_host', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取宿主机镜像列表与状态(对比) */
export const getImageCompareHost = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/image/compare_host', { params })
}
/**
* ================================
* 主控服务接口 - 网络管理
* ================================
*/
/** 获取网络列表 */
export const getNetworkList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/network/list', { params })
}
/** 获取网络详情 */
export const getNetworkDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/network/detail', { params })
}
/** 创建网络 */
export const createNetwork = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/network/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改网络 */
export const updateNetwork = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/network/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 批量创建网络 */
export const batchCreateNetwork = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/network/batch_create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除网络 */
export const deleteNetwork = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/network/delete', { params })
}
/**
* ================================
* 主控服务接口 - 数据卷管理
* ================================
*/
/** 获取数据卷列表 */
export const getVolumeList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/volume/list', { params })
}
/** 获取数据卷详情 */
export const getVolumeDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/volume/detail', { params })
}
/** 创建数据卷 */
export const createVolume = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/volume/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 调整数据卷大小 */
export const resizeVolume = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/volume/resize', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 挂载卷到虚拟机 */
export const mountVolume = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/volume/mount', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 卸载卷 */
export const unmountVolume = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/volume/unmount', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 迁移卷 */
export const transferVolume = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/volume/transfer', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除卷 */
export const deleteVolume = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/volume/delete', { params })
}
/**
* ================================
* 主控服务接口 - 虚拟机管理
* ================================
*/
/** 获取虚拟机列表 */
export const getVmList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/list', { params })
}
/** 获取虚拟机详情 */
export const getVmDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/detail', { params })
}
/** 获取虚拟机状态 */
export const getVmStatus = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/status', { params })
}
/** 获取虚拟机指标数据 */
export const getVmMetrics = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/metrics', { params })
}
/** 创建虚拟机 */
export const createVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改虚拟机 */
export const updateVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 重建虚拟机 */
export const rebuildVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/rebuild', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 重构虚拟机 */
export const refactorVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/refactor', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改虚拟机带宽 */
export const updateVmTraffic = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/update_traffic', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// ========== 流量策略 ==========
// 测试未通过(接口新增,待联调)
/** 获取虚拟机流量策略 */
export const getVmTrafficPolicy = (params) => http2.get('/api/v1/admin/server/host_service/point/vm/traffic_policy', { params })
/** 修改虚拟机流量策略 */
export const updateVmTrafficPolicy = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/update', data, { headers: { 'Content-Type': 'multipart/form-data' } })
/** 增加虚拟机固定流量上限 */
export const addVmFixedTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_fixed', data, { headers: { 'Content-Type': 'multipart/form-data' } })
/** 增加虚拟机一次性临时流量 */
export const addVmTemporaryTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_temporary', data, { headers: { 'Content-Type': 'multipart/form-data' } })
/** 启动虚拟机 */
export const startVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/start', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 停止虚拟机 */
export const stopVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/stop', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 重启虚拟机 */
export const rebootVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/reboot', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 暂停虚拟机 */
export const suspendVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/suspend', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 恢复虚拟机 */
export const resumeVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/resume', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 虚拟机进入救援系统 */
export const rescueVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/rescue', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 虚拟机退出救援系统 */
export const exitRescueVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/exit_rescue', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除虚拟机 */
export const deleteVm = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/vm/delete', { params })
}
/** 迁移虚拟机(更换宿主机) */
export const migrateVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/migrate', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 发起虚拟机数据迁移 */
export const dataMigrateVm = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取虚拟机数据迁移进度 */
export const getDataMigrateProgress = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/progress', { params })
}
/** 中断虚拟机数据迁移 */
export const abortDataMigrate = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate/abort', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* ================================
* 主控服务接口 - 安全组管理
* ================================
*/
/** 获取安全组列表 */
export const getSecurityGroupList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/post_group/list', { params })
}
/** 获取安全组详情 */
export const getSecurityGroupDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/post_group/detail', { params })
}
/** 创建安全组 */
export const createSecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改安全组 */
export const updateSecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 同步安全组 */
export const syncSecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/sync', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 绑定安全组到虚拟机 */
export const bindSecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/bind', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 解绑安全组 */
export const unbindSecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/unbind', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除安全组 */
export const deleteSecurityGroup = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete', { params })
}
/** 开启安全组白名单 */
export const enableSecurityGroupWhitelist = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/enable_whitelist', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 关闭安全组白名单 */
export const disableSecurityGroupWhitelist = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/disable_whitelist', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 新增安全组规则 */
export const createSecurityGroupRule = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/create_rule', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改安全组规则 */
export const updateSecurityGroupRule = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/update_rule', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除安全组规则 */
export const deleteSecurityGroupRule = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/post_group/delete_rule', { params })
}
/** 应用安全组 */
export const applySecurityGroup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/apply', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* ================================
* 主控服务接口 - VNC 节点管理
* ================================
*/
/** 获取 VNC 节点列表 */
export const getVncNodeList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vnc/list', { params })
}
/** 获取虚拟机 VNC 连接信息 */
export const getVmVnc = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vnc/vm_vnc', { params })
}
/** 新增 VNC 节点 */
export const addVncNode = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vnc/add', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 测试 VNC 节点连接 */
export const testVncNode = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vnc/test', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改 VNC 节点 */
export const updateVncNode = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vnc/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除 VNC 节点 */
export const deleteVncNode = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/vnc/delete', { params })
}
/** 设置安全组共享状态 */
export const setSecurityGroupShared = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/post_group/set_shared', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// ========== 快照管理 ==========
/** 获取快照列表 */
export const getSnapshotList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/snapshot/list', { params })
}
/** 获取快照任务进度 */
export const getSnapshotProgress = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/snapshot/progress', { params })
}
/** 创建快照 */
export const createSnapshot = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/snapshot/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 恢复快照 */
export const restoreSnapshot = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/snapshot/restore', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除快照 */
export const deleteSnapshot = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/snapshot/delete', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// ========== 备份管理 ==========
/** 获取备份列表 */
export const getBackupList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/backup/list', { params })
}
/** 获取备份任务进度 */
export const getBackupProgress = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/backup/progress', { params })
}
/** 创建备份 */
export const createBackup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/backup/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 恢复备份 */
export const restoreBackup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/backup/restore', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除备份 */
export const deleteBackup = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/backup/delete', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取快照数量与上限 */
export const getSnapshotCount = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/snapshot/count', { params })
}
/** 设置快照数量上限 */
export const setSnapshotLimit = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/snapshot/set_limit', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取备份数量与上限 */
export const getBackupCount = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/backup/count', { params })
}
/** 设置备份数量上限 */
export const setBackupLimit = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/backup/set_limit', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* ================================
* 用户组网管理 (UserNetworking)
* ================================
*/
/** 获取组网列表 */
export const getUserNetworkingList = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/networking/list', { params })
}
/** 获取组网详情 */
export const getUserNetworkingDetail = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/networking/detail', { params })
}
/** 创建用户组网 */
export const createUserNetworking = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/networking/create', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 为虚拟机分配组网 IP */
export const assignUserNetworking = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/networking/assign', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除组网 */
export const deleteUserNetworking = (params) => {
return http2.delete('/api/v1/admin/server/host_service/point/networking/delete', { params })
}
/** 删除组网下的指定网络 */
export const removeUserNetworkingNetwork = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/networking/remove_network', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
+43
View File
@@ -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'
}
})
}
+256
View File
@@ -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})
}
+19
View File
@@ -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'
}
})
}
+86
View File
@@ -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'
}
})
}
+171
View File
@@ -0,0 +1,171 @@
import {http2} from "@/utils/request.js";
/**用户余额管理 */
/**修改用户余额 */
export const editUserBalance = (data) => {
return http2.post('/api/v1/admin/user/balance/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**添加用户消费记录 */
export const addUserConsumption = (data) => {
return http2.post('/api/v1/admin/user/balance/add_history', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**获取用户余额记录 */
export const getUserBalanceRecord = (data) => {
return http2.get('/api/v1/admin/user/balance/history?user_id='+data.user_id + '&balance_type=' + data.balance_type + '&page=' + data.page + '&count=' + data.count)
}
/**获取用户余额 */
export const getUserBalanceCount = (data) => {
return http2.get('/api/v1/admin/user/balance/get?user_id='+data.user_id)
}
/**获取用户信息 */
export const getUserInfo = (data) => {
return http2.get('/api/v1/admin/user/user/detail?user_id='+data.user_id)
}
/**获取用户列表 */
export const getUserList = (data) => {
return http2.get('/api/v1/admin/user/user/list?page=' + data.page + '&count=' + data.count + '&key=' + data.key)
}
/**更新用户信息 */
export const updateUserInfo = (data) => {
return http2.post('/api/v1/admin/user/user/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**删除用户 */
export const deleteUser = (data) => {
return http2.delete('/api/v1/admin/user/user/delete?user_id='+data.user_id)
}
/**修改用户头像 */
export const updateUserAvatar = (data) => {
return http2.post('/api/v1/admin/user/user/update_cover', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**修改用户密码 */
export const updateUserPassword = (data) => {
return http2.post('/api/v1/admin/user/user/update_password', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**修改用户组 */
export const updateUserGroup = (data) => {
return http2.post('/api/v1/admin/user/user/update_group', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**修改用户管理员权限*/
export const updateUserAdmin = (data) => {
return http2.post('/api/v1/admin/user/user/user2admin', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**修改用户实名信息*/
export const updateUserRealName = (data) => {
return http2.post('/api/v1/admin/user/user/update_real_name', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**获取用户登录记录*/
export const getUserLoginRecord = (data) => {
return http2.get('/api/v1/admin/user/user/login_history?user_id='+data.user_id + '&page=' + data.page + '&count=' + data.count)
}
/**获取用户操作记录 */
export const getUserOperationRecord = (data) => {
return http2.get('/api/v1/admin/user/user/manage_history?user_id='+data.user_id + '&page=' + data.page + '&count=' + data.count)
}
/**模拟用户登录 */
export const mockUserLogin = (data) => {
return http2.get('/api/v1/admin/user/user/simulation_login?user_id='+data.user_id)
}
/**新建任务 */
export const createTask = (data) => {
return http2.post('/api/v1/admin/user/user/create', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**用户组管理 */
/**获取用户组列表 */
export const getUserGroupList = (data) => {
return http2.get('/api/v1/admin/user_group/list?page=' + data.page + '&count=' + data.count)
}
/**获取用户组成员列表 */
export const getUserGroupMemberList = (data) => {
return http2.get('/api/v1/admin/user_group/member_list?group_id=' + data.group_id + '&page=' + data.page + '&count=' + data.count)
}
/**获取用户组详情信息 */
export const getUserGroupDetail = (data) => {
return http2.get('/api/v1/admin/user_group/detail?group_id=' + data.group_id)
}
/**新建用户组 */
export const createUserGroup = (data) => {
return http2.post('/api/v1/admin/user_group/create', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**更新用户组信息 */
export const updateUserGroupInfo = (data) => {
return http2.post('/api/v1/admin/user_group/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**删除用户组 */
export const deleteUserGroup = (data) => {
return http2.delete(`/api/v1/admin/user_group/delete?group_id=`+data.group_id,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**添加用户组成员 */
export const addUserGroupMember = (data) => {
return http2.post('/api/v1/admin/user_group/add_member', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**退款对应账单 */
export const refundBalance = (data) => {
return http2.get('/api/v1/admin/user/balance/refund', {
params:data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
+118
View File
@@ -0,0 +1,118 @@
import { http2 } from '@/utils/request.js'
const BASE = '/api/v1/admin/good/user_vm'
const GOODS_BASE = '/api/v1/admin/good/user_goods'
const fd = (data) => {
const f = new FormData()
Object.entries(data).forEach(([k, v]) => {
if (v === undefined || v === null) return
if (Array.isArray(v)) {
v.forEach(item => f.append(k, item))
} else if (typeof v === 'boolean') {
f.append(k, v ? 'true' : 'false')
} else {
f.append(k, v)
}
})
return f
}
// ========== 用户虚拟机 ==========
export const getUserVmList = (params) => http2.get(`${BASE}/list`, { params })
export const getUserVmDetail = (params) => http2.get(`${BASE}/detail`, { params })
export const getUserVmVnc = (params) => http2.get(`${BASE}/vnc`, { params })
export const getUserVmHostImages = (params) => http2.get(`${BASE}/host_images`, { params })
export const getGoodHostGroupImages = (params) => http2.get(`${BASE}/good_host_group_images`, { params })
export const createUserVm = (data) => http2.post(`${BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const bindUserVm = (data) => http2.post(`${BASE}/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const transferUserVm = (data) => http2.post(`${BASE}/transfer`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const migrateUserVm = (data) => http2.post(`${BASE}/migrate`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const updateUserVmTraffic = (data) => http2.post(`${BASE}/update_traffic`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const updateUserVm = (data) => http2.post(`${BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const refactorUserVm = (data) => http2.post(`${BASE}/refactor`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const startUserVm = (data) => http2.post(`${BASE}/start`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const stopUserVm = (data) => http2.post(`${BASE}/stop`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const rebootUserVm = (data) => http2.post(`${BASE}/reboot`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const suspendUserVm = (data) => http2.post(`${BASE}/suspend`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const resumeUserVm = (data) => http2.post(`${BASE}/resume`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const rescueUserVm = (data) => http2.post(`${BASE}/rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const exitRescueUserVm = (data) => http2.post(`${BASE}/exit_rescue`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const rebuildUserVm = (data) => http2.post(`${BASE}/rebuild`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVm = (params) => http2.delete(`${BASE}/delete`, { params })
// ========== 数据卷 ==========
export const getUserVmVolumeList = (params) => http2.get(`${BASE}/volume/list`, { params })
export const getUserVmVolumeDetail = (params) => http2.get(`${BASE}/volume/detail`, { params })
export const createUserVmVolume = (data) => http2.post(`${BASE}/volume/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const resizeUserVmVolume = (data) => http2.post(`${BASE}/volume/resize`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const mountUserVmVolume = (data) => http2.post(`${BASE}/volume/mount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const unmountUserVmVolume = (data) => http2.post(`${BASE}/volume/unmount`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmVolume = (params) => http2.delete(`${BASE}/volume/delete`, { params })
// ========== 快照 ==========
export const getUserVmSnapshotList = (params) => http2.get(`${BASE}/snapshot/list`, { params })
export const getUserVmSnapshotProgress = (params) => http2.get(`${BASE}/snapshot/progress`, { params })
export const getUserVmSnapshotCount = (params) => http2.get(`${BASE}/snapshot/count`, { params })
export const createUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const restoreUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmSnapshot = (data) => http2.post(`${BASE}/snapshot/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const setUserVmSnapshotLimit = (data) => http2.post(`${BASE}/snapshot/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
// ========== 备份 ==========
export const getUserVmBackupList = (params) => http2.get(`${BASE}/backup/list`, { params })
export const getUserVmBackupProgress = (params) => http2.get(`${BASE}/backup/progress`, { params })
export const getUserVmBackupCount = (params) => http2.get(`${BASE}/backup/count`, { params })
export const createUserVmBackup = (data) => http2.post(`${BASE}/backup/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const restoreUserVmBackup = (data) => http2.post(`${BASE}/backup/restore`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmBackup = (data) => http2.post(`${BASE}/backup/delete`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const setUserVmBackupLimit = (data) => http2.post(`${BASE}/backup/set_limit`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
// ========== 安全组 ==========
export const getUserVmPostGroupList = (params) => http2.get(`${BASE}/post_group/list`, { params })
export const getUserVmPostGroupDetail = (params) => http2.get(`${BASE}/post_group/detail`, { params })
export const getUserVmPostGroupUserList = (params) => http2.get(`${BASE}/post_group/user_list`, { params })
export const createUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const updateUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const bindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/bind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const unbindUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/unbind`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const applyUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/apply`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const setSharedUserVmPostGroup = (data) => http2.post(`${BASE}/post_group/set_shared`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmPostGroup = (params) => http2.delete(`${BASE}/post_group/delete`, { params })
export const enableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/enable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const disableUserVmPostGroupWhitelist = (data) => http2.post(`${BASE}/post_group/disable_whitelist`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const createUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/create_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const updateUserVmPostGroupRule = (data) => http2.post(`${BASE}/post_group/update_rule`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmPostGroupRule = (params) => http2.delete(`${BASE}/post_group/delete_rule`, { params })
// ========== 网络 ==========
export const getUserVmNetworkList = (params) => http2.get(`${BASE}/network/list`, { params })
export const getUserVmNetworkDetail = (params) => http2.get(`${BASE}/network/detail`, { params })
// ========== 组网 ==========
export const getUserVmNetworkingList = (params) => http2.get(`${BASE}/networking/list`, { params })
export const getUserVmNetworkingDetail = (params) => http2.get(`${BASE}/networking/detail`, { params })
export const createUserVmNetworking = (data) => http2.post(`${BASE}/networking/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const assignUserVmNetworking = (data) => http2.post(`${BASE}/networking/assign`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const removeUserVmNetworkingNetwork = (data) => http2.post(`${BASE}/networking/remove_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserVmNetworking = (params) => http2.delete(`${BASE}/networking/delete`, { params })
// ========== 用户商品 ==========
export const getUserGoodsList = (params) => http2.get(`${GOODS_BASE}/list`, { params })
export const getUserGoodsDetail = (params) => http2.get(`${GOODS_BASE}/detail`, { params })
export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params })
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
// ========== 流量策略 ==========
// 测试未通过(接口新增,待联调)
export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params })
export const updateUserVmTrafficPolicy = (data) => http2.post(`${BASE}/traffic_policy/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const addUserVmFixedTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_fixed`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const addUserVmTemporaryTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_temporary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
// ========== 到期提醒 ==========
export const getExpireRemindList = (params) => http2.get(`${GOODS_BASE}/expire_remind/list`, { params })
export const sendExpireRemind = (data) => http2.post(`${GOODS_BASE}/expire_remind/send`, data, { headers: { 'Content-Type': 'application/json' } })
+24
View File
@@ -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 })
+64
View File
@@ -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
View File
@@ -11,15 +11,20 @@ export function addDomain(data) {
}
// 删除域名白名单
export function deleteDomain(id) {
return request.post("/api/v1/admin/server/domain_withe/delete",{domain_id: id})
export function deleteDomain(data) {
return request.post("/api/v1/admin/server/domain_withe/delete",data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
// 批量删除域名白名单
export async function batchDeleteDomain(ids) {
let promises = []
for (let id of ids) {
promises.push(deleteDomain(id))
promises.push(deleteDomain({domain_id:id}))
}
return await Promise.all(promises)
}
+202
View File
@@ -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 } })
}
+10
View File
@@ -7,4 +7,14 @@ export const userLogin = (username,password) => {
export const getUserInfo = () => {
return request.get("/api/v1/users/info/info")
}
// 获取交换token(用于无感刷新)
export const getRefreshToken = (domain) => {
return request.get("/api/v1/users/info/refresh_token", { domain })
}
// 使用交换token获取新的access token
export const refreshAccessToken = (refresh_token) => {
return request.post("/api/v1/user/refresh_token", { refresh_token })
}
+61 -5
View File
@@ -5,8 +5,15 @@ import request from "@/utils/request.js";
* @returns {Promise}
*/
export function getTickerList(count, page, status) {
return request.get('/api/v1/admin/work_order/list', { count, page, status })
export function getTickerList(count, page, status, orderBy, order, userId, keyword) {
const params = { count, page }
if (status !== undefined && status !== '') params.status = status
if (orderBy) params.orderBy = orderBy
if (order) params.order = order
if (userId) params.user_id = userId
if (keyword) params.keyword = keyword
console.log('工单列表请求参数:', params) // 调试日志
return request.get('/api/v1/admin/work_order/list', params)
}
// 待处理
@@ -29,12 +36,12 @@ export function getCompletedTicketList(count, page) {
return getTickerList(count,page,3)
}
// 获取详情
// 获取工单详情
export function getTicketDetail(work_id) {
return request.get('/api/v1/admin/work_order/detail', { work_id })
}
// 回复
// 回复工单
export function replyTicket(work_id, content, files) {
return request.post('/api/v1/admin/work_order/reply', { work_id, content, files })
}
@@ -67,4 +74,53 @@ export async function parseFilesToImages(files) {
const fileIds = files.split(',')
return await Promise.all(fileIds.map(async (id) => await getFileImage(id.trim())))
}
}
/**获取工单数量 */
export function getTicketCount() {
return request.get('/api/v1/admin/work_order/count')
}
/**修改工单信息 */
export function updateTicketInfo(data) {
return request.post('/api/v1/admin/work_order/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**添加工单类型 */
export function addTicketType(data) {
return request.post('/api/v1/admin/work_order/add_type', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**修改工单类型 */
export function updateTicketType(data) {
return request.post('/api/v1/admin/work_order/update_type', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**删除工单类型 */
export function deleteTicketType(data) {
return request.delete('/api/v1/admin/work_order/delete_type', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**获取工单类型列表 */
export function getTicketTypeList(data) {
return request.get('/api/v1/admin/work_order/type_list', data)
}
/**修改工单回复信息 */
export function updateTicketReplayInfo(data){
return request.post('/api/v1/admin/work_order/update_reply',data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+272
View File
@@ -0,0 +1,272 @@
<template>
<el-dialog
:model-value="visible"
title="选择用户"
width="700px"
class="user-selector-dialog"
append-to-body
@update:model-value="handleVisibleChange"
>
<div class="user-selector-content">
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名、邮箱或ID"
clearable
@keyup.enter="handleSearch"
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #append>
<el-button @click="handleSearch">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button @click="handleReset" class="reset-btn">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
max-height="350"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="user_name" label="用户名" min-width="130">
<template #default="{ row }">
<div class="user-name-cell">
<el-avatar v-if="row.cover" :src="row.cover" :size="28" />
<el-avatar v-else :size="28">
{{ row.user_name?.charAt(0)?.toUpperCase() || 'U' }}
</el-avatar>
<span class="user-name">{{ row.user_name || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="180">
<template #default="{ row }">
<span class="text-ellipsis">{{ row.email || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.disable" type="danger" size="small">禁用</el-tag>
<el-tag v-else type="success" size="small">正常</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
background
small
class="selector-pagination"
/>
</div>
<template #footer>
<div class="dialog-footer">
<span v-if="selectedUser" class="selected-info">
已选择: <el-tag type="primary" size="small">{{ selectedUser.user_name }} (ID: {{ selectedUser.user_id }})</el-tag>
</span>
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getUserList } from '@/api/admin/user'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'select'])
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 visible 变化,打开时加载数据
watch(() => props.visible, (newVal) => {
if (newVal) {
selectedUser.value = null
fetchUserList()
}
})
const handleVisibleChange = (val) => {
emit('update:visible', val)
}
const closeDialog = () => {
emit('update:visible', false)
}
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(searchParams)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchUserList()
}
const handleCurrentChange = (row) => {
selectedUser.value = row
}
const handleSizeChange = (size) => {
searchParams.count = size
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
const confirmSelection = () => {
if (!selectedUser.value) {
ElMessage.warning('请选择一个用户')
return
}
emit('select', selectedUser.value)
closeDialog()
}
</script>
<style scoped>
.user-selector-content {
max-height: 500px;
overflow: hidden;
}
.selector-search {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
margin-bottom: 16px;
}
.search-input {
flex: 1;
max-width: 350px;
}
.reset-btn {
flex-shrink: 0;
}
.user-name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.user-name {
font-weight: 500;
color: #303133;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.selected-info {
margin-right: auto;
color: #606266;
font-size: 13px;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.current-row td) {
color: var(--el-color-primary);
}
:deep(.el-avatar) {
background-color: var(--el-color-primary-light-5);
color: #fff;
font-size: 12px;
}
</style>
+436
View File
@@ -0,0 +1,436 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="800px"
append-to-body
@close="handleClose"
>
<div class="avatar-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 文件列表 -->
<el-tab-pane label="文件" name="userFiles">
<div class="file-list-container">
<div class="file-list-header">
<h4>文件列表</h4>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新文件
</el-button>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in fileList"
:key="file.cover_id"
class="file-item"
:class="{ 'selected': selectedId === file.cover_id }"
@click="selectFile(file)"
>
<div class="file-preview">
<img
v-if="isImageFile(file)"
:src="file.url"
:alt="file.realName"
@error="handleImageError"
/>
<el-icon v-else class="file-icon"><Document /></el-icon>
</div>
<div class="file-info">
<p class="file-name">{{ file.realName }}</p>
<p class="file-size">{{ formatFileSize(file.size) }}</p>
</div>
</div>
</div>
<el-empty v-if="fileList.length === 0 && !loading" description="暂无文件" />
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 上传文件 -->
<el-tab-pane label="上传文件" name="upload">
<div class="upload-section">
<el-upload
:http-request="handleUpload"
:before-upload="beforeUpload"
:show-file-list="false"
accept="image/*"
drag
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png文件且不超过2MB
</div>
</template>
</el-upload>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedId"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload, UploadFilled, Document } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
import { closeAllMessage } from '../../utils/message'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
userId: {
type: [String, Number],
required: true
},
currentCoverId: {
type: [String, Number],
default: ''
},
title: {
type: String,
default: '选择文件'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('userFiles')
const fileList = ref([])
const loading = ref(false)
const selectedId = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
selectedId.value = props.currentCoverId
currentPage.value = 1
fetchFileList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取文件列表
const fetchFileList = async () => {
if (!props.userId) return
loading.value = true
fileList.value = [] // 清空列表
try {
const res = await getFileList({
page: currentPage.value,
count: pageSize.value
})
console.log("获取文件列表:", res)
if (res.data.code === 200) {
const list = res.data.data.list || []
total.value = res.data.data.all_count || 0
// 获取每个文件的详情
for (let i = 0; i < list.length; i++) {
try {
console.log("获取文件详情:", list[i].id)
const res2 = await getFileDetail({ file_id: list[i].id })
if (res2.data.code === 200) {
fileList.value.push({
url: res2.data.data.url,
cover_id: res2.data.data.data.id,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
})
}
} catch (error) {
console.error('获取文件详情失败:', error)
}
}
console.log("文件列表1237", fileList.value)
}
} catch (error) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.name === 'userFiles') {
currentPage.value = 1
fetchFileList()
}
}
// 分页处理
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchFileList()
}
const handlePageChange = (page) => {
currentPage.value = page
fetchFileList()
}
// 切换到上传标签页
const switchToUpload = () => {
activeTab.value = 'upload'
}
// 判断是否为图片文件
const isImageFile = (file) => {
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
const extension = file.realName?.split('.').pop()?.toLowerCase()
return imageTypes.includes(extension)
}
// 格式化文件大小
const formatFileSize = (size) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
// 选择文件
const selectFile = (file) => {
selectedId.value = file.cover_id
}
// 上传前验证
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 自定义上传
const handleUpload = async (options) => {
const { file } = options
const formData = new FormData()
formData.append('files', file)
formData.append('file_names', file.name)
formData.append('update_type', 'cover')
formData.append('open_down', 'true')
try {
const res = await uploadFile(formData)
console.log("上传文件:", res)
if (res.data.code === 200) {
ElMessage.success("上传成功")
// 重置到第一页并刷新文件列表
currentPage.value = 1
await fetchFileList()
// 切换到文件列表标签页
activeTab.value = 'userFiles'
// 自动选择新上传的文件
if (res.data.data?.id) {
selectedId.value = res.data.data.id
}
} else {
ElMessage.error(res.data.msg || '上传失败')
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('上传失败')
}
}
// 图片加载错误处理
const handleImageError = (event) => {
event.target.style.display = 'none'
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedId.value = ''
fileList.value = []
currentPage.value = 1
total.value = 0
}
// 确认选择
const handleConfirm = () => {
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.cover_id === selectedId.value)
emit('confirm', {
cover_id: selectedId.value,
url: selectedFile?.url || ''
})
handleClose()
}
}
</script>
<style scoped>
.avatar-selector {
min-height: 400px;
}
.file-list-container {
padding: 20px 0;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.file-list-header h4 {
margin: 0;
color: #303133;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
max-height: 400px;
overflow-y: auto;
}
.file-item {
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.file-item:hover {
border-color: #409EFF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.file-item.selected {
border-color: #409EFF;
background-color: #f0f9ff;
}
.file-preview {
width: 80px;
height: 80px;
margin: 0 auto 8px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
}
.file-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-preview .file-icon {
font-size: 32px;
color: #909399;
}
.file-info {
text-align: center;
}
.file-name {
font-size: 12px;
color: #303133;
margin: 0 0 4px 0;
word-break: break-all;
line-height: 1.3;
}
.file-size {
font-size: 11px;
color: #909399;
margin: 0;
}
.upload-section {
padding: 40px 20px;
text-align: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,392 @@
<template>
<el-dialog
v-model="visible"
title="选择优惠码"
width="900px"
append-to-body
@close="handleClose"
>
<div class="discount-code-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择优惠码 -->
<el-tab-pane label="选择优惠码" name="selectCode">
<div class="code-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索优惠码名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 优惠码列表表格 -->
<el-table
v-loading="loading"
:data="codeList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="id" label="优惠码ID" width="100" align="center" />
<el-table-column prop="name" label="优惠码名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="code" label="优惠码" width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="success" effect="plain">{{ row.code }}</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠值" width="100" align="right">
<template #default="{ row }">
<span v-if="row.percentage > 0" class="discount-value">{{ row.percentage }}%</span>
<span v-else class="discount-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="100" align="right">
<template #default="{ row }">
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
<span v-else>无限制</span>
</template>
</el-table-column>
<el-table-column label="使用次数" width="100" align="center">
<template #default="{ row }">
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
</template>
</el-table-column>
<el-table-column label="有效期" width="160" align="center">
<template #default="{ row }">
<span :class="{ 'expired': isExpired(row.endTime) }">
{{ formatDate(row.endTime) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="codeList.length === 0 && !loading" description="暂无优惠码数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedCode"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getDiscountCodeList } from '@/api/admin/discount'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的优惠码ID(用于回显)
currentCodeId: {
type: [String, Number],
default: ''
},
// 类型过滤:discount_code - 优惠码
codeType: {
type: String,
default: 'code'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectCode')
const loading = ref(false)
const codeList = ref([])
const total = ref(0)
const selectedCode = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectCode'
selectedCode.value = null
searchParams.page = 1
fetchCodeList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取优惠码列表
const fetchCodeList = async () => {
loading.value = true
codeList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count,
discount_type: props.codeType
}
if (searchParams.key) {
params.key = searchParams.key
}
const res = await getDiscountCodeList(params)
if (res.data.code === 200) {
codeList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
// 如果有当前选中的优惠码ID,自动选中
if (props.currentCodeId) {
const currentCode = codeList.value.find(
code => code.id === props.currentCodeId
)
if (currentCode) {
selectedCode.value = currentCode
}
}
} else {
ElMessage.error(res.data.msg || '获取优惠码列表失败')
}
} catch (error) {
console.error('获取优惠码列表失败:', error)
ElMessage.error('获取优惠码列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectCode') {
fetchCodeList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchCodeList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchCodeList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchCodeList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchCodeList()
}
// 选择优惠码
const handleCurrentChange = (row) => {
selectedCode.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedCode.value && row.id === selectedCode.value.id) {
return 'selected-row'
}
return ''
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedCode.value = null
codeList.value = []
searchParams.key = ''
searchParams.page = 1
total.value = 0
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 判断是否过期
const isExpired = (endTime) => {
if (!endTime) return false
return new Date(endTime) < new Date()
}
// 确认选择
const handleConfirm = () => {
if (selectedCode.value) {
emit('confirm', selectedCode.value)
handleClose()
} else {
ElMessage.warning('请选择一个优惠码')
}
}
</script>
<style scoped>
.discount-code-selector {
min-height: 450px;
}
.code-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.discount-value {
color: #e6a23c;
font-weight: 600;
}
.expired {
color: #f56c6c;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
@@ -0,0 +1,100 @@
<template>
<el-dialog v-model="visible" title="选择宿主机组" width="650px" append-to-body @close="handleClose">
<div class="selector-container">
<div class="filter-bar">
<el-input v-model="keyword" placeholder="搜索宿主机组名称" clearable style="width:200px" @keyup.enter="handleSearch" @clear="handleSearch" />
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</div>
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.note || '-' }}</template>
</el-table-column>
<el-table-column prop="serviceId" label="服务ID" width="80" />
</el-table>
<div class="pagination-wrapper" v-if="total > pageSize">
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { getHostGroupList } from '@/api/admin/kvmService'
const props = defineProps({
modelValue: { type: Boolean, default: false },
serviceId: { type: Number, default: 0 },
currentId: { type: Number, default: 0 }
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const selectedItem = ref(null)
const keyword = ref('')
const page = ref(1)
const pageSize = 10
const total = ref(0)
const filteredList = computed(() => {
if (!keyword.value) return list.value
const kw = keyword.value.toLowerCase()
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw))
})
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) { page.value = 1; loadList() }
})
watch(visible, (val) => emit('update:modelValue', val))
const handleSearch = () => { page.value = 1; loadList() }
const loadList = async () => {
loading.value = true
try {
const res = await getHostGroupList({ service_id: props.serviceId, page: page.value, count: pageSize })
const body = res?.data
if (body?.code === 200 && body?.data) {
const items = Array.isArray(body.data) ? body.data : (body.data.data || body.data.list || [])
list.value = items.map(i => ({
id: i.id,
name: i.name ?? i.Name,
note: i.note ?? i.Note,
serviceId: i.serviceId ?? i.service_id ?? 0,
serviceHostGroupId: i.serviceHostGroupId ?? 0
}))
total.value = body.data.total ?? body.data.all_count ?? list.value.length
}
} catch { /* ignore */ }
finally { loading.value = false }
}
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
const handleCurrentChange = (row) => { selectedItem.value = row }
const handleConfirm = () => {
if (selectedItem.value) {
emit('confirm', selectedItem.value)
visible.value = false
}
}
const handleClose = () => { selectedItem.value = null }
</script>
<style scoped>
.selector-container { min-height: 200px; }
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
:deep(.current-row) { background-color: #ecf5ff !important; }
:deep(.el-table__body tr) { cursor: pointer; }
</style>
@@ -0,0 +1,98 @@
<template>
<el-dialog v-model="visible" title="选择宿主机" width="700px" append-to-body @close="handleClose">
<div class="selector-container">
<div class="filter-bar">
<el-input v-model="keyword" placeholder="搜索宿主机名称/IP" clearable style="width:200px" @keyup.enter="loadList" @clear="loadList" />
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</div>
<el-table v-loading="loading" :data="filteredList" highlight-current-row @current-change="handleCurrentChange" :height="300" :row-class-name="rowClassName">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="ip" label="IP" min-width="130" />
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '在线' : '离线' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" v-if="total > pageSize">
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev,pager,next" small @current-change="loadList" />
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { getRemoteHostList } from '@/api/admin/kvmService'
const props = defineProps({
modelValue: { type: Boolean, default: false },
serviceId: { type: Number, default: 0 },
hostGroupId: { type: Number, default: 0 },
currentId: { type: Number, default: 0 }
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const selectedItem = ref(null)
const keyword = ref('')
const page = ref(1)
const pageSize = 10
const total = ref(0)
const filteredList = computed(() => {
if (!keyword.value) return list.value
const kw = keyword.value.toLowerCase()
return list.value.filter(i => (i.name || '').toLowerCase().includes(kw) || (i.ip || '').includes(kw))
})
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) { page.value = 1; loadList() }
})
watch(visible, (val) => emit('update:modelValue', val))
const loadList = async () => {
loading.value = true
try {
const params = { service_id: props.serviceId, page: page.value, count: pageSize }
if (props.hostGroupId) params.host_group_id = props.hostGroupId
const res = await getRemoteHostList(params)
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
const hosts = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
list.value = hosts.map(i => ({
id: i.id, name: i.name, ip: i.ip, is_active: i.is_active ?? true,
host_group_id: i.host_group_id
}))
total.value = inner.total ?? list.value.length
}
} catch { /* ignore */ }
finally { loading.value = false }
}
const rowClassName = ({ row }) => row.id === props.currentId ? 'current-row' : ''
const handleCurrentChange = (row) => { selectedItem.value = row }
const handleConfirm = () => {
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
}
const handleClose = () => { selectedItem.value = null }
</script>
<style scoped>
.selector-container { min-height: 200px; }
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 8px; }
:deep(.current-row) { background-color: #ecf5ff !important; }
:deep(.el-table__body tr) { cursor: pointer; }
</style>
+131
View File
@@ -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>
+684
View File
@@ -0,0 +1,684 @@
<template>
<el-dialog
v-model="visible"
title="选择图片"
width="900px"
append-to-body
@close="handleClose"
>
<div class="image-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 文件库 -->
<el-tab-pane label="文件库" name="fileLibrary">
<div class="file-list-container">
<div class="file-list-header">
<h4>图片文件库</h4>
<div class="header-actions">
<span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
已选 {{ selectedIds.size }} 个文件
</span>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新图片
</el-button>
</div>
</div>
<!-- 搜索过滤 -->
<div class="filter-section">
<el-input
v-model="searchKeyword"
placeholder="搜索文件名"
:prefix-icon="Search"
clearable
@input="handleSearch"
style="width: 300px;"
/>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in filteredFileList"
:key="file.id"
class="file-item"
:class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }"
@click="selectFile(file)"
>
<div class="file-check-badge" v-if="props.multiple && selectedIds.has(file.id)">
<el-icon><Select /></el-icon>
</div>
<div class="file-preview">
<img
:src="processImageUrl(file.url)"
:alt="file.realName"
@error="handleImageError"
/>
</div>
<div class="file-info">
<p class="file-name" :title="file.realName">{{ file.realName }}</p>
<p class="file-size">{{ formatFileSize(file.size) }}</p>
</div>
</div>
</div>
<el-empty v-if="filteredFileList.length === 0 && !loading" description="暂无图片文件" />
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[12, 24, 36, 48]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 上传图片 -->
<el-tab-pane label="上传图片" name="upload">
<div class="upload-section">
<el-upload
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
accept="image/*"
multiple
drag
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持jpgpnggifwebp等图片格式单个文件不超过5MB
</div>
</template>
</el-upload>
<!-- 待上传文件列表 -->
<div v-if="pendingFiles.length > 0" class="pending-files">
<div class="pending-header">
<h4>待上传文件 ({{ pendingFiles.length }})</h4>
<el-button type="danger" link @click="pendingFiles = []">清空</el-button>
</div>
<div class="pending-list">
<div v-for="(file, index) in pendingFiles" :key="index" class="pending-item">
<img :src="file.previewUrl" class="pending-preview" />
<span class="pending-name" :title="file.name">{{ file.name }}</span>
<span class="pending-size">{{ formatFileSize(file.size) }}</span>
<el-button type="danger" link size="small" @click="removePendingFile(index)">移除</el-button>
</div>
</div>
<el-button
type="primary"
@click="handleBatchUpload"
:loading="uploading"
style="margin-top: 16px; width: 100%;"
>
开始上传 ({{ pendingFiles.length }} 个文件)
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="props.multiple ? selectedIds.size === 0 : !selectedId"
>
确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload, UploadFilled, Search, Select, Delete } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
currentFileId: {
type: [String, Number],
default: ''
},
multiple: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('fileLibrary')
const fileList = ref([])
const loading = ref(false)
const selectedId = ref('')
const selectedIds = ref(new Set()) // 多选模式下选中的文件ID集合
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const searchKeyword = ref('')
const pendingFiles = ref([]) // 待上传文件列表
const uploading = ref(false) // 批量上传中
let fetchVersion = 0 // 防止 fetchFileList 竞态条件
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
selectedId.value = props.currentFileId
selectedIds.value = new Set()
currentPage.value = 1
searchKeyword.value = ''
fetchFileList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 过滤后的文件列表
const filteredFileList = computed(() => {
if (!searchKeyword.value) {
return fileList.value
}
return fileList.value.filter(file =>
file.realName?.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// 处理图片URL,确保正确显示
const processImageUrl = (url) => {
if (!url) return ''
// 先处理转义字符:将 \u0026 替换为 &
let processedUrl = url.replace(/\\u0026/g, '&')
// 再进行URL解码
return decodeURIComponent(processedUrl)
}
// 获取文件列表(带版本号防止竞态条件)
const fetchFileList = async () => {
const currentFetchVersion = ++fetchVersion
loading.value = true
try {
const res = await getFileList({
page: currentPage.value,
count: pageSize.value
})
// 如果有更新的请求发起,丢弃当前结果
if (currentFetchVersion !== fetchVersion) return
if (res.data.code === 200) {
const list = res.data.data.list || []
total.value = res.data.data.all_count || 0
// 并行获取所有文件详情(替代逐个串行,大幅提升速度)
const detailPromises = list.map(item =>
getFileDetail({ file_id: item.id })
.then(res2 => {
if (res2.data.code === 200) {
return {
id: res2.data.data.data.id,
url: res2.data.data.url,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
}
}
return null
})
.catch(error => {
console.error('获取文件详情失败:', error)
return null
})
)
const results = await Promise.all(detailPromises)
// 再次检查版本号,防止旧结果覆盖新结果
if (currentFetchVersion !== fetchVersion) return
fileList.value = results.filter(item => item !== null)
}
} catch (error) {
if (currentFetchVersion === fetchVersion) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
}
} finally {
if (currentFetchVersion === fetchVersion) {
loading.value = false
}
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.name === 'fileLibrary') {
currentPage.value = 1
fetchFileList()
}
}
// 处理搜索
const handleSearch = () => {
// 搜索时重置到第一页
currentPage.value = 1
}
// 分页处理
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchFileList()
}
const handlePageChange = (page) => {
currentPage.value = page
fetchFileList()
}
// 切换到上传标签页
const switchToUpload = () => {
activeTab.value = 'upload'
}
// 格式化文件大小
const formatFileSize = (size) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
// 选择文件
const selectFile = (file) => {
if (props.multiple) {
// 多选模式:切换选中状态
const newSet = new Set(selectedIds.value)
if (newSet.has(file.id)) {
newSet.delete(file.id)
} else {
newSet.add(file.id)
}
selectedIds.value = newSet
} else {
selectedId.value = file.id
}
}
// 文件选择变化(收集待上传文件)
const handleFileChange = (file) => {
const rawFile = file.raw
if (!rawFile) return
// 验证文件类型
const isImage = rawFile.type.startsWith('image/')
if (!isImage) {
ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
return
}
// 验证文件大小
const isLt5M = rawFile.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error(`${rawFile.name} 超过 5MB,已跳过`)
return
}
// 检查是否重复添加
const exists = pendingFiles.value.some(f => f.name === rawFile.name && f.size === rawFile.size)
if (exists) return
// 添加到待上传列表,生成本地预览URL
pendingFiles.value.push({
raw: rawFile,
name: rawFile.name,
size: rawFile.size,
previewUrl: URL.createObjectURL(rawFile)
})
}
// 移除待上传文件
const removePendingFile = (index) => {
const file = pendingFiles.value[index]
if (file?.previewUrl) {
URL.revokeObjectURL(file.previewUrl)
}
pendingFiles.value.splice(index, 1)
}
// 批量上传(所有文件合并为一次请求,多个 file_names 和 files 条目)
const handleBatchUpload = async () => {
if (pendingFiles.value.length === 0) {
ElMessage.warning('请先选择要上传的文件')
return
}
uploading.value = true
const formData = new FormData()
pendingFiles.value.forEach(file => {
formData.append('file_names', file.name)
formData.append('files', file.raw)
})
formData.append('update_type', 'cover')
formData.append('open_down', 'true')
try {
const res = await uploadFile(formData)
if (res.data.code === 200) {
const count = pendingFiles.value.length
// 释放所有预览URL
pendingFiles.value.forEach(f => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
pendingFiles.value = []
ElMessage.success(`成功上传 ${count} 个文件`)
// 刷新文件列表并切换到文件库
currentPage.value = 1
await fetchFileList()
activeTab.value = 'fileLibrary'
} else {
ElMessage.error(res.data.msg || '上传失败')
}
} catch (error) {
console.error('批量上传失败:', error)
ElMessage.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
// 图片加载错误处理
const handleImageError = (event) => {
event.target.style.display = 'none'
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedId.value = ''
selectedIds.value = new Set()
fileList.value = []
currentPage.value = 1
total.value = 0
searchKeyword.value = ''
// 清理待上传文件的预览URL
pendingFiles.value.forEach(f => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
pendingFiles.value = []
}
// 确认选择
const handleConfirm = () => {
if (props.multiple) {
// 多选模式:返回选中的文件数组
if (selectedIds.value.size === 0) return
const selectedFiles = fileList.value
.filter(file => selectedIds.value.has(file.id))
.map(file => ({
id: file.id,
url: file.url || '',
realName: file.realName || ''
}))
emit('confirm', selectedFiles)
handleClose()
} else {
// 单选模式:返回单个文件对象
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.id === selectedId.value)
emit('confirm', {
id: selectedId.value,
url: selectedFile?.url || '',
realName: selectedFile?.realName || ''
})
handleClose()
}
}
}
</script>
<style scoped>
.image-selector {
min-height: 500px;
}
.file-list-container {
padding: 20px 0;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.file-list-header h4 {
margin: 0;
color: #303133;
}
.filter-section {
margin-bottom: 20px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
max-height: 450px;
overflow-y: auto;
padding: 10px 0;
}
.file-item {
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
background: #fff;
}
.file-item:hover {
border-color: #409EFF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.file-item.selected {
border-color: #409EFF;
background-color: #f0f9ff;
}
.file-item {
position: relative;
}
.file-check-badge {
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
background-color: #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
z-index: 1;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.4);
}
.selected-count {
color: #409EFF;
font-weight: 600;
font-size: 14px;
margin-right: 12px;
}
.header-actions {
display: flex;
align-items: center;
}
.file-preview {
width: 100px;
height: 100px;
margin: 0 auto 8px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
}
.file-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-info {
text-align: center;
}
.file-name {
font-size: 12px;
color: #303133;
margin: 0 0 4px 0;
word-break: break-all;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 11px;
color: #909399;
margin: 0;
}
.upload-section {
padding: 20px;
text-align: center;
}
/* 待上传文件列表 */
.pending-files {
margin-top: 20px;
text-align: left;
}
.pending-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.pending-header h4 {
margin: 0;
color: #303133;
font-size: 14px;
}
.pending-list {
max-height: 240px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 6px;
}
.pending-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
gap: 10px;
}
.pending-item:last-child {
border-bottom: none;
}
.pending-item:hover {
background-color: #fafafa;
}
.pending-preview {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid #ebeef5;
}
.pending-name {
flex: 1;
font-size: 13px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pending-size {
font-size: 12px;
color: #909399;
flex-shrink: 0;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
+124
View File
@@ -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>
+148
View File
@@ -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>
+90
View File
@@ -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>
+234
View File
@@ -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>
+418
View File
@@ -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>
+444
View File
@@ -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>
+567
View File
@@ -0,0 +1,567 @@
<template>
<el-dialog
v-model="visible"
title="选择用户"
width="900px"
append-to-body
@close="handleClose"
>
<div class="user-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择用户 -->
<el-tab-pane label="选择用户" name="selectUser">
<div class="user-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="用户状态">
<el-select
v-model="searchParams.status"
placeholder="全部状态"
clearable
style="width: 120px"
>
<el-option label="正常" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
<el-button type="success" @click="switchToAdd" :icon="Plus">
添加新用户
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 用户列表表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="user_id" label="用户ID" width="100" align="center" />
<el-table-column prop="user_name" label="用户名" min-width="120">
<template #default="{ row }">
<div class="user-info-cell">
<el-avatar :size="32" :src="row.cover">
<el-icon><User /></el-icon>
</el-avatar>
<span class="user-name">{{ row.user_name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" width="130" show-overflow-tooltip />
<!-- <el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column> -->
<el-table-column prop="created_at" label="注册时间" width="160" show-overflow-tooltip />
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="userList.length === 0 && !loading" description="暂无用户数据" />
</div>
</el-tab-pane>
<!-- 添加用户 -->
<el-tab-pane label="添加用户" name="addUser">
<div class="add-user-section">
<el-form
ref="addFormRef"
:model="addForm"
:rules="addFormRules"
label-width="100px"
class="add-user-form"
>
<el-form-item label="用户名" prop="user_name">
<el-input
v-model="addForm.user_name"
placeholder="请输入用户名"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="addForm.email"
placeholder="请输入邮箱地址"
type="email"
/>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input
v-model="addForm.phone"
placeholder="请输入手机号"
maxlength="11"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="addForm.password"
placeholder="请输入密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="addForm.confirmPassword"
placeholder="请再次输入密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAddUser" :loading="addLoading">
<el-icon><Plus /></el-icon>
立即创建
</el-button>
<el-button @click="resetAddForm">
<el-icon><Refresh /></el-icon>
重置表单
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedUser"
v-if="activeTab === 'selectUser'"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
import { getUserList, createTask } from '@/api/admin/user'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的用户ID(用于回显)
currentUserId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectUser')
const loading = ref(false)
const addLoading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const addFormRef = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
status: '',
page: 1,
count: 10
})
// 添加用户表单
const addForm = reactive({
user_name: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
})
// 密码确认验证
const validateConfirmPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== addForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
// 添加用户表单验证规则
const addFormRules = {
user_name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '用户名长度在 2 到 50 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectUser'
selectedUser.value = null
searchParams.page = 1
fetchUserList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
userList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count,
key: searchParams.key || ''
}
const res = await getUserList(params)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
// 如果有当前选中的用户ID,自动选中
if (props.currentUserId) {
const currentUser = userList.value.find(
user => user.user_id === props.currentUserId
)
if (currentUser) {
selectedUser.value = currentUser
}
}
} else {
ElMessage.error(res.data.msg || '获取用户列表失败')
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectUser') {
fetchUserList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.status = ''
searchParams.page = 1
fetchUserList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
// 切换到添加用户标签页
const switchToAdd = () => {
activeTab.value = 'addUser'
}
// 选择用户
const handleCurrentChange = (row) => {
selectedUser.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedUser.value && row.user_id === selectedUser.value.user_id) {
return 'selected-row'
}
return ''
}
// 添加用户
const handleAddUser = async () => {
if (!addFormRef.value) return
await addFormRef.value.validate(async (valid) => {
if (!valid) return
addLoading.value = true
try {
const formData = new FormData()
formData.append('user_name', addForm.user_name)
formData.append('email', addForm.email)
if (addForm.phone) {
formData.append('phone', addForm.phone)
}
formData.append('password', addForm.password)
const res = await createTask(formData)
if (res.data.code === 200) {
ElMessage.success('用户创建成功')
// 获取新创建的用户信息
const newUser = res.data.data
// 自动选择新创建的用户
if (newUser) {
selectedUser.value = {
user_id: newUser.user_id || newUser.id,
user_name: newUser.user_name || addForm.user_name,
email: newUser.email || addForm.email,
phone: newUser.phone || addForm.phone,
...newUser
}
// 触发确认事件并关闭弹窗
emit('confirm', selectedUser.value)
handleClose()
} else {
// 如果没有返回用户信息,切换到选择标签页并刷新列表
activeTab.value = 'selectUser'
searchParams.page = 1
await fetchUserList()
}
// 重置表单
resetAddForm()
} else {
ElMessage.error(res.data.msg || '用户创建失败')
}
} catch (error) {
console.error('用户创建失败:', error)
ElMessage.error('用户创建失败')
} finally {
addLoading.value = false
}
})
}
// 重置添加表单
const resetAddForm = () => {
addForm.user_name = ''
addForm.email = ''
addForm.phone = ''
addForm.password = ''
addForm.confirmPassword = ''
addFormRef.value?.resetFields()
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedUser.value = null
userList.value = []
searchParams.key = ''
searchParams.status = ''
searchParams.page = 1
total.value = 0
resetAddForm()
}
// 确认选择
const handleConfirm = () => {
if (selectedUser.value) {
emit('confirm', selectedUser.value)
handleClose()
} else {
ElMessage.warning('请选择一个用户')
}
}
</script>
<style scoped>
.user-selector {
min-height: 450px;
}
.user-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.user-info-cell {
display: flex;
align-items: center;
gap: 10px;
}
.user-name {
font-weight: 500;
color: #303133;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.add-user-section {
padding: 30px 60px;
}
.add-user-form {
max-width: 500px;
margin: 0 auto;
}
.add-user-form :deep(.el-input) {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
@@ -0,0 +1,134 @@
<template>
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
<div class="selector-container">
<div class="filter-bar">
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-tag v-if="filterType" :type="filterType === 'bridge' ? 'success' : 'warning'" size="small" effect="dark">{{ filterType === 'bridge' ? '网桥' : 'NAT' }}</el-tag>
<el-tag v-if="filterUnused" type="success" size="small" effect="dark">仅未占用</el-tag>
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
<el-option label="IPv4" value="ipv4" />
<el-option label="IPv6" value="ipv6" />
</el-select>
<el-button :icon="Refresh" @click="loadList" circle />
</div>
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
:height="340" :row-class-name="rowClassName" size="small" stripe>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
<el-table-column prop="gateway" label="网关" min-width="120" />
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
</el-table>
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
@size-change="s => { pageSize = s; page = 1; loadList() }"
@current-change="p => { page = p; loadList() }" />
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: space-between; width: 100%">
<el-button v-if="props.showCreateButton" type="success" @click="handleCreate">创建网络</el-button>
<div style="display: flex; gap: 8px">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
</div>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getUserVmNetworkList } from '@/api/admin/userVm'
const props = defineProps({
modelValue: { type: Boolean, default: false },
userGoodsId: { type: Number, default: 0 },
filterType: { type: String, default: '' },
filterUnused: { type: Boolean, default: false },
showCreateButton: { type: Boolean, default: true }
})
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const keyword = ref('')
const ipVersionFilter = ref('')
const selectedItem = ref(null)
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) {
page.value = 1
keyword.value = ''
ipVersionFilter.value = ''
selectedItem.value = null
loadList()
}
})
watch(visible, (val) => emit('update:modelValue', val))
const handleSearch = () => { page.value = 1; loadList() }
const loadList = async () => {
if (!props.userGoodsId) return
loading.value = true
try {
const params = { user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value }
if (keyword.value) params.key = keyword.value
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
const res = await getUserVmNetworkList(params)
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
let all = inner.data || (Array.isArray(inner) ? inner : [])
if (props.filterType) {
all = all.filter(n => n.type === props.filterType)
}
if (props.filterUnused) {
all = all.filter(n => !n.vm_id)
}
list.value = all
total.value = inner.meta?.count ?? inner.total ?? all.length
} else { list.value = []; total.value = 0 }
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
}
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
const handleCurrentChange = (row) => { selectedItem.value = row }
const handleConfirm = () => {
if (selectedItem.value) {
emit('confirm', selectedItem.value)
visible.value = false
}
}
const handleClose = () => { selectedItem.value = null }
const handleCreate = () => {
visible.value = false
emit('create')
}
</script>
<style scoped>
.selector-container { min-height: 200px; }
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.selected-row) { background-color: #ecf5ff !important; }
:deep(.el-table__body tr) { cursor: pointer; }
</style>
@@ -0,0 +1,143 @@
<template>
<el-dialog v-model="visible" title="选择安全组" width="640px" append-to-body @close="handleClose">
<div class="selector-toolbar">
<el-input v-model="keyword" placeholder="搜索安全组名称" clearable style="width:200px"
@keyup.enter="loadList" @clear="loadList">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="showCreate = true">新增安全组</el-button>
</div>
<el-table :data="list" v-loading="loading" highlight-current-row
@current-change="row => selected = row" :height="280" stripe size="small">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
<el-table-column label="方向" width="80">
<template #default="{ row }">
<el-tag :type="row.direction === 'in' ? 'success' : 'warning'" size="small">
{{ row.direction === 'in' ? '入站' : row.direction === 'out' ? '出站' : (row.direction || '-') }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="白名单" width="80">
<template #default="{ row }">
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="共享" width="70">
<template #default="{ row }">
<el-tag :type="row.shared ? 'success' : 'info'" size="small">{{ row.shared ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无安全组" />
<div class="selector-footer-bar">
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
layout="total,sizes,prev,pager,next" small background
@size-change="s => { pageSize = s; page = 1; loadList() }"
@current-change="p => { page = p; loadList() }" />
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
</template>
</el-dialog>
<!-- 新增安全组弹窗 -->
<el-dialog v-model="showCreate" title="新增安全组" width="440px" append-to-body destroy-on-close>
<el-form :model="createForm" label-width="90px">
<el-form-item label="名称" required>
<el-input v-model="createForm.name" placeholder="安全组名称" />
</el-form-item>
<el-form-item label="方向">
<el-select v-model="createForm.direction" style="width:100%">
<el-option label="入站 (in)" value="in" />
<el-option label="出站 (out)" value="out" />
</el-select>
</el-form-item>
<el-form-item label="锁定">
<el-switch v-model="createForm.lock" />
</el-form-item>
<el-form-item label="白名单">
<el-switch v-model="createForm.drop_all" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreate = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getUserVmPostGroupUserList, createUserVmPostGroup } from '@/api/admin/userVm'
const props = defineProps({
modelValue: { type: Boolean, default: false },
userGoodsId: { type: Number, default: 0 }
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const keyword = ref('')
const selected = ref(null)
const showCreate = ref(false)
const createLoading = ref(false)
const createForm = reactive({ name: '', direction: 'in', lock: false, drop_all: false })
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
watch(visible, (v) => emit('update:modelValue', v))
const loadList = async () => {
if (!props.userGoodsId) return
loading.value = true
try {
const params = { user_goods_id: props.userGoodsId, page: page.value, page_size: pageSize.value }
if (keyword.value) params.keyword = keyword.value
const res = await getUserVmPostGroupUserList(params)
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
list.value = d.groups || d.data || (Array.isArray(d) ? d : [])
total.value = d.total ?? list.value.length
}
} catch { /* */ } finally { loading.value = false }
}
const submitCreate = async () => {
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
createLoading.value = true
try {
const res = await createUserVmPostGroup({
user_goods_id: props.userGoodsId,
name: createForm.name,
direction: createForm.direction,
lock: createForm.lock,
drop_all: createForm.drop_all
})
if (res?.data?.code === 200) {
ElMessage.success('创建成功')
showCreate.value = false
Object.assign(createForm, { name: '', direction: 'in', lock: false, drop_all: false })
loadList()
} else ElMessage.error(res?.data?.message || '创建失败')
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
}
const handleClose = () => { visible.value = false }
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
</script>
<style scoped>
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
</style>
@@ -0,0 +1,144 @@
<template>
<el-dialog v-model="visible" title="选择数据卷进行挂载" width="680px" append-to-body @close="handleClose">
<div class="selector-toolbar">
<el-input v-model="keyword" placeholder="搜索数据卷名称" clearable style="width:200px"
@keyup.enter="loadList" @clear="loadList">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建数据卷</el-button>
</div>
<el-table :data="list" v-loading="loading" highlight-current-row
@current-change="row => selected = row" :height="280" stripe size="small">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
<el-table-column label="大小" width="80">
<template #default="{ row }">{{ row.size }} GB</template>
</el-table-column>
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="挂载" width="80">
<template #default="{ row }">
<el-tag :type="row.is_mount ? 'success' : 'info'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无数据卷" />
<div class="selector-footer-bar">
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
layout="total,sizes,prev,pager,next" small background
@size-change="s => { pageSize = s; page = 1; loadList() }"
@current-change="p => { page = p; loadList() }" />
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :disabled="!selected || !!selected.is_mount" @click="handleConfirm">
{{ selected?.is_mount ? '已挂载' : '确定挂载' }}
</el-button>
</template>
</el-dialog>
<!-- 新建数据卷弹窗 -->
<el-dialog v-model="showCreate" title="新建数据卷" width="440px" append-to-body destroy-on-close>
<el-form :model="createForm" label-width="100px">
<el-form-item label="名称" required><el-input v-model="createForm.name" placeholder="数据卷名称" /></el-form-item>
<el-form-item label="大小">
<div class="unit-input-row">
<el-input-number v-model="createForm.size" :min="1" controls-position="right" style="flex:1" />
<el-select v-model="createForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
</div>
</el-form-item>
<el-form-item label="目标设备名"><el-input v-model="createForm.target_device" placeholder="不填自动生成" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreate = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getUserVmVolumeList, createUserVmVolume } from '@/api/admin/userVm'
const props = defineProps({
modelValue: { type: Boolean, default: false },
userGoodsId: { type: Number, default: 0 }
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const keyword = ref('')
const selected = ref(null)
const showCreate = ref(false)
const createLoading = ref(false)
const createForm = reactive({ name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
watch(visible, (v) => emit('update:modelValue', v))
const loadList = async () => {
if (!props.userGoodsId) return
loading.value = true
try {
const res = await getUserVmVolumeList({ user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
list.value = d.data || (Array.isArray(d) ? d : [])
total.value = d.all_count ?? d.total ?? list.value.length
}
} catch { /* */ } finally { loading.value = false }
}
const submitCreate = async () => {
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
createLoading.value = true
try {
const sizeGb = createForm._sizeUnit === 'TB' ? createForm.size * 1024 : createForm.size
const res = await createUserVmVolume({
user_goods_id: props.userGoodsId,
name: createForm.name,
size: sizeGb,
target_device: createForm.target_device
})
if (res?.data?.code === 200) {
ElMessage.success('创建成功')
showCreate.value = false
Object.assign(createForm, { name: '', size: 10, _sizeUnit: 'GB', target_device: '' })
loadList()
} else ElMessage.error(res?.data?.message || '创建失败')
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
}
const handleClose = () => { visible.value = false }
const handleConfirm = () => {
if (selected.value && !selected.value.is_mount) {
emit('confirm', selected.value)
handleClose()
}
}
</script>
<style scoped>
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.unit-select { width: 90px; flex-shrink: 0; }
</style>
+110
View File
@@ -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>
+389
View File
@@ -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>
+537 -42
View File
@@ -1,23 +1,36 @@
<template>
<div class="admin-layout">
<div class="admin-layout" :class="{ 'sidebar-collapsed': isCollapsed, 'mobile-open': isMobileMenuOpen }">
<!-- 移动端遮罩层 -->
<div class="mobile-overlay" v-if="isMobileMenuOpen" @click="closeMobileMenu"></div>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar" :class="{ 'collapsed': isCollapsed }">
<div class="logo-container">
<h1 class="title">零零七云计算后台控制面板</h1>
<img src="@/assets/logo.png" alt="Logo" class="logo-img" v-show="!isCollapsed" />
<img src="@/assets/logo.svg" alt="Logo" class="logo-img-mini" v-show="isCollapsed" />
</div>
<el-scrollbar>
<el-scrollbar class="sidebar-scrollbar">
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
background-color="#ffffff"
text-color="#333333"
active-text-color="#1890ff"
background-color="transparent"
text-color="#34495e"
active-text-color="#2c3e50"
:unique-opened="true"
:collapse="isCollapsed"
:collapse-transition="false"
router
>
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
</el-menu>
</el-scrollbar>
<!-- 收缩按钮 -->
<div class="collapse-btn" @click="toggleCollapse">
<el-icon :size="18">
<Fold v-if="!isCollapsed" />
<Expand v-else />
</el-icon>
</div>
</div>
<!-- 主区域 -->
@@ -25,10 +38,14 @@
<!-- 顶部导航 -->
<div class="navbar">
<div class="navbar-left">
<!-- 移动端菜单按钮 -->
<el-button type="text" class="mobile-menu-btn" @click="toggleMobileMenu">
<el-icon :size="22"><Menu /></el-icon>
</el-button>
<breadcrumb />
</div>
<div class="navbar-right">
<div class="navbar-item">
<div class="navbar-item hidden-mobile">
<el-tooltip content="全屏" placement="bottom">
<el-button type="text" class="header-btn" @click="toggleFullScreen">
<el-icon :size="18"><full-screen /></el-icon>
@@ -39,9 +56,9 @@
<div class="navbar-item">
<el-dropdown trigger="click">
<div class="avatar-container">
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
<span class="username">{{ userStore.userInfo.user_name }}</span>
<el-icon class="el-icon--right"><arrow-down /></el-icon>
<el-avatar :size="32" :src="userStore.getUserAvatar() || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'" />
<span class="username hidden-mobile">{{ userStore.userInfo.user_name }}</span>
<el-icon class="el-icon--right hidden-mobile"><arrow-down /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
@@ -81,7 +98,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SidebarMenuItem from './SidebarMenuItem.vue'
import Breadcrumb from './Breadcrumb.vue'
@@ -92,7 +109,10 @@ import {
ArrowDown,
User,
Key,
SwitchButton
SwitchButton,
Fold,
Expand,
Menu
} from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import { ElMessageBox } from 'element-plus'
@@ -105,11 +125,46 @@ const router = useRouter()
// 侧边栏菜单数据
const menus = ref(menuConfig)
// 侧边栏收缩状态
const isCollapsed = ref(false)
// 移动端菜单状态
const isMobileMenuOpen = ref(false)
// 检测是否是移动端
const isMobile = ref(false)
const checkMobile = () => {
isMobile.value = window.innerWidth <= 768
// 移动端默认收起侧边栏
if (isMobile.value) {
isCollapsed.value = false
isMobileMenuOpen.value = false
}
}
// 获取当前激活的菜单项
const activeMenu = computed(() => {
return route.path
})
// 切换侧边栏收缩
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
// 保存状态到localStorage
localStorage.setItem('sidebarCollapsed', isCollapsed.value)
}
// 切换移动端菜单
const toggleMobileMenu = () => {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
// 关闭移动端菜单
const closeMobileMenu = () => {
isMobileMenuOpen.value = false
}
// 切换全屏
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
@@ -129,9 +184,35 @@ const handleLogout = () => {
type: 'warning'
}).then(() => {
localStorage.removeItem('token')
localStorage.removeItem('tokenExpire')
localStorage.removeItem('userInfo')
userStore.clearUserInfo()
router.push('/login')
}).catch(() => {})
}
// 监听路由变化,移动端自动关闭菜单
router.afterEach(() => {
if (isMobile.value) {
closeMobileMenu()
}
})
onMounted(() => {
// 恢复侧边栏状态
const savedState = localStorage.getItem('sidebarCollapsed')
if (savedState !== null) {
isCollapsed.value = savedState === 'true'
}
// 检测设备类型
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
@@ -141,44 +222,95 @@ const handleLogout = () => {
width: 100%;
}
/* 移动端遮罩层 */
.mobile-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
}
/* 侧边栏样式 */
.sidebar {
width: 240px;
width: 260px;
height: 100%;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-right: 1px solid #e1e8ed;
overflow: hidden;
z-index: 20;
z-index: 999;
transition: width 0.3s ease;
display: flex;
flex-direction: column;
position: relative;
}
.sidebar.collapsed {
width: 64px;
}
.logo-container {
height: 60px;
height: 70px;
display: flex;
align-items: center;
padding: 0 16px;
color: #333;
justify-content: center;
padding: 0 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
overflow: hidden;
border-bottom: 1px solid #e1e8ed;
flex-shrink: 0;
}
.logo {
width: 32px;
.logo-img {
height: 50px;
width: auto;
object-fit: contain;
}
.logo-img-mini {
height: 32px;
margin-right: 10px;
width: 32px;
object-fit: contain;
}
.title {
font-size: 18px;
font-weight: 600;
white-space: nowrap;
.sidebar-scrollbar {
flex: 1;
overflow: hidden;
color: #1890ff;
}
.sidebar-menu {
border-right: none;
height: calc(100vh - 60px);
min-height: 100%;
background-color: transparent !important;
padding: 0;
}
/* 收缩按钮 */
.collapse-btn {
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e8ed;
cursor: pointer;
color: #7f8c8d;
transition: all 0.2s ease;
flex-shrink: 0;
}
.collapse-btn:hover {
color: #2c3e50;
background-color: #f8f9fa;
}
/* 移动端菜单按钮 */
.mobile-menu-btn {
display: none;
margin-right: 12px;
padding: 8px;
color: #34495e;
}
/* 主容器样式 */
@@ -188,49 +320,56 @@ const handleLogout = () => {
flex-direction: column;
background-color: #f0f2f5;
overflow: hidden;
min-width: 0;
}
/* 顶部导航栏样式 */
.navbar {
height: 60px;
padding: 0 15px;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0 20px;
background-color: #ffffff;
border-bottom: 1px solid #e1e8ed;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 10;
flex-shrink: 0;
}
.navbar-left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.navbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.navbar-item {
padding: 0 10px;
height: 60px;
display: flex;
align-items: center;
}
.header-btn {
height: 40px;
width: 40px;
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
color: #34495e;
transition: all 0.2s ease;
border-radius: 0;
}
.header-btn:hover {
background-color: #f5f7fa;
border-radius: 4px;
background-color: #f8f9fa;
color: #2c3e50;
}
.avatar-container {
@@ -239,16 +378,31 @@ const handleLogout = () => {
cursor: pointer;
padding: 0 12px;
height: 60px;
gap: 8px;
transition: all 0.2s ease;
border-radius: 0;
}
.avatar-container:hover {
background-color: rgba(0, 0, 0, 0.025);
background-color: #f8f9fa;
}
.username {
margin: 0 8px;
margin: 0;
font-size: 14px;
color: #606266;
font-weight: 500;
color: #2c3e50;
}
:deep(.avatar-container .el-icon--right) {
color: #7f8c8d;
font-size: 12px;
margin-left: 4px;
transition: color 0.2s ease;
}
.avatar-container:hover :deep(.el-icon--right) {
color: #34495e;
}
/* 内容区域样式 */
@@ -271,12 +425,353 @@ const handleLogout = () => {
opacity: 0;
}
/* 移动端隐藏元素 */
.hidden-mobile {
display: flex;
}
/* 移动端响应式 */
@media (max-width: 768px) {
.mobile-overlay {
display: block;
}
.sidebar {
position: fixed;
left: -260px;
top: 0;
bottom: 0;
transition: left 0.3s ease;
}
.sidebar.collapsed {
width: 260px;
left: -260px;
}
.admin-layout.mobile-open .sidebar {
left: 0;
}
.admin-layout.mobile-open .sidebar.collapsed {
left: 0;
}
.collapse-btn {
display: none;
}
.mobile-menu-btn {
display: flex;
}
.hidden-mobile {
display: none !important;
}
.navbar {
padding: 0 12px;
}
.content-container {
padding: 12px;
}
.main-container {
width: 100%;
}
}
:deep(.el-dropdown-menu) {
border-radius: 0;
border: 1px solid #e1e8ed;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
padding: 4px 0;
}
:deep(.el-dropdown-menu__item) {
display: flex;
align-items: center;
color: #34495e;
transition: all 0.2s ease;
padding: 8px 16px;
}
:deep(.el-dropdown-menu__item i) {
margin-right: 8px;
color: #7f8c8d;
}
:deep(.el-dropdown-menu__item:hover) {
background-color: #f8f9fa;
color: #2c3e50;
}
:deep(.el-dropdown-menu__item:hover i) {
color: #2c3e50;
}
:deep(.el-dropdown-menu__item.is-divided) {
border-top: 1px solid #e1e8ed;
}
/* 侧边栏滚动条样式优化 */
:deep(.sidebar-scrollbar .el-scrollbar__wrap) {
overflow-x: hidden;
}
:deep(.sidebar-scrollbar .el-scrollbar__view) {
height: 100%;
}
/* 自定义滚动条样式 */
:deep(.sidebar-scrollbar .el-scrollbar__bar) {
opacity: 0.3;
}
:deep(.sidebar-scrollbar .el-scrollbar__thumb) {
background-color: rgba(255, 255, 255, 0.2);
transition: background-color 0.2s;
}
:deep(.sidebar-scrollbar .el-scrollbar__thumb:hover) {
background-color: rgba(255, 255, 255, 0.35);
}
/* Element Plus 菜单项样式优化 */
:deep(.el-menu) {
border-right: none;
padding: 8px 0;
}
/* 一级菜单标题(有子菜单) */
:deep(.el-sub-menu__title) {
height: 48px;
line-height: 48px;
margin: 2px 8px;
padding: 0 16px !important;
border-radius: 6px;
transition: all 0.2s ease;
color: #34495e !important;
font-weight: 500;
font-size: 14px;
}
:deep(.el-sub-menu__title:hover) {
background-color: #f5f7fa !important;
color: #2c3e50 !important;
}
/* 一级菜单项(无子菜单) */
:deep(.sidebar-menu > .el-menu-item) {
height: 48px;
line-height: 48px;
margin: 2px 8px;
padding: 0 16px !important;
border-radius: 6px;
transition: all 0.2s ease;
color: #34495e !important;
font-weight: 500;
font-size: 14px;
}
:deep(.sidebar-menu > .el-menu-item:hover) {
background-color: #f5f7fa !important;
color: #2c3e50 !important;
}
:deep(.sidebar-menu > .el-menu-item.is-active) {
background-color: rgba(44, 62, 80, 0.08) !important;
color: #2c3e50 !important;
font-weight: 600;
}
:deep(.sidebar-menu > .el-menu-item.is-active::before) {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 24px;
background-color: #2c3e50;
border-radius: 0 2px 2px 0;
}
/* 展开的一级菜单标题 */
:deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
color: #2c3e50 !important;
font-weight: 600;
background-color: #f5f7fa !important;
}
/* 二级菜单容器 */
:deep(.sidebar-menu > .el-sub-menu > .el-menu) {
background-color: transparent !important;
padding: 4px 0 8px 0;
}
/* 二级菜单项 */
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item) {
height: 40px;
line-height: 40px;
margin: 2px 8px 2px 16px;
padding: 0 16px 0 28px !important;
border-radius: 6px;
font-size: 13px;
color: #606266 !important;
background-color: transparent !important;
position: relative;
font-weight: 400;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
background-color: #c0c4cc;
border-radius: 50%;
transition: all 0.2s ease;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
background-color: #f5f7fa !important;
color: #2c3e50 !important;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
background-color: #7f8c8d;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
background-color: rgba(44, 62, 80, 0.08) !important;
color: #2c3e50 !important;
font-weight: 500;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
width: 6px;
height: 6px;
background-color: #2c3e50;
}
/* 二级菜单中的子菜单标题(三级菜单父级) */
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title) {
height: 40px;
line-height: 40px;
margin: 2px 8px 2px 16px;
padding: 0 16px 0 28px !important;
border-radius: 6px;
font-size: 13px;
color: #606266 !important;
font-weight: 400;
position: relative;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title::before) {
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
background-color: #c0c4cc;
border-radius: 50%;
transition: all 0.2s ease;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover) {
background-color: #f5f7fa !important;
color: #2c3e50 !important;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-sub-menu__title:hover::before) {
background-color: #7f8c8d;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title) {
color: #2c3e50 !important;
font-weight: 500;
background-color: #f5f7fa !important;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu.is-opened > .el-sub-menu__title::before) {
width: 6px;
height: 6px;
background-color: #2c3e50;
}
/* 三级菜单容器 */
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu) {
background-color: transparent !important;
padding: 4px 0;
}
/* 三级菜单项 */
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item) {
height: 36px;
line-height: 36px;
margin: 2px 8px 2px 28px;
padding: 0 16px 0 24px !important;
border-radius: 6px;
font-size: 13px;
color: #909399 !important;
background-color: transparent !important;
position: relative;
font-weight: 400;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item::before) {
content: '-';
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #c0c4cc;
font-size: 12px;
transition: all 0.2s ease;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover) {
background-color: #f5f7fa !important;
color: #606266 !important;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item:hover::before) {
color: #7f8c8d;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active) {
background-color: rgba(44, 62, 80, 0.08) !important;
color: #2c3e50 !important;
font-weight: 500;
}
:deep(.sidebar-menu > .el-sub-menu > .el-menu > .el-sub-menu > .el-menu > .el-menu-item.is-active::before) {
content: '•';
color: #2c3e50;
font-size: 14px;
}
/* 子菜单箭头图标 */
:deep(.el-sub-menu__icon-arrow) {
color: #909399 !important;
transition: all 0.2s ease;
font-size: 12px;
}
:deep(.el-sub-menu:hover > .el-sub-menu__title .el-sub-menu__icon-arrow) {
color: #7f8c8d !important;
}
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
transform: rotate(180deg);
color: #2c3e50 !important;
}
</style>
+43 -5
View File
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
display: flex;
align-items: center;
font-size: 14px;
height: 100%;
}
.breadcrumb-icon {
margin-right: 6px;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
transition: color 0.2s ease;
}
:deep(.el-breadcrumb__item) {
display: flex !important;
align-items: center;
height: 100%;
}
:deep(.el-breadcrumb__inner) {
display: flex !important;
align-items: center;
color: #7f8c8d;
font-weight: 400;
font-size: 14px;
transition: all 0.2s ease;
}
:deep(.el-breadcrumb__inner a) {
color: #606266;
font-weight: normal;
transition: color 0.2s ease;
color: #7f8c8d;
font-weight: 400;
transition: all 0.2s ease;
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 0;
}
:deep(.el-breadcrumb__inner a:hover) {
color: #1890ff;
color: #2c3e50;
background-color: #f8f9fa;
}
:deep(.el-breadcrumb__inner.is-link) {
color: #7f8c8d;
}
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
color: #2c3e50;
font-weight: 500;
font-size: 14px;
}
:deep(.el-breadcrumb__item:last-child .breadcrumb-icon) {
color: #2c3e50;
}
:deep(.el-breadcrumb__separator) {
margin: 0 8px;
margin: 0 10px;
color: #bdc3c7;
font-size: 12px;
font-weight: 300;
}
:deep(.el-breadcrumb__item:first-child .el-breadcrumb__inner) {
font-weight: 500;
}
:deep(.el-breadcrumb__item:first-child .breadcrumb-icon) {
color: #2c3e50;
}
</style>
+40 -53
View File
@@ -1,22 +1,25 @@
<template>
<el-sub-menu v-if="hasChildren" :index="menu.path">
<template #title>
<el-icon v-if="menu.icon || menu.meta?.icon">
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
<component :is="menu.icon || menu.meta?.icon" />
</el-icon>
<span>{{ menu.title || menu.meta?.title }}</span>
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
</template>
<sidebar-menu-item
v-for="child in menu.children"
:key="child.path"
:menu="child"
:menu="child"
:level="level + 1"
/>
</el-sub-menu>
<el-menu-item v-else :index="menu.path">
<el-icon v-if="menu.icon || menu.meta?.icon">
<el-icon v-if="menu.icon || menu.meta?.icon" class="menu-icon">
<component :is="menu.icon || menu.meta?.icon" />
</el-icon>
<template #title>{{ menu.title || menu.meta?.title }}</template>
<template #title>
<span class="menu-title">{{ menu.title || menu.meta?.title }}</span>
</template>
</el-menu-item>
</template>
@@ -29,6 +32,10 @@ const props = defineProps({
menu: {
type: Object,
required: true
},
level: {
type: Number,
default: 1
}
})
@@ -39,65 +46,45 @@ const hasChildren = computed(() => {
</script>
<style scoped>
.el-menu-item, :deep(.el-sub-menu__title) {
height: 50px;
line-height: 50px;
color: #333333;
}
:deep(.el-sub-menu .el-menu-item) {
height: 50px;
line-height: 50px;
padding-left: 55px !important;
background-color: #fafafa;
}
.el-icon {
/* 菜单图标样式 */
.menu-icon {
margin-right: 10px;
width: 24px;
width: 20px;
height: 20px;
text-align: center;
color: #666666;
color: #7f8c8d;
transition: color 0.2s ease;
font-size: 18px;
flex-shrink: 0;
}
/* 激活菜单项特效 */
.el-menu-item.is-active {
position: relative;
background-color: #e6f7ff !important;
color: #1890ff !important;
font-weight: 600;
/* 菜单标题 */
.menu-title {
font-size: inherit;
letter-spacing: 0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.el-menu-item.is-active::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: #1890ff;
/* 图标交互状态 */
.el-menu-item .menu-icon,
:deep(.el-sub-menu__title .menu-icon) {
color: #7f8c8d !important;
transition: color 0.2s ease;
}
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
color: #1890ff !important;
font-weight: 600;
.el-menu-item:hover .menu-icon,
:deep(.el-sub-menu__title:hover .menu-icon) {
color: #34495e !important;
}
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
background-color: #f5f7fa !important;
/* 激活菜单项图标 */
.el-menu-item.is-active .menu-icon {
color: #2c3e50 !important;
}
/* 修复图标颜色 */
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
color: #1890ff !important;
}
/* 修复箭头颜色 */
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
color: #1890ff !important;
}
/* 子菜单样式 */
:deep(.el-menu--inline) {
background-color: #fafafa;
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .menu-icon) {
color: #2c3e50 !important;
}
</style>
+246 -140
View File
@@ -1,6 +1,9 @@
<template>
<div class="tags-view-container">
<div class="tags-view-wrapper">
<div class="tags-view-container"
@mouseenter="hovered = true" @mouseleave="hovered = false">
<div class="tags-view-wrapper" ref="scrollWrapperRef"
@wheel.prevent="handleWheel"
@scroll="onScroll">
<div class="tags-view-scroll">
<router-link
v-for="tag in visitedViews"
@@ -23,6 +26,10 @@
</router-link>
</div>
</div>
<div class="scroll-track" :class="{ visible: hovered && hasOverflow }">
<div class="scroll-thumb" :style="thumbStyle" @mousedown="onThumbDown"></div>
</div>
<!-- 右键菜单 -->
<ul v-show="visible" :style="{left: left+'px', top: top+'px'}" class="contextmenu">
@@ -59,130 +66,79 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { useTagsViewStore } from '@/store/tagsViewStore'
const router = useRouter()
const route = useRoute()
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.visitedViews)
const affixTags = computed(() => tagsViewStore.affixTags)
// 固定标签
const affixTags = ref([])
// 访问过的标签
const visitedViews = ref([])
// 右键菜单
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const selectedTag = ref({})
// 初始化标签
const initTags = () => {
// 如果当前路由不在访问过的标签中,添加它
if (route.name) {
addVisitedView(route)
tagsViewStore.addVisitedView(route)
}
// 添加固定标签(仪表盘)
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
if (dashboardRoute) {
affixTags.value.push(dashboardRoute)
addVisitedView(dashboardRoute)
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
tagsViewStore.affixTags.push(dashboardRoute)
}
tagsViewStore.addVisitedView(dashboardRoute)
}
}
// 添加访问过的标签
const addVisitedView = (view) => {
if (visitedViews.value.some(v => v.path === view.path)) return
// 过滤404和登录页
if (view.name === 'NotFound' || view.name === 'Login') return
visitedViews.value.push(
Object.assign({}, view, {
title: view.meta.title || 'unknown'
})
)
}
// 刷新选中的标签
const refreshSelectedTag = (view) => {
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
// 然后立即再将路由重定向回原路由,实现刷新效果
const { fullPath } = view
router.replace('/redirect' + fullPath)
}
// 关闭选中的标签
const closeSelectedTag = (view) => {
// 从访问过的标签中移除
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index > -1) {
visitedViews.value.splice(index, 1)
}
// 如果关闭的是当前标签,则跳转到下一个标签
if (isActive(view)) {
toLastView(visitedViews.value, view)
}
tagsViewStore.delVisitedView(view).then((visitedViews) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
// 关闭其他标签
const closeOthersTags = () => {
// 保留固定标签和当前选中的标签
visitedViews.value = visitedViews.value.filter(v => {
return isAffix(v) || v.path === selectedTag.value.path
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
router.push(selectedTag.value)
tagsViewStore.delOthersViews(selectedTag.value)
}
// 关闭左侧标签
const closeLeftTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
if (selectedIndex === -1) return
// 保留固定标签和右侧标签
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i >= selectedIndex
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
}
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
}
// 关闭右侧标签
const closeRightTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
if (selectedIndex === -1) return
// 保留固定标签和左侧标签
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i <= selectedIndex
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
}
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
}
// 关闭所有标签
const closeAllTags = () => {
// 仅保留固定标签
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
// 跳转到第一个标签或首页
toLastView(visitedViews.value)
tagsViewStore.delAllViews().then((visitedViews) => {
toLastView(visitedViews)
})
}
// 跳转到最后一个标签或首页
const toLastView = (visitedViews, view) => {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView)
} else {
// 如果没有标签,则跳转到首页
if (view && view.name === 'Dashboard') {
// 如果当前是首页,则刷新页面
router.push('/redirect' + '/dashboard')
} else {
router.push('/')
@@ -190,17 +146,14 @@ const toLastView = (visitedViews, view) => {
}
}
// 判断是否是当前激活的标签
const isActive = (tag) => {
return tag.path === route.path
}
// 判断是否是固定标签
const isAffix = (tag) => {
return affixTags.value.some(t => t.path === tag.path)
return tag.meta && tag.meta.affix
}
// 打开右键菜单
const openMenu = (e, tag) => {
const menuMinWidth = 125
const offsetLeft = e.clientX
@@ -214,30 +167,112 @@ const openMenu = (e, tag) => {
selectedTag.value = tag
}
// 关闭右键菜单
// ---- 滚动 & 滚动条 ----
const scrollWrapperRef = ref(null)
const hovered = ref(false)
const hasOverflow = ref(false)
const thumbStyle = ref({ width: '0px', left: '0px' })
const handleWheel = (e) => {
if (scrollWrapperRef.value) {
scrollWrapperRef.value.scrollLeft += e.deltaY || e.deltaX
}
}
const refreshState = () => {
const el = scrollWrapperRef.value
if (!el) return
const { scrollLeft, scrollWidth, clientWidth } = el
const maxScroll = scrollWidth - clientWidth
hasOverflow.value = maxScroll > 1
if (!hasOverflow.value) {
thumbStyle.value = { width: '0px', left: '0px' }
return
}
const trackWidth = clientWidth
const thumbW = Math.max((clientWidth / scrollWidth) * trackWidth, 30)
const scrollRatio = maxScroll > 0 ? scrollLeft / maxScroll : 0
const thumbLeft = scrollRatio * (trackWidth - thumbW)
thumbStyle.value = { width: thumbW + 'px', left: thumbLeft + 'px' }
}
const onScroll = () => {
refreshState()
}
const scrollToActiveTag = () => {
const el = scrollWrapperRef.value
if (!el) return
const activeEl = el.querySelector('.active-tag')
if (!activeEl) return
const wrapperRect = el.getBoundingClientRect()
const tagRect = activeEl.getBoundingClientRect()
if (tagRect.left < wrapperRect.left + 28) {
el.scrollLeft -= (wrapperRect.left + 28 - tagRect.left + 12)
} else if (tagRect.right > wrapperRect.right - 28) {
el.scrollLeft += (tagRect.right - wrapperRect.right + 28 + 12)
}
}
const onThumbDown = (e) => {
e.preventDefault()
const el = scrollWrapperRef.value
if (!el) return
const startX = e.clientX
const startScroll = el.scrollLeft
const maxScroll = el.scrollWidth - el.clientWidth
const trackWidth = el.clientWidth
const thumbW = Math.max((el.clientWidth / el.scrollWidth) * trackWidth, 30)
const movable = trackWidth - thumbW
const onMove = (ev) => {
const dx = ev.clientX - startX
const scrollDelta = movable > 0 ? (dx / movable) * maxScroll : 0
el.scrollLeft = Math.min(Math.max(startScroll + scrollDelta, 0), maxScroll)
}
const onUp = () => {
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onUp)
}
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onUp)
}
watch(visitedViews, () => nextTick(() => { refreshState(); scrollToActiveTag() }), { deep: true })
const closeMenu = () => {
visible.value = false
}
// 监听路由变化,添加标签
watch(route, (newRoute) => {
if (newRoute.name) {
addVisitedView(newRoute)
tagsViewStore.addVisitedView(newRoute)
}
nextTick(scrollToActiveTag)
})
// 点击其他区域关闭右键菜单
const handleClickOutside = () => {
closeMenu()
}
const onResize = () => {
refreshState()
scrollToActiveTag()
}
onMounted(() => {
initTags()
document.addEventListener('click', handleClickOutside)
nextTick(() => { refreshState(); scrollToActiveTag() })
window.addEventListener('resize', onResize)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', onResize)
})
</script>
@@ -245,21 +280,24 @@ onBeforeUnmount(() => {
.tags-view-container {
height: 40px;
width: 100%;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
background-color: #ffffff;
border-bottom: 1px solid #e1e8ed;
z-index: 10;
display: flex;
align-items: stretch;
position: relative;
overflow: hidden;
}
/* 标签滚动区域 */
.tags-view-wrapper {
flex: 1;
min-width: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
padding: 0 16px;
overflow-x: auto;
white-space: nowrap;
position: relative;
overflow-x: scroll;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.tags-view-wrapper::-webkit-scrollbar {
@@ -267,44 +305,94 @@ onBeforeUnmount(() => {
}
.tags-view-scroll {
display: flex;
align-items: center;
height: 100%;
}
.tag, .active-tag {
height: 28px;
display: inline-flex;
align-items: center;
padding: 0 10px;
margin-right: 5px;
border-radius: 2px;
font-size: 12px;
color: #333333;
background-color: #f4f4f5;
height: 100%;
padding: 0 8px;
gap: 4px;
}
/* 底部自定义滚动条(在容器上,不在滚动区域内) */
.scroll-track {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
opacity: 0;
transition: opacity 0.25s;
pointer-events: none;
z-index: 5;
}
.scroll-track.visible {
opacity: 1;
pointer-events: auto;
}
.scroll-thumb {
position: absolute;
top: 0;
height: 100%;
border-radius: 3px;
background: rgba(180,188,199,0.45);
transition: background 0.15s;
cursor: pointer;
}
.scroll-thumb:hover {
background: rgba(180,188,199,0.65);
}
/* 标签样式 */
.tag, .active-tag {
height: 32px;
display: inline-flex;
align-items: center;
padding: 0 12px;
border-radius: 0;
font-size: 13px;
text-decoration: none;
position: relative;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid #e8e8e8;
transition: all 0.2s ease;
border: 1px solid transparent;
border-bottom: none;
flex-shrink: 0;
white-space: nowrap;
}
.tag {
color: #7f8c8d;
background-color: #f8f9fa;
}
.tag:hover {
color: #1890ff;
background-color: #e6f7ff;
border-color: #1890ff;
color: #34495e;
background-color: #e8ecf0;
}
.active-tag {
color: #1890ff;
background-color: #e6f7ff;
border-color: #1890ff;
font-weight: 600;
color: #2c3e50;
background-color: #ffffff;
border: 1px solid #e1e8ed;
border-bottom: 2px solid #2c3e50;
font-weight: 500;
}
.tag-icon {
margin-right: 4px;
margin-right: 6px;
width: 14px;
height: 14px;
color: #95a5a6;
transition: color 0.2s ease;
}
.tag:hover .tag-icon {
color: #34495e;
}
.active-tag .tag-icon {
color: #2c3e50;
}
.tag-title {
@@ -315,36 +403,47 @@ onBeforeUnmount(() => {
}
.tag-close {
margin-left: 5px;
width: 14px;
height: 14px;
border-radius: 50%;
transition: all 0.3s;
margin-left: 8px;
width: 16px;
height: 16px;
border-radius: 0;
transition: all 0.2s ease;
color: #95a5a6;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
}
.tag-close:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
.tag:hover .tag-close {
color: #666;
background-color: rgba(0, 0, 0, 0.1);
color: #7f8c8d;
}
.active-tag .tag-close {
color: #1890ff;
color: #7f8c8d;
}
.active-tag:hover .tag-close {
background-color: rgba(24, 144, 255, 0.1);
.active-tag .tag-close:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
/* 右键菜单 */
.contextmenu {
position: fixed;
z-index: 100;
background-color: #fff;
background-color: #ffffff;
list-style-type: none;
padding: 6px 0;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 4px 0;
border-radius: 0;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
font-size: 12px;
border: 1px solid #ebeef5;
border: 1px solid #e1e8ed;
}
.contextmenu li {
@@ -352,15 +451,22 @@ onBeforeUnmount(() => {
cursor: pointer;
display: flex;
align-items: center;
color: #34495e;
transition: all 0.2s ease;
}
.contextmenu li:hover {
background-color: #f5f7fa;
color: #1890ff;
background-color: #f8f9fa;
color: #2c3e50;
}
.contextmenu li .el-icon {
margin-right: 8px;
font-size: 14px;
color: #7f8c8d;
}
</style>
.contextmenu li:hover .el-icon {
color: #2c3e50;
}
</style>
@@ -0,0 +1,295 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<div v-if="detailData" class="detail-content">
<table class="detail-table">
<!-- 优惠码特有字段 -->
<tr v-if="type === 'code'">
<td class="label">优惠码</td>
<td class="value">{{ detailData.code }}</td>
</tr>
<!-- 名称 -->
<tr>
<td class="label">{{ type === 'code' ? '名称' : '代金券名称' }}</td>
<td class="value">{{ detailData.name }}</td>
</tr>
<!-- 备注 -->
<tr>
<td class="label">备注</td>
<td class="value secondary">{{ detailData.note || '无' }}</td>
</tr>
<!-- 优惠类型仅优惠码 -->
<tr v-if="type === 'code'" class="alternate">
<td class="label">优惠类型</td>
<td class="value">
<span :class="['type-tag', detailData.percentage ? 'percentage' : 'amount']">
{{ detailData.percentage ? '百分比折扣' : '固定金额' }}
</span>
</td>
</tr>
<!-- 优惠值/面额 -->
<tr :class="type === 'code' ? '' : 'alternate'">
<td class="label">{{ type === 'code' ? '优惠值' : '面额' }}</td>
<td class="value">
<span v-if="detailData.percentage" class="highlight-value percentage">
{{ (detailData.percentage / 100).toFixed(0) }}%
</span>
<span v-else class="highlight-value amount">
¥{{ (detailData.amount / 100).toFixed(2) }}
</span>
</td>
</tr>
<!-- 最低消费 -->
<tr>
<td class="label">最低消费</td>
<td class="value">¥{{ (detailData.minAmount / 100).toFixed(2) }}</td>
</tr>
<!-- 最大抵扣 -->
<tr>
<td class="label">最大抵扣</td>
<td class="value">
<span v-if="detailData.maxAmount">¥{{ (detailData.maxAmount / 100).toFixed(2) }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 最大使用次数 -->
<tr class="alternate">
<td class="label">最大使用次数</td>
<td class="value">
<span v-if="detailData.maxTimes">{{ detailData.maxTimes }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 单用户次数 -->
<tr>
<td class="label">单用户次数</td>
<td class="value">
<span v-if="detailData.userTimes">{{ detailData.userTimes }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 有效期仅代金券 -->
<tr v-if="type === 'coupon'" class="alternate">
<td class="label">有效期()</td>
<td class="value">
{{ detailData.duration ? (detailData.duration / 86400).toFixed(0) + '天' : '-' }}
</td>
</tr>
<!-- 有效期开始 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">{{ type === 'code' ? '有效期开始' : '发放时间开始' }}</td>
<td class="value">{{ formatISODate(detailData.startTime) }}</td>
</tr>
<!-- 有效期结束 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">{{ type === 'code' ? '有效期结束' : '发放时间结束' }}</td>
<td class="value">{{ formatISODate(detailData.endTime) }}</td>
</tr>
<!-- 续费可用 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">续费可用</td>
<td class="value">
<span :class="['status-icon', detailData.renew ? 'success' : 'danger']">
{{ detailData.renew ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 同类型可叠加 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">同类型可叠加</td>
<td class="value">
<span :class="['status-icon', detailData.canStacking ? 'success' : 'danger']">
{{ detailData.canStacking ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 其他类型可叠加 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">其他类型可叠加</td>
<td class="value">
<span :class="['status-icon', detailData.canCombine ? 'success' : 'danger']">
{{ detailData.canCombine ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 创建时间 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">创建时间</td>
<td class="value timestamp">{{ formatISODate(detailData.CreatedAt) }}</td>
</tr>
<!-- 更新时间 -->
<tr>
<td class="label">更新时间</td>
<td class="value timestamp">{{ formatISODate(detailData.UpdatedAt) }}</td>
</tr>
</table>
</div>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
type: {
type: String,
required: true,
validator: (value) => ['code', 'coupon'].includes(value)
},
detailData: {
type: Object,
default: null
}
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const dialogTitle = computed(() => {
return props.type === 'code' ? '优惠码详情' : '代金券详情'
})
// 格式化ISO 8601日期字符串
const formatISODate = (isoStr) => {
if (!isoStr) return '-'
try {
const date = new Date(isoStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} catch {
return isoStr
}
}
const handleClose = () => {
emit('update:modelValue', false)
}
</script>
<style scoped>
.detail-content {
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.detail-table {
width: 100%;
border-collapse: collapse;
}
.detail-table tr {
border-bottom: 1px solid #f0f0f0;
}
.detail-table tr.alternate {
background-color: #fafafa;
}
.detail-table td {
padding: 12px 8px;
}
.detail-table .label {
width: 140px;
color: #606266;
font-weight: 500;
}
.detail-table .value {
color: #303133;
}
.detail-table .value.secondary {
color: #606266;
}
.detail-table .value.timestamp {
color: #909399;
font-size: 13px;
}
/* 类型标签 */
.type-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
display: inline-block;
}
.type-tag.percentage {
background-color: #f0f9ff;
color: #67c23a;
}
.type-tag.amount {
background-color: #eff6ff;
color: #409eff;
}
/* 突出显示的值 */
.highlight-value {
font-weight: bold;
font-size: 18px;
}
.highlight-value.percentage {
color: #67c23a;
}
.highlight-value.amount {
color: #f56c6c;
}
/* 状态图标 */
.status-icon.success {
color: #67c23a;
}
.status-icon.danger {
color: #f56c6c;
}
.secondary {
color: #909399;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
/**
* 环境配置文件
* 所有硬编码的 URL / 域名 / 环境变量统一在此管理
*/
// 当前环境
const isDevelopment = import.meta.env.MODE === 'development'
// API 基础地址
// 开发环境使用 vite 代理 (baseUrl 为空),生产环境使用实际地址
const API_BASE_MAP = {
development: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren', // 直接请求后端,不走 vite proxy
production: import.meta.env.VITE_API_BASE_URL || 'https://cloudapi.007yjs.com',
staging: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren'
}
// 获取当前环境的 API 基础地址
const currentEnv = import.meta.env.VITE_APP_ENV || import.meta.env.MODE || 'development'
export const baseUrl = API_BASE_MAP[currentEnv] || API_BASE_MAP.development
// ACS 服务基础地址
export const acsBaseUrl = baseUrl
// 网站标题
export const siteTitle = '007UI管理系统'
// 请求超时时间(毫秒)
export const requestTimeout = 50000
export const acsRequestTimeout = 30000
// Token 存储键名
export const TOKEN_KEY = 'token'
export const TOKEN_EXPIRE_KEY = 'tokenExpire'
export const USER_INFO_KEY = 'userInfo'
// 不需要 token 认证的 URL 前缀
export const noAuthUrls = [
'/v1/user/login',
'/v1/user/check/get_code_img',
'/v1/user/register',
'/v1/user/refresh_token'
]
export default {
isDevelopment,
baseUrl,
acsBaseUrl,
siteTitle,
requestTimeout,
acsRequestTimeout,
TOKEN_KEY,
TOKEN_EXPIRE_KEY,
USER_INFO_KEY,
noAuthUrls
}
+146 -18
View File
@@ -5,10 +5,93 @@ export const menus = [
icon: 'DataBoard'
},
{
path : '/ticket',
title: '工单理',
icon: 'DataBoard'
path: '/ticket',
title: '工单理',
icon: 'Tickets',
children: [
{
path: '/ticket/list',
title: '工单列表'
}
]
},
{
path: '/user',
title: '用户管理',
icon: 'User',
children: [
{
path: '/user/list',
title: '用户列表'
},
{
path: '/user/group',
title: '用户组管理'
},
{
path: '/user/admin-group',
title: '管理员组管理'
}
]
},
{
path: '/product',
title: '商品管理',
icon: 'Goods',
children: [
{ path: '/product/manage', title: '商品管理' }
]
},
{
path: '/user-goods',
title: '用户商品管理',
icon: 'ShoppingCart',
children: [
{ path: '/user-goods/list', title: '所有商品' },
{ path: '/user-goods/vm-list', title: '云服务器' }
]
},
{
path: '/order',
title: '订单管理',
icon: 'Document',
children: [
{
path: '/order/list',
title: '订单列表'
}
]
},
{
path: '/marketing',
title: '优惠营销',
icon: 'Present',
children: [
{
path: '/marketing/discount',
title: '优惠码管理'
},
{
path: '/marketing/voucher',
title: '代金券管理'
},
]
},
{
path: '/activity',
title: '活动管理',
icon: 'TrophyBase',
children: [
{
path: '/activity/signin',
title: '签到活动'
},
{
path: '/activity/groupbuy',
title: '拼团管理'
}
]
},
{
path: '/acs',
@@ -33,39 +116,84 @@ export const menus = [
{ path: '/acs/images/categories', title: '镜像分类' }
]
},
{
path: '/acs/nodes', title: '节点管理'
{
path: '/acs/nodes',
title: '节点管理'
},
{
path: '/acs/guacamole',
title: '远程桌面网关管理',
icon: 'Monitor'
},{
title: '远程桌面网关管理'
},
{
path: '/audit',
title: '站点审计',
icon: 'Monitor',
children: [
{ path: '/audit/all', title: '所有站点' },
{ path: '/audit/violation', title: '违规站点' }
]
},{
path:'/setting',
title:'全局设置管理',
icon:'Setting',
children:[
{path:'/setting/global',title:'全局设置'}
},
{
path: '/setting',
title: '全局设置管理',
children: [
{ path: '/setting/global', title: '全局设置' }
]
}
]
},
{
title: '虚拟化平台管理',
icon: 'Platform',
children: [
{
path: '/virtualization/kvm-service',
title: '主控服务管理'
},
{
path: '/virtualization/host-group-mapping',
title: '宿主机组映射管理'
},
{
path: '/virtualization/vnc-command',
title: 'VNC指令管理'
}
]
},
{
path: '/system',
title: '系统管理',
icon: 'Setting',
children: [
// { path: '/system/users', title: '用户管理' },
// { path: '/system/operation-log', title: '操作日志' },
{ path: '/system/domain-whitelist', title: '域名白名单' }
{
path: '/system/permission',
title: '权限管理',
children: [
{ path: '/system/permission/route', title: '路由权限' },
{ path: '/system/permission/admin', title: '管理员权限' }
]
},
{
path: '/system/file',
title: '文件管理'
},
{
path: '/system/domain-whitelist',
title: '域名白名单'
},
{
path: '/system/setting-manage',
title: '配置管理'
},
{
path: '/system/menu',
title: '菜单管理',
children: [
{ path: '/system/menu-manage', title: '菜单列表' },
{ path: '/system/menu-permission', title: '菜单权限' }
]
}
]
}
]
+449 -34
View File
@@ -39,9 +39,29 @@ const routes = [
title: '工单管理',
icon: 'Tickets'
},
component: () => import('../views/ticket/TicketChat.vue'),
redirect: '/ticket/list',
children: [
{
path: 'list',
name: 'TicketList',
component: () => import('../views/ticket/TicketList.vue'),
meta: {
title: '工单列表'
}
},
{
path: 'detail',
name: 'TicketDetail',
component: () => import('../views/ticket/TicketDetail.vue'),
meta: {
title: '工单详情',
hidden: true,
activeMenu: '/ticket/list'
}
}
]
},
// ACS管理路由
{
path: 'acs',
@@ -130,7 +150,19 @@ const routes = [
meta: {
title: '节点管理'
}
},{
},
{
path: 'nodes/form',
name: 'ServerForm',
component: () => import('@/views/acs/nodes/ServerForm.vue'),
meta: { title: '服务器表单', activeMenu: '/acs/nodes', hidden: true }
},
{
path: 'images/form',
name: 'ImageForm',
component: () => import('@/views/acs/images/ImageForm.vue'),
meta: { title: '镜像表单', activeMenu: '/acs/images/vm', hidden: true }
}, {
path: 'guacamole',
name: 'Guacamole',
component: () => import('../views/acs/guacamole/Guacamole.vue'),
@@ -140,6 +172,195 @@ const routes = [
}
]
},
// 用户管理路由
{
path: 'user',
name: 'User',
meta: {
title: '用户管理',
icon: 'User'
},
redirect: '/user/list',
children: [
{
path: 'list',
name: 'UserList',
component: () => import('../views/user/UserList.vue'),
meta: {
title: '用户列表'
}
},
{
path: 'detail',
name: 'UserDetail',
component: () => import('../views/user/UserDetail.vue'),
meta: {
title: '用户详情'
}
},
{
path: 'balance',
name: 'UserBalance',
component: () => import('../views/user/UserBalance.vue'),
meta: {
title: '用户余额管理'
}
},
{
path: 'group',
name: 'UserGroup',
component: () => import('../views/user/UserGroup.vue'),
meta: {
title: '用户组管理'
}
},
{
path: 'admin-group',
name: 'AdminGroup',
component: () => import('../views/user/AdminGroup.vue'),
meta: {
title: '管理员组管理'
}
}
]
},
// 商品管理路由
{
path: 'product',
name: 'Product',
meta: { title: '商品管理', icon: 'Goods' },
redirect: '/product/manage',
children: [
{
path: 'manage',
name: 'ProductManage',
component: () => import('../views/product/ProductGroup.vue'),
meta: { title: '商品管理' }
},
{ path: 'list', redirect: '/product/manage' },
{ path: 'group', redirect: '/product/manage' }
]
},
// 用户商品管理路由
{
path: 'user-goods',
name: 'UserGoods',
meta: { title: '用户商品管理', icon: 'ShoppingCart' },
redirect: '/user-goods/list',
children: [
{
path: 'list',
name: 'UserGoodsList',
component: () => import('../views/product/UserGoodsList.vue'),
meta: { title: '所有商品' }
},
{
path: 'detail/:id',
name: 'UserGoodsDetail',
component: () => import('../views/product/UserGoodsDetail.vue'),
meta: { title: '用户商品详情', hidden: true, activeMenu: '/user-goods/list' }
},
{
path: 'vm-list',
name: 'UserVmList',
component: () => import('../views/user-vm/UserVmList.vue'),
meta: { title: '云服务器' }
},
{
path: 'vm-detail',
name: 'UserVmDetail',
component: () => import('../views/user-vm/UserVmDetail.vue'),
meta: { title: '用户虚拟机详情', hidden: true, activeMenu: '/user-goods/vm-list' }
}
]
},
// 订单管理路由
{
path: 'order',
name: 'Order',
meta: {
title: '订单管理',
icon: 'Document'
},
redirect: '/order/list',
children: [
{
path: 'list',
name: 'OrderList',
component: () => import('../views/order/OrderList.vue'),
meta: {
title: '订单列表'
}
}
]
},
// 优惠营销路由
{
path: 'marketing',
name: 'Marketing',
meta: {
title: '优惠营销',
icon: 'Present'
},
redirect: '/marketing/discount',
children: [
{
path: 'discount',
name: 'DiscountCode',
component: () => import('../views/marketing/DiscountCode.vue'),
meta: {
title: '优惠码管理'
}
},
{
path: 'voucher',
name: 'Voucher',
component: () => import('../views/marketing/Voucher.vue'),
meta: {
title: '代金券管理'
}
},
{
path: 'voucher/:id/manage',
name: 'VoucherManagement',
component: () => import('../views/marketing/VoucherManagement.vue'),
meta: {
title: '代金券详情管理',
hidden: true,
activeMenu: '/marketing/voucher'
}
},
]
},
// 活动管理路由
{
path: 'activity',
name: 'Activity',
meta: {
title: '活动管理',
icon: 'TrophyBase'
},
redirect: '/activity/signin',
children: [
{
path: 'signin',
name: 'SigninActivity',
component: () => import('../views/activity/SigninActivity.vue'),
meta: {
title: '签到活动'
}
},
{
path: '/activity/groupbuy',
name: 'GroupBuyManage',
component: () => import('../views/activity/GroupBuyManage.vue'),
meta: {
title: '拼团管理'
}
}
]
},
{
path: 'system',
name: 'System',
@@ -147,28 +368,222 @@ const routes = [
title: '系统管理',
icon: 'Setting'
},
redirect: '/system/domain-whitelist',
redirect: '/system/permission/route',
children: [
// 注释掉的用户管理和操作日志路由,与菜单配置保持一致
// {
// path: 'users',
// name: 'Users',
// component: () => import('../views/system/Users.vue'),
// meta: {
// title: '用户管理'
// }
// },
// {
// path: 'operation-log',
// name: 'OperationLog',
// component: OperationLog,
// meta: { title: '操作日志' }
// },
{
path: 'permission/route',
name: 'PermissionRoute',
component: () => import('../views/system/PermissionRoute.vue'),
meta: {
title: '路由权限'
}
},
{
path: 'permission/admin',
name: 'PermissionAdmin',
component: () => import('../views/system/PermissionAdmin.vue'),
meta: {
title: '管理员权限'
}
},
{
path: 'file',
name: 'SystemFile',
component: () => import('../views/system/SystemFile.vue'),
meta: {
title: '文件管理'
}
},
{
path: 'domain-whitelist',
name: 'DomainWhitelist',
component: () => import('../views/system/DomainWhitelist.vue'),
meta: { title: '域名白名单' }
},
{
path: 'setting-manage',
name: 'SettingManage',
component: () => import('../views/system/SettingManage.vue'),
meta: { title: '配置管理' }
},
{
path: 'menu-manage',
name: 'MenuManage',
component: () => import('../views/system/MenuManage.vue'),
meta: { title: '菜单管理' }
},
{
path: 'menu-permission',
name: 'MenuPermission',
component: () => import('../views/system/MenuPermission.vue'),
meta: { title: '菜单权限' }
}
]
},
{
path: 'virtualization',
name: 'Virtualization',
meta: {
title: '虚拟化平台管理',
icon: 'Platform'
},
redirect: '/virtualization/kvm-service',
children: [
{
path: 'kvm-service',
name: 'KvmService',
component: () => import('../views/virtualization/KvmService.vue'),
meta: {
title: '主控服务管理'
}
},
{
path: 'kvm-service-detail',
name: 'KvmServiceDetail',
component: () => import('../views/virtualization/KvmServiceDetail.vue'),
meta: {
title: '主控服务详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'host-group-mapping',
name: 'HostGroupMapping',
component: () => import('../views/virtualization/HostGroupMapping.vue'),
meta: {
title: '宿主机组映射管理'
}
},
{
path: 'host-manage',
name: 'HostManage',
component: () => import('../views/virtualization/HostManage.vue'),
meta: {
title: '宿主机管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'image-manage',
name: 'ImageManage',
component: () => import('../views/virtualization/ImageManage.vue'),
meta: {
title: '镜像管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'network-manage',
name: 'NetworkManage',
component: () => import('../views/virtualization/NetworkManage.vue'),
meta: {
title: '网络管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'volume-manage',
name: 'VolumeManage',
component: () => import('../views/virtualization/VolumeManage.vue'),
meta: {
title: '数据卷管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'vm-manage',
name: 'VmManage',
component: () => import('../views/virtualization/VmManage.vue'),
meta: {
title: '虚拟机管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'security-group',
name: 'SecurityGroupManage',
component: () => import('../views/virtualization/SecurityGroupManage.vue'),
meta: {
title: '安全组管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'vnc-node',
name: 'VncNodeManage',
component: () => import('../views/virtualization/VncNodeManage.vue'),
meta: {
title: 'VNC节点管理',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'vnc-command',
name: 'VncCommandManage',
component: () => import('../views/virtualization/VncCommandManage.vue'),
meta: {
title: 'VNC指令管理'
}
},
{
path: 'host-detail',
name: 'VirtHostDetail',
component: () => import('../views/virtualization/HostDetail.vue'),
meta: {
title: '宿主机详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'image-detail',
name: 'VirtImageDetail',
component: () => import('../views/virtualization/ImageDetail.vue'),
meta: {
title: '镜像详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'vm-detail',
name: 'VirtVmDetail',
component: () => import('../views/virtualization/VmDetail.vue'),
meta: {
title: '虚拟机详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'security-group-detail',
name: 'VirtSecurityGroupDetail',
component: () => import('../views/virtualization/SecurityGroupDetail.vue'),
meta: {
title: '安全组详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
},
{
path: 'volume-detail',
name: 'VirtVolumeDetail',
component: () => import('../views/virtualization/VolumeDetail.vue'),
meta: {
title: '数据卷详情',
hidden: true,
activeMenu: '/virtualization/kvm-service'
}
}
]
},
@@ -269,21 +684,21 @@ const routes = [
title: '容器详情',
hidden: true
}
},{
path:'servers/container/console',
name:'ContainerConsole',
component:()=>import('../views/acs/nodes/containerConsole.vue'),
meta:{
title:'终端容器',
hidden:true
}, {
path: 'servers/container/console',
name: 'ContainerConsole',
component: () => import('../views/acs/nodes/containerConsole.vue'),
meta: {
title: '终端容器',
hidden: true
}
},{
path:'servers/container/files',
name:'ContainerFiles',
component:()=>import('../views/acs/nodes/containFile.vue'),
meta:{
title:'容器文件管理',
hidden:true
}, {
path: 'servers/container/files',
name: 'ContainerFiles',
component: () => import('../views/acs/nodes/containFile.vue'),
meta: {
title: '容器文件管理',
hidden: true
}
}
]
@@ -317,7 +732,7 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
// 这里可以添加登录验证逻辑
const isAuthenticated = localStorage.getItem('token')
if (to.path !== '/login' && !isAuthenticated) {
+91
View File
@@ -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
View File
@@ -4,11 +4,28 @@ import {ref} from "vue";
export const useUserStore = defineStore('userStore',() => {
let userInfo = ref({})
// 初始化时从localStorage读取用户信息
const savedUserInfo = localStorage.getItem('userInfo')
let userInfo = ref(savedUserInfo ? JSON.parse(savedUserInfo) : {})
function setUserInfo(u){
userInfo.value = u
// 同步保存到localStorage
if (u && Object.keys(u).length > 0) {
localStorage.setItem('userInfo', JSON.stringify(u))
}
}
return {userInfo,setUserInfo}
// 清除用户信息
function clearUserInfo() {
userInfo.value = {}
localStorage.removeItem('userInfo')
}
// 获取用户头像
function getUserAvatar() {
return userInfo.value?.cover || ''
}
return {userInfo, setUserInfo, clearUserInfo, getUserAvatar}
})
+374 -1
View File
@@ -114,11 +114,384 @@ body {
padding-right: 10px;
}
/* 响应式工具类 */
/* 可点击元素统一手型光标 */
.el-button,
.el-button--link,
.el-tag.is-closable .el-tag__close,
.el-dropdown,
.el-dropdown-menu__item,
.el-switch,
.el-checkbox,
.el-radio,
.el-select .el-input__wrapper,
.el-table__body tr.el-table__row {
cursor: pointer;
}
.back-btn {
cursor: pointer;
}
/* ==================== 全局弹窗卡片样式 ==================== */
/* 自动为所有未手动分区的弹窗表单添加卡片背景 */
.el-dialog:not(.tk-dialog):not(.token-dialog):not(.token-result-dialog) .el-dialog__body > .el-form {
background: #fafbfc;
border-radius: 8px;
padding: 20px 20px 4px;
border: 1px solid #f0f2f5;
}
/* 统一弹窗 footer 按钮对齐 */
.el-dialog .el-dialog__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
}
.tk-dialog .el-dialog__body {
max-height: 70vh;
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.tk-dialog .el-dialog__body::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.tk-dialog .el-form {
padding: 0 4px;
}
.tk-section {
background: #fafbfc;
border-radius: 8px;
padding: 20px 20px 4px;
margin-bottom: 16px;
border: 1px solid #f0f2f5;
}
.tk-section-title {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 18px;
padding-left: 10px;
border-left: 3px solid #409eff;
line-height: 1;
}
.tk-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.tk-resource-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 24px;
}
.tk-resource-grid .el-form-item {
margin-bottom: 18px;
}
.tk-resource-grid .el-form-item .el-form-item__label {
width: 80px !important;
}
.tk-resource-grid .el-form-item .el-form-item__content {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
}
.tk-resource-grid .el-input-number {
flex: 1;
min-width: 0;
}
.tk-unit-select {
width: 68px;
flex-shrink: 0;
}
.tk-res-unit {
font-size: 13px;
color: #909399;
flex-shrink: 0;
white-space: nowrap;
}
.tk-inline-unit {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.tk-inline-unit .el-input-number,
.tk-inline-unit .el-input,
.tk-inline-unit .el-select {
flex: 1;
min-width: 0;
}
/* ==================== 全局页面布局组件 ==================== */
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-header .header-left {
display: flex;
align-items: center;
gap: 16px;
}
.page-header .header-info h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
.page-header .sub-info {
font-size: 13px;
color: #909399;
margin-top: 2px;
}
.page-header .header-right {
display: flex;
gap: 8px;
flex-shrink: 0;
}
/* 嵌入式工具栏 */
.embedded-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
/* 通用工具栏 */
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
/* 筛选栏 */
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
/* 筛选区域(卡片式) */
.filter-section {
margin-bottom: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 8px;
}
/* 绑定选择器行 */
.bind-selector-row {
display: flex;
align-items: center;
width: 100%;
}
/* 详情操作按钮组 */
.detail-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
/* ==================== 全局表格增强 ==================== */
.el-table {
--el-table-header-bg-color: #fafafa;
--el-table-row-hover-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
}
.el-table th.el-table__cell {
font-weight: 600 !important;
color: #1d2129 !important;
font-size: 13px !important;
border-bottom: 2px solid #e1e8ed !important;
}
.el-table td.el-table__cell {
border-bottom: 1px solid #f0f2f5 !important;
color: #34495e !important;
transition: background-color 0.15s ease;
}
.el-table .el-table__empty-block {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.el-table .el-table__empty-text {
color: #909399;
font-size: 14px;
line-height: 1.6;
}
/* 表格固定列阴影 */
.el-table__fixed {
box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.1);
}
.el-table__fixed-right {
box-shadow: -4px 0 8px -4px rgba(0, 0, 0, 0.1);
}
/* ==================== 全局骨架屏样式 ==================== */
@keyframes tk-skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: tk-skeleton-loading 1.5s ease-in-out infinite;
border-radius: 4px;
}
/* ==================== 全局过渡动画 ==================== */
.el-table,
.el-card,
.el-tag,
.el-button {
transition: all 0.2s ease;
}
/* ==================== 通用文本类 ==================== */
.text-muted {
color: #c0c4cc;
}
.mono-text {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
color: #409eff;
font-size: 13px;
}
/* ==================== 视觉增强 ==================== */
/* 卡片式筛选区域 */
.filter-card {
background: #ffffff;
border: 1px solid #ebeef5;
padding: 16px 20px;
margin-bottom: 16px;
}
/* 操作栏 */
.action-bar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* 通用结果/令牌展示 */
.tk-result-wrapper { text-align: center; }
.tk-result-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; text-align: left; }
.tk-result-icon { font-size: 36px; color: #e6a23c; background: #fdf6ec; border-radius: 50%; padding: 10px; }
.tk-result-name { font-size: 16px; font-weight: 600; color: #1d2129; }
.tk-result-meta { font-size: 13px; color: #909399; margin-top: 2px; }
.tk-token-block { background: #1d2129; border-radius: 8px; padding: 16px; margin-bottom: 16px; text-align: left; }
.tk-token-label { font-size: 11px; color: #909399; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
.tk-token-value { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; color: #67c23a; word-break: break-all; line-height: 1.6; user-select: all; }
.tk-copy-btn { width: 100%; }
/* 表单提示 */
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
/* 资源信息标签组 */
.resource-info {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* ==================== 响应式工具类 ==================== */
/* 表格横向滚动提示 */
.el-table {
overflow: visible;
}
@media (max-width: 768px) {
.hidden-xs {
display: none !important;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.page-header .header-right {
width: 100%;
flex-wrap: wrap;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-bar .el-input,
.filter-bar .el-select {
width: 100% !important;
}
.pagination-wrapper {
justify-content: center;
}
.pagination-wrapper .el-pagination {
flex-wrap: wrap;
justify-content: center;
}
/* 弹窗在移动端更宽 */
.el-dialog {
width: 92% !important;
margin: 5vh auto !important;
}
/* 表格小屏字号调整 */
.el-table td.el-table__cell {
font-size: 13px !important;
}
/* 表单小屏行距压缩 */
.el-form-item {
margin-bottom: 16px;
}
/* tk-resource-grid 在移动端变为单列 */
.tk-resource-grid {
grid-template-columns: 1fr;
}
}
/* 中等屏幕适配 */
@media (max-width: 1200px) {
.el-table .el-table__body-wrapper {
overflow-x: auto;
}
}
@media (min-width: 768px) and (max-width: 992px) {
+4 -4
View File
@@ -3,15 +3,15 @@ import {http2} from "@/utils/request.js";
/**获取所有站点 */
export const getSiteList = (data) => {
return http2.get(`/v1/admin/audit/list?page=${data.page}&server_id=${data.server_id}&user_id=${data.user_id}&count=${data.count}&key=${data.key}`)
return http2.get(`/acs/v1/admin/audit/list?page=${data.page}&server_id=${data.server_id}&user_id=${data.user_id}&count=${data.count}&key=${data.key}`)
}
/**手动触发站点审计 */
export const auditSite = () => {
return http2.get(`/v1/admin/audit/start`)
return http2.get(`/acs/v1/admin/audit/start`)
}
/**删除违规网页审计 传入参数: web_key 站点名*/
export const delAudit = (data) => {
return http2.post(`/v1/admin/audit/delete`,data,{
return http2.post(`/acs/v1/admin/audit/delete`,data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -19,5 +19,5 @@ export const delAudit = (data) => {
}
/**获取违规网页审计列表 */
export const getAuditList = (data) => {
return http2.get(`/v1/admin/audit/violation_list?page=${data.page}&count=${data.count}&key=${data.key}`)
return http2.get(`/acs/v1/admin/audit/violation_list?page=${data.page}&count=${data.count}&key=${data.key}`)
}
+9 -9
View File
@@ -1,10 +1,10 @@
import {http2} from "@/utils/request.js";
export const getFileList = (data) => {
return http2.get(`/v1/file/list?container_id=${data.container_id}&path=${data.path}`)
return http2.get(`/acs/v1/file/list?container_id=${data.container_id}&path=${data.path}`)
}
/** 读取文件内容 */
export const readFile = (data) => {
return http2.post(`/v1/file/read`,data, {
return http2.post(`/acs/v1/file/read`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -12,7 +12,7 @@ export const readFile = (data) => {
}
/*删除文件或文件夹 */
export const deleteFile = (data) => {
return http2.post(`/v1/file/delete`,data, {
return http2.post(`/acs/v1/file/delete`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -20,7 +20,7 @@ export const deleteFile = (data) => {
}
/*写入文件 */
export const writeFile = (data) => {
return http2.post(`/v1/file/write`,data, {
return http2.post(`/acs/v1/file/write`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -28,7 +28,7 @@ export const writeFile = (data) => {
}
/*创建文件夹 */
export const createFolder = (data) => {
return http2.post(`/v1/file/mkdir`,data, {
return http2.post(`/acs/v1/file/mkdir`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -36,7 +36,7 @@ export const createFolder = (data) => {
}
/**上传文件 */
export const uploadFile = (data) => {
return http2.post(`/v1/file/upload_file`,data, {
return http2.post(`/acs/v1/file/upload_file`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -44,7 +44,7 @@ export const uploadFile = (data) => {
}
/**下载文件链接 */
export const downloadFile = (data) => {
return http2.post(`/v1/file/get_down_link`,data, {
return http2.post(`/acs/v1/file/get_down_link`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -52,7 +52,7 @@ export const downloadFile = (data) => {
}
/**压缩文件 */
export const compressFile = (data) => {
return http2.post(`/v1/file/zip_file`,data, {
return http2.post(`/acs/v1/file/zip_file`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -60,7 +60,7 @@ export const compressFile = (data) => {
}
/**解压文件 */
export const decompressFile = (data) => {
return http2.post(`/v1/file/unzip_file`,data, {
return http2.post(`/acs/v1/file/unzip_file`,data, {
headers: {
"Content-Type": "multipart/form-data"
}
+5 -5
View File
@@ -2,15 +2,15 @@ import {http2} from "@/utils/request.js";
/**获取 guacamole 列表 */
export const getGuacamoleList = data => {
return http2.get(`/v1/admin/server/get_guacamole_list`);
return http2.get(`/acs/v1/admin/server/get_guacamole_list`);
};
/**获取服务器 guacamole 信息 */
export const getGuacamoleInfo = data => {
return http2.get(`/v1/admin/server/get_server_guacamole?server_id=${data}`);
return http2.get(`/acs/v1/admin/server/get_server_guacamole?server_id=${data}`);
};
/**新增 guacamole 参数 url:string,username:string,password:string*/
export const addGuacamoleInfo = data => {
return http2.post(`/v1/admin/server/add_guacamole`, data,{
return http2.post(`/acs/v1/admin/server/add_guacamole`, data,{
headers:{
'Content-Type': 'multipart/form-data'
}
@@ -18,7 +18,7 @@ export const addGuacamoleInfo = data => {
};
/**修改guacamole 参数 id:string,url:string,username:string,password:string*/
export const updateGuacamoleInfo = data => {
return http2.post(`/v1/admin/server/edit_guacamole`, data,{
return http2.post(`/acs/v1/admin/server/edit_guacamole`, data,{
headers:{
'Content-Type': 'multipart/form-data'
}
@@ -26,7 +26,7 @@ export const updateGuacamoleInfo = data => {
};
/**删除guacamole 参数 id:string */
export const deleteGuacamoleInfo = data => {
return http2.post(`/v1/admin/server/delete_guacamole`, data,{
return http2.post(`/acs/v1/admin/server/delete_guacamole`, data,{
headers:{
'Content-Type': 'multipart/form-data'
}
+11 -11
View File
@@ -1,15 +1,15 @@
import {http2} from "@/utils/request.js";
/**获取消息列表 */
export const getMessageList = (data) => {
return http2.get(`/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
return http2.get(`/acs/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
}
/**获取单条消息 */
export const getMessage = (data) => {
return http2.get(`/v1/messages/get_message?message_id=${data}`)
return http2.get(`/acs/v1/messages/get_message?message_id=${data}`)
}
/**添加消息 */
export const addMessage = (data) => {
return http2.post(`/v1/messages/add_message`, data,{
return http2.post(`/acs/v1/messages/add_message`, data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -17,7 +17,7 @@ headers: {
}
/**删除消息 */
export const deleteMessage = (data) => {
return http2.post(`/v1/messages/delete_message`, data,{
return http2.post(`/acs/v1/messages/delete_message`, data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -25,7 +25,7 @@ headers: {
}
/**修改消息 */
export const editMessage = (data) => {
return http2.post(`/v1/messages/update_message`, data,{
return http2.post(`/acs/v1/messages/update_message`, data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -33,11 +33,11 @@ headers: {
}
/**获取附件列表 */
export const getFileList = (data) => {
return http2.get(`/v1/attachment/get_attachment_list?page=${data.page}&count=${data.count}&key=${data.key}&user_type=${data.user_type}`)
return http2.get(`/acs/v1/attachment/get_attachment_list?page=${data.page}&count=${data.count}&key=${data.key}&user_type=${data.user_type}`)
}
/**上传附件 */
export const uploadFile = (data) => {
return http2.post(`/v1/attachment/add_attachment`, data,{
return http2.post(`/acs/v1/attachment/add_attachment`, data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -45,20 +45,20 @@ headers: {
}
/**删除附件 */
export const deleteFile = (data) => {
return http2.get(`/v1/attachment/delete_attachment?aid=${data}`)
return http2.get(`/acs/v1/attachment/delete_attachment?aid=${data}`)
}
/**用户获取消息列表 */
export const getUserMessageList = (data) => {
return http2.get(`/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
return http2.get(`/acs/v1/messages/get_message_list?page=${data.page}&count=${data.count}&message_type=${data.message_type}`)
}
/**用户获取单条消息 */
export const getUserMessage = (data) => {
return http2.get(`/v1/messages/get_message?message_id=${data}`)
return http2.get(`/acs/v1/messages/get_message?message_id=${data}`)
}
/**获取消息详情 */
export const getMessageDetail = (data) => {
return http2.get(`/v1/messages/get_message?message_id=${data.message_id}`)
return http2.get(`/acs/v1/messages/get_message?message_id=${data.message_id}`)
}
/**修改图片大小 */
export const compressAndConvertFileToBase64=async(file)=> {
+15 -12
View File
@@ -1,17 +1,20 @@
import {http2} from "@/utils/request.js";
/**获取镜像列表 */
export const getMirrorList = data => {
return http2.get(`/v1/image/list?server_id=${data.server_id}&page=${data.page}&count=${data.count}&key=${data.key}&class_id=${data.class_id}`);
if(typeof data == "string"){
return http2.get("/acs/v1/image/list?server_id=" + data + "&count=9999999")
}
return http2.get(`/acs/v1/image/list?server_id=${data.server_id}&page=${data.page}&count=${data.count}&key=${data.key}&class_id=${data.class_id}`);
};
/*用户获取镜像列表 */
export const getUserMirrorList = data => {
return http2.get(
`/v1/image/list?server_id=${data.server_id}&count=${data.count}&page=${data.page}&key=${data.key}`
`/acs/v1/image/list?server_id=${data.server_id}&count=${data.count}&page=${data.page}&key=${data.key}`
);
};
/**上传镜像 */
export const uploadMirror = data => {
return http2.post("/v1/image/pull", data, {
return http2.post("/acs/v1/image/pull", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -19,7 +22,7 @@ export const uploadMirror = data => {
};
/**编辑镜像 */
export const editMirror = data => {
return http2.post("/v1/image/update", data, {
return http2.post("/acs/v1/image/update", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -27,7 +30,7 @@ export const editMirror = data => {
};
/**删除镜像 */
export const delMirror = data => {
return http2.post("/v1/image/delete", data, {
return http2.post("/acs/v1/image/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -35,11 +38,11 @@ export const delMirror = data => {
};
/**镜像同步 */
export const syncMirror = data => {
return http2.get(`/v1/image/sync?server_id=${data}`);
return http2.get(`/acs/v1/image/sync?server_id=${data}`);
};
/**重新拉取镜像 */
export const pullMirror = data => {
return http2.post(`/v1/image/repull`, data, {
return http2.post(`/acs/v1/image/repull`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -50,12 +53,12 @@ export const pullMirror = data => {
export const Mirrorinfo = data => {
const serverType = data.server_type || "dockerContainer"; // 设置默认值
return http2.get(
`/v1/image/info?image_id=${data.image_id}&server_type=${serverType}`
`/acs/v1/image/info?image_id=${data.image_id}&server_type=${serverType}`
);
};
export const addVirtualMirror = data => {
return http2.post("/v1/image/create", data, {
return http2.post("/acs/v1/image/create", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -64,11 +67,11 @@ export const addVirtualMirror = data => {
export const getImageTypeList = (server_id) => {
return http2.get(`/v1/image/class_list?server_id=${server_id}`);
return http2.get(`/acs/v1/image/class_list?server_id=${server_id}`);
};
export const createImageType = (server_id,class_name,class_ico) => {
return http2.post("/v1/image/class_create", {
return http2.post("/acs/v1/image/class_create", {
server_id,
class_name,
class_ico
@@ -80,7 +83,7 @@ export const createImageType = (server_id,class_name,class_ico) => {
};
export const updateImageType = (class_id,class_name,class_ico) => {
return http2.post("/v1/image/class_update", {
return http2.post("/acs/v1/image/class_update", {
class_id,
class_name,
class_ico
+4 -4
View File
@@ -1,11 +1,11 @@
import {http2} from "@/utils/request.js";
/**获取订单列表 */
export const getOrderList = (data) => {
return http2.get(`/v1/admin/trades/get_trades?page=${data.page}&count=${data.count}&key=${data.key}`)
return http2.get(`/acs/v1/admin/trades/get_trades?page=${data.page}&count=${data.count}&key=${data.key}`)
}
/**编辑订单 */
export const editOrder = (data) => {
return http2.post('/v1/admin/trades/update_trades',data,{
return http2.post('/acs/v1/admin/trades/update_trades',data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -13,7 +13,7 @@ headers: {
}
/**删除订单 */
export const deleteOrder = (data) => {
return http2.post('/v1/admin/trades/delete_trade',data,{
return http2.post('/acs/v1/admin/trades/delete_trade',data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -21,5 +21,5 @@ headers: {
}
/**用户获取订单列表 */
export const getUserOrderList = (data) => {
return http2.get(`/v1/user/procedure/get_trade_list?page=${data.page}&count=${data.count}&key=${data.key}`)
return http2.get(`/acs/v1/user/procedure/get_trade_list?page=${data.page}&count=${data.count}&key=${data.key}`)
}
+3 -3
View File
@@ -7,7 +7,7 @@ export const get_pay_code = data => {
};
// /**email验证码 */
// export const ask_update_user_email = data => {
// return http2.post("/v1/user/info/ask_update_user_email", data, {
// return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
// headers: {
// "Content-Type": "multipart/form-data"
// }
@@ -15,7 +15,7 @@ export const get_pay_code = data => {
// };
/**获取容器订单金额 */
export const procedure_get_price = data => {
return http2.post("/v1/user/procedure/get_price", data, {
return http2.post("/acs/v1/user/procedure/get_price", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -24,7 +24,7 @@ export const procedure_get_price = data => {
/**获取虚拟机订单金额 */
export const procedure_vir_price = data => {
return http2.post("/v1/user/procedure/get_vm_price", data, {
return http2.post("/acs/v1/user/procedure/get_vm_price", data, {
headers: {
"Content-Type": "multipart/form-data"
}
+70 -70
View File
@@ -3,13 +3,13 @@ import {http2} from "@/utils/request.js";
/** 获取所有服务器 */
export const getServer = (page, count, key, type = "dockerContainer") => {
return http2.get(
`/v1/admin/server/get_server_list?page=${page}&count=${count}&key=${key}&server_type=${type}`
`/acs/v1/admin/server/get_server_list?page=${page}&count=${count}&key=${key}&server_type=${type}`
);
};
/**新增服务器 */
export const addServer = data => {
return http2.post("/v1/admin/server/add_server", data, {
return http2.post("/acs/v1/admin/server/add_server", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -17,7 +17,7 @@ export const addServer = data => {
};
/**编辑服务器 */
export const editServer = data => {
return http2.post("/v1/admin/server/update_server", data, {
return http2.post("/acs/v1/admin/server/update_server", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -25,7 +25,7 @@ export const editServer = data => {
};
/**删除服务器 */
export const deleteServer = data => {
return http2.post("/v1/admin/server/delete_server", data, {
return http2.post("/acs/v1/admin/server/delete_server", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -33,7 +33,7 @@ export const deleteServer = data => {
};
/**查询指定服务器 */
export const selectServer = data => {
return http2.post("/v1/admin/server/select_server", data, {
return http2.post("/acs/v1/admin/server/select_server", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -42,12 +42,12 @@ export const selectServer = data => {
/**获取服务器套餐列表*/
export const getServerPlan = data => {
return http2.get(
`/v1/admin/container_plan/get_server_plan_list?server_id=${data.server_id}&count=${data.count}`
`/acs/v1/admin/container_plan/get_server_plan_list?server_id=${data.server_id}&count=${data.count}`
);
};
/**获取指定套餐 */
export const selectServerPlan = data => {
return http2.post("/v1/admin/container_plan/get_server_plan_detail", data, {
return http2.post("/acs/v1/admin/container_plan/get_server_plan_detail", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -55,7 +55,7 @@ export const selectServerPlan = data => {
};
/**新增容器 */
export const addContainer = data => {
return http2.post("/v1/admin/container/add_container", data, {
return http2.post("/acs/v1/admin/container/add_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -63,7 +63,7 @@ export const addContainer = data => {
};
/**删除容器网络 */
export const deleteContainerNetwork = data => {
return http2.post("/v1/user/container/delete_connect", data, {
return http2.post("/acs/v1/user/container/delete_connect", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -72,7 +72,7 @@ export const deleteContainerNetwork = data => {
/**修改套餐信息 */
export const editServerPlan = data => {
return http2.post("/v1/admin/container_plan/update_server_plan", data, {
return http2.post("/acs/v1/admin/container_plan/update_server_plan", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -80,7 +80,7 @@ export const editServerPlan = data => {
};
/**新增套餐 */
export const addServerPlan = data => {
return http2.post("/v1/admin/container_plan/add_server_plan", data, {
return http2.post("/acs/v1/admin/container_plan/add_server_plan", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -88,7 +88,7 @@ export const addServerPlan = data => {
};
/**删除套餐 */
export const deleteServerPlan = data => {
return http2.post("/v1/admin/container_plan/delete_server_plan", data, {
return http2.post("/acs/v1/admin/container_plan/delete_server_plan", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -97,19 +97,19 @@ export const deleteServerPlan = data => {
/**获取容器列表 */
export const getContainer = data => {
return http2.get(
`/v1/admin/container/get_container_list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
`/acs/v1/admin/container/get_container_list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
);
};
/**获取虚拟机列表 */
export const getInstanceList = data => {
return http2.get(
`/v1/admin/instance/list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
`/acs/v1/admin/instance/list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
);
};
/**获取单个指定容器 */
export const getOneContainer = data => {
return http2.post("/v1/admin/container/get_container_detail", data, {
return http2.post("/acs/v1/admin/container/get_container_detail", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -117,11 +117,11 @@ export const getOneContainer = data => {
};
/**查询指定虚拟机信息(管理员查询) */
export const getVmAdminContainer = id => {
return http2.get(`/v1/admin/instance/detail/${id}`);
return http2.get(`/acs/v1/admin/instance/detail/${id}`);
};
// 暂停容器
export const pauseContainer = data => {
return http2.post("/v1/admin/container/pause_container", data, {
return http2.post("/acs/v1/admin/container/pause_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -129,7 +129,7 @@ export const pauseContainer = data => {
};
// 暂停虚拟机
export const pauseInstance = (data, id) => {
return http2.post(`/v1/admin/instance/pause/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/pause/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -137,7 +137,7 @@ export const pauseInstance = (data, id) => {
};
/**恢复虚拟机 */
export const unpauseInstance = (id, data = "") => {
return http2.post(`/v1/admin/instance/resume/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/resume/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -146,7 +146,7 @@ export const unpauseInstance = (id, data = "") => {
// 解除暂停
export const unpauseContainer = data => {
return http2.post("/v1/admin/container/unpause_container", data, {
return http2.post("/acs/v1/admin/container/unpause_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -154,7 +154,7 @@ export const unpauseContainer = data => {
};
/**获取容器状态 */
export const getContainerStatus = data => {
return http2.post("/v1/admin/container/get_container_status", data, {
return http2.post("/acs/v1/admin/container/get_container_status", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -162,15 +162,15 @@ export const getContainerStatus = data => {
};
/**获取虚拟机状态 */
export const getInstanceStatus = id => {
return http2.get(`/v1/admin/instance/get_state/${id}`);
return http2.get(`/acs/v1/admin/instance/get_state/${id}`);
};
/**查询服务器状态 */
export const getServerStatus = id => {
return http2.get(`/v1/admin/server/send_server_status?server_id=${id}`);
return http2.get(`/acs/v1/admin/server/send_server_status?server_id=${id}`);
};
/**开通容器 */
export const openContainer = data => {
return http2.post("/v1/admin/container/open_container", data, {
return http2.post("/acs/v1/admin/container/open_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -178,7 +178,7 @@ export const openContainer = data => {
};
/**开通虚拟机 */
export const openInstance = (id, data = "") => {
return http2.post(`/v1/admin/instance/approve/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/approve/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -186,7 +186,7 @@ export const openInstance = (id, data = "") => {
};
/**启动容器 */
export const startContainer = data => {
return http2.post("/v1/admin/container/start_container", data, {
return http2.post("/acs/v1/admin/container/start_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -194,11 +194,11 @@ export const startContainer = data => {
};
/**启动虚拟机 */
export const startInstance = data => {
return http2.get(`/v1/admin/instance/start/${data}`);
return http2.get(`/acs/v1/admin/instance/start/${data}`);
};
/**重装容器 */
export const reinstallC = data => {
return http2.post("/v1/admin/container/reinstall_container", data, {
return http2.post("/acs/v1/admin/container/reinstall_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -206,7 +206,7 @@ export const reinstallC = data => {
};
/**重装虚拟机 */
export const reinstallI = (data, id) => {
return http2.post(`/v1/admin/instance/reinstall/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/reinstall/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -215,7 +215,7 @@ export const reinstallI = (data, id) => {
/**获取容器日志 */
export const getContainerLog = data => {
return http2.post(`/v1/admin/container/get_container_log`, data, {
return http2.post(`/acs/v1/admin/container/get_container_log`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -224,13 +224,13 @@ export const getContainerLog = data => {
/**获取虚拟机操作日志 */
export const getInstanceLog = (id, data) => {
return http2.get(
`/v1/admin/instance/log/${id}?page=${data.page}&count=${data.count}`
`/acs/v1/admin/instance/log/${id}?page=${data.page}&count=${data.count}`
);
};
/**重启容器 */
export const restartContainer = data => {
return http2.post("/v1/admin/container/reboot_container", data, {
return http2.post("/acs/v1/admin/container/reboot_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -238,12 +238,12 @@ export const restartContainer = data => {
};
/**重启虚拟机 */
export const restartInstance = data => {
return http2.get(`/v1/admin/instance/reboot/${data}`);
return http2.get(`/acs/v1/admin/instance/reboot/${data}`);
};
/**停止容器 */
export const stopContainer = data => {
return http2.post("/v1/admin/container/stop_container", data, {
return http2.post("/acs/v1/admin/container/stop_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -251,12 +251,12 @@ export const stopContainer = data => {
};
/**停止虚拟机 */
export const stopInstance = data => {
return http2.get(`/v1/admin/instance/stop/${data}`);
return http2.get(`/acs/v1/admin/instance/stop/${data}`);
};
/**删除容器 */
export const deleteContainer = data => {
return http2.post("/v1/admin/container/delete_container", data, {
return http2.post("/acs/v1/admin/container/delete_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -264,7 +264,7 @@ export const deleteContainer = data => {
};
/**删除虚拟机 */
export const deleteInstance = (id, data = "") => {
return http2.post(`/v1/admin/instance/delete/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/delete/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -272,7 +272,7 @@ export const deleteInstance = (id, data = "") => {
};
/**清除容器流量 */
export const clearContainerTraffic = data => {
return http2.post("/v1/admin/container/clear_container_traffic", data, {
return http2.post("/acs/v1/admin/container/clear_container_traffic", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -280,7 +280,7 @@ export const clearContainerTraffic = data => {
};
/**连接控制台 */
export const connectConsole = data => {
return http2.post("/v1/admin/container/get_container_console", data, {
return http2.post("/acs/v1/admin/container/get_container_console", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -288,7 +288,7 @@ export const connectConsole = data => {
};
/**新增虚拟机 (管理员) */
export const addInstance = data => {
return http2.post("/v1/admin/instance/create_vm", data, {
return http2.post("/acs/v1/admin/instance/create_vm", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -296,21 +296,21 @@ export const addInstance = data => {
};
/**获取虚拟机控制台 */
export const getInstanceConsole = data => {
return http2.get(`/v1/admin/instance/console/${data}`);
return http2.get(`/acs/v1/admin/instance/console/${data}`);
};
/**查询容器所有卷信息 */
export const getVolumeList = data => {
return http2.get(`/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`);
return http2.get(`/acs/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`);
};
/**查询虚拟机所有卷信息 */
export const getInstanceVolumeList = data => {
return http2.get(
`/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`
`/acs/v1/admin/volume/get_volume_list?instance_id=${data.instance_id}&page=${data.page}&count=${data.count}`
);
};
/**新增卷 */
export const addVolume = data => {
return http2.post("/v1/admin/volume/add_volume", data, {
return http2.post("/acs/v1/admin/volume/add_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -318,7 +318,7 @@ export const addVolume = data => {
};
/**修改卷大小 */
export const updateVolume = data => {
return http2.post("/v1/admin/volume/update_volume_size", data, {
return http2.post("/acs/v1/admin/volume/update_volume_size", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -326,7 +326,7 @@ export const updateVolume = data => {
};
/**删除数据卷 */
export const deleteVolume = data => {
return http2.post("/v1/admin/volume/delete_volume", data, {
return http2.post("/acs/v1/admin/volume/delete_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -336,7 +336,7 @@ export const deleteVolume = data => {
/**获取容器网络信息 */
export const getNetworkList = data => {
return http2.get(
`/v1/container/proxy/get_container_proxy?container_id=${data}`
`/acs/v1/container/proxy/get_container_proxy?container_id=${data}`
);
};
/**获取虚拟机端口列表 */
@@ -347,12 +347,12 @@ export const getInstancePortList = data => {
if (data.internal_port !== undefined)
params.append("internal_port", data.internal_port.toString());
return http2.get(
`/v1/admin/instance_port/list?instance_id=${data.instance_id}&${params.toString()}`
`/acs/v1/admin/instance_port/list?instance_id=${data.instance_id}&${params.toString()}`
);
};
/**添加容器网络 */
export const addNetwork = data => {
return http2.post("/v1/container/proxy/add_container_proxy", data, {
return http2.post("/acs/v1/container/proxy/add_container_proxy", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -360,7 +360,7 @@ export const addNetwork = data => {
};
/**创建端口 */
export const addPort = data => {
return http2.post("/v1/admin/instance_port/create", data, {
return http2.post("/acs/v1/admin/instance_port/create", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -369,12 +369,12 @@ export const addPort = data => {
/**获取浮动ip列表 */
export const getFloatingIpList = data => {
return http2.get(
`/v1/admin/floating_ip/get_list?server_id=${data.server_id}&page=${data.page}&count=${data.count}`
`/acs/v1/admin/floating_ip/get_list?server_id=${data.server_id}&page=${data.page}&count=${data.count}`
);
};
/**新增浮动ip */
export const addFloatingIp = data => {
return http2.post("/v1/admin/floating_ip/add", data, {
return http2.post("/acs/v1/admin/floating_ip/add", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -382,7 +382,7 @@ export const addFloatingIp = data => {
};
/**批量添加浮动ip */
export const addFloatingIpBatch = data => {
return http2.post("/v1/admin/floating_ip/add_list", data, {
return http2.post("/acs/v1/admin/floating_ip/add_list", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -390,7 +390,7 @@ export const addFloatingIpBatch = data => {
};
/**删除浮动ip */
export const delFloatingIp = data => {
return http2.post("/v1/admin/floating_ip/delete", data, {
return http2.post("/acs/v1/admin/floating_ip/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -400,13 +400,13 @@ export const delFloatingIp = data => {
/**获取单个用户操作日志 */
export const getUserLog = data => {
return http2.get(
`/v1/user/procedure/get_user_log?user_id=${data.user_id}&page=${data.page}&count=${data.count}`
`/acs/v1/user/procedure/get_user_log?user_id=${data.user_id}&page=${data.page}&count=${data.count}`
);
};
/**管理员修改头像 */
export const editAvatar = data => {
return http2.post("/v1/admin/users/upload_user_avatar", data, {
return http2.post("/acs/v1/admin/users/upload_user_avatar", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -415,28 +415,28 @@ export const editAvatar = data => {
/**获取服务器硬盘信息 */
export const getDiskInfo = data => {
return http2.get(`/v1/admin/server/get_server_disk?server_id=${data}`);
return http2.get(`/acs/v1/admin/server/get_server_disk?server_id=${data}`);
};
/**获取服务器实际划分硬盘信息 */
export const getRealDisk = data => {
return http2.get(`/v1/admin/server/get_server_disk_info?server_id=${data}`);
return http2.get(`/acs/v1/admin/server/get_server_disk_info?server_id=${data}`);
};
/**获取服务器流量信息 */
export const getTraffic = data => {
return http2.get(`/v1/admin/server/get_server_bandwidth?server_id=${data}`);
return http2.get(`/acs/v1/admin/server/get_server_bandwidth?server_id=${data}`);
};
/**获取服务器总流量信息 */
export const getTotalTraffic = data => {
return http2.get(`/v1/admin/server/get_server_total_bandwidth?server_id=${data}`);
return http2.get(`/acs/v1/admin/server/get_server_total_bandwidth?server_id=${data}`);
};
/**获取版本更新 */
export const getVersion = () => {
return http2.get(`/v1/admin/version`);
return http2.get(`/acs/v1/admin/version`);
};
// 管理员删除https网络
export const AdminDelHttps = data => {
return http2.post("/v1/container/proxy/del_https_connet", data, {
return http2.post("/acs/v1/container/proxy/del_https_connet", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -445,7 +445,7 @@ export const AdminDelHttps = data => {
// 管理员添加https网络
export const AdminAddHttps = data => {
return http2.post("/v1/container/proxy/add_https_proxy", data, {
return http2.post("/acs/v1/container/proxy/add_https_proxy", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -453,11 +453,11 @@ export const AdminAddHttps = data => {
};
/**获取指定端口信息 */
export const getPortInfo = data => {
return http2.get(`/v1/admin/instance_port/detail?port_id=${data}`);
return http2.get(`/acs/v1/admin/instance_port/detail?port_id=${data}`);
};
/**新增卷 */
export const addVolumeMount = data => {
return http2.post("/v1/admin/volume/add_volume", data, {
return http2.post("/acs/v1/admin/volume/add_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -466,17 +466,17 @@ export const addVolumeMount = data => {
/**进入救援系统 */
export const rescueInstance = id => {
return http2.get(`/v1/admin/instance/rescue/enter/${id}`);
return http2.get(`/acs/v1/admin/instance/rescue/enter/${id}`);
};
/**退出救援系统 */
export const exitRescueInstance = id => {
return http2.get(`/v1/admin/instance/rescue/exit/${id}`);
return http2.get(`/acs/v1/admin/instance/rescue/exit/${id}`);
};
/**修改虚拟机密码 */
export const changeInstancePassword = (id, data) => {
return http2.post(`/v1/admin/instance/update_password/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/update_password/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -484,7 +484,7 @@ export const changeInstancePassword = (id, data) => {
};
/**修改虚拟机密码(用户) */
export const changeInstancePasswordUser = (id, data) => {
return http2.post(`/v1/user/instance/update_password/${id}`, data, {
return http2.post(`/acs/v1/user/instance/update_password/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -493,7 +493,7 @@ export const changeInstancePasswordUser = (id, data) => {
/**删除端口 */
export const deletePort = data => {
return http2.post("/v1/admin/instance_port/delete", data, {
return http2.post("/acs/v1/admin/instance_port/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
+7 -7
View File
@@ -1,11 +1,11 @@
import {http2} from "@/utils/request.js";
/**获取全局配置 */
export const getSetting = () => {
return http2.get('/v1/admin/settings/get_settings')
return http2.get('/acs/v1/admin/settings/get_settings')
}
/**变更设置 */
export const updateSetting = (data) => {
return http2.post('/v1/admin/settings/update_settings', data, {
return http2.post('/acs/v1/admin/settings/update_settings', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -13,7 +13,7 @@ export const updateSetting = (data) => {
}
/**新增设置 */
export const addSetting = (data) => {
return http2.post('/v1/admin/settings/add_settings', data, {
return http2.post('/acs/v1/admin/settings/add_settings', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -21,7 +21,7 @@ export const addSetting = (data) => {
}
/**删除设置 */
export const deleteSetting = (data) => {
return http2.post('/v1/admin/settings/delete_settings', data,{
return http2.post('/acs/v1/admin/settings/delete_settings', data,{
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -29,12 +29,12 @@ export const deleteSetting = (data) => {
}
/**获取单项配置 */
export const getOneSetting = (data) => {
return http2.get(`/v1/admin/settings/get_setting?name=${data}`)
return http2.get(`/acs/v1/admin/settings/get_setting?name=${data}`)
}
/**获取多个配置 */
export const getSettings = (data) => {
// return http2.get(`/v1/admin/settings/get_settings?names=${data}`);
// return http2.get(`/acs/v1/admin/settings/get_settings?names=${data}`);
const namesParam = data.join(',');
// 将处理后的namesParam放入URL中
return http2.get(`/v1/admin/settings/get_setting?names=${encodeURIComponent(namesParam)}`);
return http2.get(`/acs/v1/admin/settings/get_setting?names=${encodeURIComponent(namesParam)}`);
}
+6 -6
View File
@@ -1,11 +1,11 @@
import {http2} from "@/utils/request.js";
/**获取用户列表 */
export const ask_update_user_email11 = data => {
return http2.get(`/v1/user/info/ask_update_user_email?email=${data.email}`);
return http2.get(`/acs/v1/user/info/ask_update_user_email?email=${data.email}`);
};
/**email验证码 */
export const ask_update_user_email = data => {
return http2.post("/v1/user/info/ask_update_user_email", data, {
return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -13,7 +13,7 @@ export const ask_update_user_email = data => {
};
/**email修改 */
export const update_user_email = data => {
return http2.post("/v1/user/info/update_user_email", data, {
return http2.post("/acs/v1/user/info/update_user_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -21,7 +21,7 @@ export const update_user_email = data => {
};
/**phone验证码 */
export const ask_update_user_phone = data => {
return http2.post("/v1/user/info/ask_update_user_phone", data, {
return http2.post("/acs/v1/user/info/ask_update_user_phone", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -29,7 +29,7 @@ export const ask_update_user_phone = data => {
};
/**phone修改 */
export const update_user_phone = data => {
return http2.post("/v1/user/info/update_user_phone", data, {
return http2.post("/acs/v1/user/info/update_user_phone", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -37,7 +37,7 @@ export const update_user_phone = data => {
};
/**密码修改 */
export const update_user_password = data => {
return http2.post("/v1/user/info/update_user_password", data, {
return http2.post("/acs/v1/user/info/update_user_password", data, {
headers: {
"Content-Type": "multipart/form-data"
}
+67 -67
View File
@@ -4,12 +4,12 @@ import {http2} from "@/utils/request.js";
// 获取图像验证码
export const Captch = data => {
return http2.get(`/v1/user/check/get_code_img`);
return http2.get(`/acs/v1/user/check/get_code_img`);
};
/** 登录 */
export const getLogin = data => {
return http2.post("/v1/user/login", data, {
return http2.post("/acs/v1/user/login", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -24,12 +24,12 @@ export const getLogin = data => {
/**获取用户列表 */
export const getUserList = data => {
return http2.get(
`/v1/admin/users/get_user_list?page=${data.page}&count=${data.count}&key=${data.key}`
`/acs/v1/admin/users/get_user_list?page=${data.page}&count=${data.count}&key=${data.key}`
);
};
/**添加用户 */
export const addUser = data => {
return http2.post("/v1/admin/users/add_user", data, {
return http2.post("/acs/v1/admin/users/add_user", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -37,7 +37,7 @@ export const addUser = data => {
};
/**编辑用户信息 */
export const editUser = data => {
return http2.post("/v1/admin/users/update_user", data, {
return http2.post("/acs/v1/admin/users/update_user", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -45,7 +45,7 @@ export const editUser = data => {
};
/**修改用户密码 */
export const editPassword = data => {
return http2.post("/v1/admin/users/update_user_password", data, {
return http2.post("/acs/v1/admin/users/update_user_password", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -53,7 +53,7 @@ export const editPassword = data => {
};
/**删除用户 */
export const deleteUser = data => {
return http2.post("/v1/admin/users/del_user", data, {
return http2.post("/acs/v1/admin/users/del_user", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -61,7 +61,7 @@ export const deleteUser = data => {
};
/**查询单个用户 */
export const userDetail = data => {
return http2.post("/v1/admin/users/select_user", data, {
return http2.post("/acs/v1/admin/users/select_user", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -69,7 +69,7 @@ export const userDetail = data => {
};
/**修改用户余额 */
export const editBalance = data => {
return http2.post("/v1/admin/users/update_user_balance", data, {
return http2.post("/acs/v1/admin/users/update_user_balance", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -79,12 +79,12 @@ export const editBalance = data => {
export const getUserServer = (data = {}) => {
const serverType = data.server_type || "dockerContainer"; // 设置默认值
return http2.get(
`/v1/user/procedure/get_server_list?server_type=${serverType}`
`/acs/v1/user/procedure/get_server_list?server_type=${serverType}`
);
};
/**用户获取虚拟机列表 */
export const getVirtualList = data => {
let url = `/v1/user/instance/list?page=${data.page}&count=${data.count}`;
let url = `/acs/v1/user/instance/list?page=${data.page}&count=${data.count}`;
if (data.key) {
url += `&key=${data.key}`;
}
@@ -95,35 +95,35 @@ export const getVirtualList = data => {
};
/**用户获取服务器套餐 */
export const getUserPackage = data => {
return http2.get(`/v1/user/procedure/get_server_plan_list?server_id=${data}`);
return http2.get(`/acs/v1/user/procedure/get_server_plan_list?server_id=${data}`);
};
/**获取用户容器列表 */
export const getUserContainer = data => {
return http2.get(
`/v1/user/container/list?page=${data.page}&count=${data.count}`
`/acs/v1/user/container/list?page=${data.page}&count=${data.count}`
);
};
/**用户按地区获取容器 */
export const getUserContainerD = data => {
return http2.get(
`/v1/user/container/list?page=${data.page}&count=${data.count}&server_id=${data.server_id}`
`/acs/v1/user/container/list?page=${data.page}&count=${data.count}&server_id=${data.server_id}`
);
};
/**获取用户操作日志 */
export const get_user_log = () => {
return http2.get(`/v1/user/procedure/get_user_log`);
return http2.get(`/acs/v1/user/procedure/get_user_log`);
};
/**获取用户自身信息 */
export const getUserInfo = () => {
return http2.get(`/v1/user/procedure/get_user_info`);
return http2.get(`/acs/v1/user/procedure/get_user_info`);
};
/**获取用户自身信息 */
export const getUserInfoV1 = () => {
return http2.get(`/v1/user/info/get_user_info`);
return http2.get(`/acs/v1/user/info/get_user_info`);
};
/**用户实名 */
export const realName = data => {
return http2.post("/v1/external/real_name", data, {
return http2.post("/acs/v1/external/real_name", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -131,7 +131,7 @@ export const realName = data => {
};
/**发送手机验证码 */
export const sendCode = data => {
return http2.post("/v1/external/send_message", data, {
return http2.post("/acs/v1/external/send_message", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -139,7 +139,7 @@ export const sendCode = data => {
};
/**发送邮箱验证码 */
export const sendEmailCode = data => {
return http2.post("/v1/external/send_email", data, {
return http2.post("/acs/v1/external/send_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -147,7 +147,7 @@ export const sendEmailCode = data => {
};
/**用户注册 */
export const register = data => {
return http2.post("/v1/user/register", data, {
return http2.post("/acs/v1/user/register", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -155,7 +155,7 @@ export const register = data => {
};
/**手机号修改校证码 */
export const CodePhone = data => {
return http2.post("/v1/user/info/ask_update_user_phone", data, {
return http2.post("/acs/v1/user/info/ask_update_user_phone", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -163,7 +163,7 @@ export const CodePhone = data => {
};
/**修改手机号码 */
export const SetPhone = data => {
return http2.post("/v1/user/info/update_user_phone", data, {
return http2.post("/acs/v1/user/info/update_user_phone", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -171,7 +171,7 @@ export const SetPhone = data => {
};
/**邮箱修改校证码 */
export const CodeEmail = data => {
return http2.post("/v1/user/info/ask_update_user_email", data, {
return http2.post("/acs/v1/user/info/ask_update_user_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -179,7 +179,7 @@ export const CodeEmail = data => {
};
/**修改邮箱 */
export const SetEmail = data => {
return http2.post("/v1/user/info/update_user_email", data, {
return http2.post("/acs/v1/user/info/update_user_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -187,7 +187,7 @@ export const SetEmail = data => {
};
/**上传头像 */
export const uploadAvatar = data => {
return http2.post("/v1/user/info/upload_user_avatar", data, {
return http2.post("/acs/v1/user/info/upload_user_avatar", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -195,7 +195,7 @@ export const uploadAvatar = data => {
};
/**手机号忘记密码 */
export const forgetphone = data => {
return http2.post("/v1/user/info/forget_user_password_phone", data, {
return http2.post("/acs/v1/user/info/forget_user_password_phone", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -204,7 +204,7 @@ export const forgetphone = data => {
/**邮箱忘记密码 */
export const forgetemail = data => {
return http2.post("/v1/user/info/forget_user_password_email", data, {
return http2.post("/acs/v1/user/info/forget_user_password_email", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -212,7 +212,7 @@ export const forgetemail = data => {
};
/**管理员全局搜索 */
export const Find = data => {
return http2.post("/v1/admin/search", data, {
return http2.post("/acs/v1/admin/search", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -220,7 +220,7 @@ export const Find = data => {
};
// 管理员删除容器网络
export const delContainer = data => {
return http2.post("/v1/user/container/delete_connect", data, {
return http2.post("/acs/v1/user/container/delete_connect", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -228,7 +228,7 @@ export const delContainer = data => {
};
// 删除端口
export const delPort = data => {
return http2.post("/v1/admin/instance_port/delete", data, {
return http2.post("/acs/v1/admin/instance_port/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -236,7 +236,7 @@ export const delPort = data => {
};
// 自定义容器价格
export const Containerpay = data => {
return http2.post("/v1/admin/container/update_container_price", data, {
return http2.post("/acs/v1/admin/container/update_container_price", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -244,7 +244,7 @@ export const Containerpay = data => {
};
// 修改虚拟机续费价格
export const Containerpaytime = (data, id) => {
return http2.post(`/v1/admin/instance/update_price/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/update_price/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -252,7 +252,7 @@ export const Containerpaytime = (data, id) => {
};
// 自定义容器到期时间
export const Containertime = data => {
return http2.post("/v1/admin/container/update_container_expire_time", data, {
return http2.post("/acs/v1/admin/container/update_container_expire_time", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -260,7 +260,7 @@ export const Containertime = data => {
};
// 修改虚拟机续到期时间
export const Containertimetime = (data, id) => {
return http2.post(`/v1/admin/instance/update_expire_time/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/update_expire_time/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -268,7 +268,7 @@ export const Containertimetime = (data, id) => {
};
// 修改虚拟机信息
export const editContainer = (data, id) => {
return http2.post(`/v1/admin/instance/update/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/update/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -280,7 +280,7 @@ export const editContainer = (data, id) => {
/** 容器操作 */
export const startUserContainer = (type, id) => {
return http2.post(
"/v1/user/container/" + type,
"/acs/v1/user/container/" + type,
{
container_id: id
},
@@ -293,7 +293,7 @@ export const startUserContainer = (type, id) => {
};
/**用户容器退款 */
export const backUserContainer = data => {
return http2.post("/v1/user/container/delete", data, {
return http2.post("/acs/v1/user/container/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -301,7 +301,7 @@ export const backUserContainer = data => {
};
/**重装容器 */
export const reinContainer = data => {
return http2.post("/v1/user/container/reinstall", data, {
return http2.post("/acs/v1/user/container/reinstall", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -309,7 +309,7 @@ export const reinContainer = data => {
};
/**重装虚拟机 */
export const reinVmContainer = (id, data) => {
return http2.post(`/v1/user/instance/reinstall/${id}`, data, {
return http2.post(`/acs/v1/user/instance/reinstall/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -318,7 +318,7 @@ export const reinVmContainer = (id, data) => {
/** 容器操作 */
export const startAdminContainer = (type, id) => {
return http2.post(
"/v1/admin/container/" + type,
"/acs/v1/admin/container/" + type,
{
container_id: id
},
@@ -331,7 +331,7 @@ export const startAdminContainer = (type, id) => {
};
/** 容器操作 */
export const procedureUpdateContainerRenew = data => {
return http2.post("/v1/user/procedure/update_container_renew", data, {
return http2.post("/acs/v1/user/procedure/update_container_renew", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -339,15 +339,15 @@ export const procedureUpdateContainerRenew = data => {
};
/**获取容器完整信息 */
export const getContainerDetail = id => {
return http2.get(`/v1/user/container/detail?container_id=${id}`);
return http2.get(`/acs/v1/user/container/detail?container_id=${id}`);
};
/**获取虚拟机完整信息 */
export const getVmContainerDetail = id => {
return http2.get(`/v1/user/instance/detail/${id}`);
return http2.get(`/acs/v1/user/instance/detail/${id}`);
};
/**容器操作信息 */
export const containerLog = data => {
return http2.post("/v1/user/container/logs", data, {
return http2.post("/acs/v1/user/container/logs", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -356,12 +356,12 @@ export const containerLog = data => {
/**虚拟机操作日志 */
export const vmLog = data => {
return http2.get(
`/v1/user/instance/log/${data.id}?page=${data.page}&count=${data.count}`
`/acs/v1/user/instance/log/${data.id}?page=${data.page}&count=${data.count}`
);
};
/**获取容器状态 */
export const getContainerStatus = data => {
return http2.post(`/v1/user/container/status`, data, {
return http2.post(`/acs/v1/user/container/status`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -369,11 +369,11 @@ export const getContainerStatus = data => {
};
/**获取虚拟机状态 */
export const getVmStatus = id => {
return http2.get(`/v1/user/instance/get_state/${id}`);
return http2.get(`/acs/v1/user/instance/get_state/${id}`);
};
/**获取容器运行日志 */
export const getContainerLog = data => {
return http2.post(`/v1/user/container/run_logs`, data, {
return http2.post(`/acs/v1/user/container/run_logs`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -381,7 +381,7 @@ export const getContainerLog = data => {
};
/**获取容器购买网络订单 */
export const getContainerList = data => {
return http2.post(`/v1/user/procedure/add_network`, data, {
return http2.post(`/acs/v1/user/procedure/add_network`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -389,7 +389,7 @@ export const getContainerList = data => {
};
/**计算容器网络价格 */
export const getContainerPrice = data => {
return http2.post(`/v1/user/procedure/get_price_network`, data, {
return http2.post(`/acs/v1/user/procedure/get_price_network`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -397,33 +397,33 @@ export const getContainerPrice = data => {
};
/** 启动虚拟机 */
export const start_vm = id => {
return http2.get(`/v1/user/instance/start/${id}`);
return http2.get(`/acs/v1/user/instance/start/${id}`);
};
/** 停止虚拟机(关机) */
export const stop_vm = id => {
return http2.get(`/v1/user/instance/stop/${id}`);
return http2.get(`/acs/v1/user/instance/stop/${id}`);
};
/**重启虚拟机 */
export const restart_vm = id => {
return http2.get(`/v1/user/instance/reboot/${id}`);
return http2.get(`/acs/v1/user/instance/reboot/${id}`);
};
/**获取虚拟机控制台 */
export const get_vm_console = id => {
return http2.get(`/v1/user/instance/console/${id}`);
return http2.get(`/acs/v1/user/instance/console/${id}`);
};
/**进入救援系统 */
export const rescue_vm = id => {
return http2.get(`/v1/user/instance/rescue/enter/${id}`);
return http2.get(`/acs/v1/user/instance/rescue/enter/${id}`);
};
/**退出救援系统 */
export const unrescue_vm = id => {
return http2.get(`/v1/user/instance/rescue/exit/${id}`);
return http2.get(`/acs/v1/user/instance/rescue/exit/${id}`);
};
// ******************************* new
/** 提交充值订单 */
export const user_update_container_recharge = data => {
return http2.post("/v1/user/procedure/update_container_recharge", data, {
return http2.post("/acs/v1/user/procedure/update_container_recharge", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -431,7 +431,7 @@ export const user_update_container_recharge = data => {
};
/** 提交容器订单 */
export const user_update_plan_info = data => {
return http2.post("/v1/user/procedure/update_plan_info", data, {
return http2.post("/acs/v1/user/procedure/update_plan_info", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -439,7 +439,7 @@ export const user_update_plan_info = data => {
};
/** 提交虚拟机订单 */
export const user_update_vm_info = data => {
return http2.post("/v1/user/procedure/create_vm_trade", data, {
return http2.post("/acs/v1/user/procedure/create_vm_trade", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -447,11 +447,11 @@ export const user_update_vm_info = data => {
};
/**获取订单简略信息 */
export const getOrderDetail = id => {
return http2.get(`/v1/user/procedure/get_low_trade_info?trade_id=${id}`);
return http2.get(`/acs/v1/user/procedure/get_low_trade_info?trade_id=${id}`);
};
/**支付请求 */
export const pay_request = data => {
return http2.post("/v1/external/pay", data, {
return http2.post("/acs/v1/external/pay", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -459,7 +459,7 @@ export const pay_request = data => {
};
/**用户删除容器网络 */
export const deleteConNet = data => {
return http2.post("/v1/user/container/delete_connect", data, {
return http2.post("/acs/v1/user/container/delete_connect", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -468,7 +468,7 @@ export const deleteConNet = data => {
// 添加https
export const additionHttp = data => {
return http2.post("/v1/user/container/add_https_connet", data, {
return http2.post("/acs/v1/user/container/add_https_connet", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -477,7 +477,7 @@ export const additionHttp = data => {
// 删除https
export const DelHttp = data => {
return http2.post("/v1/user/container/del_https_connet", data, {
return http2.post("/acs/v1/user/container/del_https_connet", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -485,7 +485,7 @@ export const DelHttp = data => {
};
/**获取新增虚拟机端口价格 */
export const getVmPortPrice = data => {
return http2.post("/v1/user/procedure/get_price_instance_port", data, {
return http2.post("/acs/v1/user/procedure/get_price_instance_port", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -493,7 +493,7 @@ export const getVmPortPrice = data => {
};
/**提交新增虚拟机端口订单 */
export const addVmPort = data => {
return http2.post("/v1/user/procedure/add_instance_port", data, {
return http2.post("/acs/v1/user/procedure/add_instance_port", data, {
headers: {
"Content-Type": "multipart/form-data"
}
+22 -22
View File
@@ -3,7 +3,7 @@ import {http2} from "@/utils/request.js";
/**获取虚拟机列表 */
export const getVirtualList = data => {
let url = `/v1/admin/instance/list?page=${data.page}&count=${data.count}`;
let url = `/acs/v1/admin/instance/list?page=${data.page}&count=${data.count}`;
if (data.key) {
url += `&key=${data.key}`;
}
@@ -18,12 +18,12 @@ export const getVirtualList = data => {
/**新增虚拟机 */
export const addVirtual = data => {
return http2.post("/v1/admin/instance/create_vm", data);
return http2.post("/acs/v1/admin/instance/create_vm", data);
};
/**迁移数据卷 */
export const migrate_disk = data => {
return http2.post("/v1/admin/volume/migrate_volume", data, {
return http2.post("/acs/v1/admin/volume/migrate_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -32,18 +32,18 @@ export const migrate_disk = data => {
/**获取虚拟机访问控制列表 */
export const getVirtualAccessList = data => {
let url = `/v1/admin/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
let url = `/acs/v1/admin/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
return http2.get(url);
};
/**获取虚拟机访问控制列表(用户) */
export const getUserAccessList = data => {
let url = `/v1/user/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
let url = `/acs/v1/user/instance/access_control/list?page=${data.page}&count=${data.count}&instance_id=${data.instance_id}`;
return http2.get(url);
};
/**创建访问控制 */
export const createAccessControl = data => {
return http2.post("/v1/admin/instance/access_control/create", data, {
return http2.post("/acs/v1/admin/instance/access_control/create", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -51,7 +51,7 @@ export const createAccessControl = data => {
};
/**创建访问控制(用户) */
export const createUserAccessControl = data => {
return http2.post("/v1/user/instance/access_control/create", data, {
return http2.post("/acs/v1/user/instance/access_control/create", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -59,7 +59,7 @@ export const createUserAccessControl = data => {
};
/**删除访问控制 */
export const deleteAccessControl = data => {
return http2.post("/v1/admin/instance/access_control/delete", data, {
return http2.post("/acs/v1/admin/instance/access_control/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -67,7 +67,7 @@ export const deleteAccessControl = data => {
};
/**删除访问控制(用户) */
export const deleteUserAccessControl = data => {
return http2.post("/v1/user/instance/access_control/delete", data, {
return http2.post("/acs/v1/user/instance/access_control/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -76,17 +76,17 @@ export const deleteUserAccessControl = data => {
/**获取虚拟机快照列表 */
export const getSnapshotList = data => {
let url = `/v1/admin/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
let url = `/acs/v1/admin/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
return http2.get(url);
};
/**获取虚拟机快照列表(用户) */
export const getUserSnapshotList = data => {
let url = `/v1/user/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
let url = `/acs/v1/user/instance/snapshot/list/${data.instance_id}?page=${data.page}&count=${data.count}`;
return http2.get(url);
};
/**创建虚拟机快照 */
export const createSnapshot = (data, id) => {
return http2.post(`/v1/admin/instance/snapshot/create/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/snapshot/create/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -94,7 +94,7 @@ export const createSnapshot = (data, id) => {
};
/**创建虚拟机快照(用户) */
export const createUserSnapshot = (data, id) => {
return http2.post(`/v1/user/instance/snapshot/create/${id}`, data, {
return http2.post(`/acs/v1/user/instance/snapshot/create/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -103,7 +103,7 @@ export const createUserSnapshot = (data, id) => {
/**删除虚拟机快照 */
export const deleteSnapshot = (data, id) => {
return http2.post(`/v1/admin/instance/snapshot/delete/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/snapshot/delete/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -112,7 +112,7 @@ export const deleteSnapshot = (data, id) => {
/**恢复虚拟机快照 */
export const recoverSnapshot = (data, id) => {
return http2.post(`/v1/admin/instance/snapshot/restore/${id}`, data, {
return http2.post(`/acs/v1/admin/instance/snapshot/restore/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -120,7 +120,7 @@ export const recoverSnapshot = (data, id) => {
};
/**恢复虚拟机快照(用户) */
export const recoverUserSnapshot = (data, id) => {
return http2.post(`/v1/user/instance/snapshot/restore/${id}`, data, {
return http2.post(`/acs/v1/user/instance/snapshot/restore/${id}`, data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -129,24 +129,24 @@ export const recoverUserSnapshot = (data, id) => {
/**获取实时监控 */
export const getVirtualLog = data => {
return http2.get(
`/v1/admin/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
`/acs/v1/admin/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
);
};
/**获取实时监控(用户) */
export const getUserVirtualLog = data => {
return http2.get(
`/v1/user/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
`/acs/v1/user/instance/run_logs/${data.id}?start_time=${data.start_time}&end_time=${data.end_time}`
);
};
/**获取新增虚拟机快照数量价格 */
export const getSnapshotPrice = data => {
return http2.get(`/v1/user/procedure/get_price_snapshot?num=${data}`);
return http2.get(`/acs/v1/user/procedure/get_price_snapshot?num=${data}`);
};
/**提交虚拟机购买快照订单 */
export const submitSnapshotOrder = data => {
return http2.post("/v1/user/procedure/update_container_snapshot", data, {
return http2.post("/acs/v1/user/procedure/update_container_snapshot", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -154,7 +154,7 @@ export const submitSnapshotOrder = data => {
};
// 获取购买虚拟机数据卷价格
export const getVolumePrice = data => {
return http2.post("/v1/user/procedure/get_price_volume", data, {
return http2.post("/acs/v1/user/procedure/get_price_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
@@ -163,7 +163,7 @@ export const getVolumePrice = data => {
// 提交虚拟机数据卷订单
export const submitVolumeOrder = data => {
return http2.post("/v1/user/procedure/update_container_volume", data, {
return http2.post("/acs/v1/user/procedure/update_container_volume", data, {
headers: {
"Content-Type": "multipart/form-data"
}
+160
View File
@@ -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)
}
+157
View File
@@ -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
}
+88
View File
@@ -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
View File
@@ -1,26 +1,104 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
// 基础URL
const baseUrl = 'https://apiservertest.s1f.ren'
// const baseUrl = 'https://cloudapi.007yjs.com'
import {getRefreshToken,refreshAccessToken} from "@/api/login.js";
import { baseUrl, acsBaseUrl, noAuthUrls as noAuthUrlList, requestTimeout, acsRequestTimeout, TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js'
// 检查URL是否需要认证
const urlNeedAuth = (url) => {
// 这里可以添加不需要认证的URL列表
const noAuthUrls = ['/v1/user/login', '/v1/user/check/get_code_img', '/v1/user/register']
return !noAuthUrls.some(noAuthUrl => url.includes(noAuthUrl))
return !noAuthUrlList.some(noAuthUrl => url.includes(noAuthUrl))
}
// 检查token是否过期
const isTokenExpired = () => {
const token = localStorage.getItem('token')
const token = localStorage.getItem(TOKEN_KEY)
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
if (!token) return true
// 这里可以添加token过期检查逻辑,如果有JWT可以解析它
// 简单实现,仅检查token是否存在
return false
// 检查过期时间
if (expire) {
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
const now = Date.now()
return now >= expireTime
}
// 没有过期时间时,默认认为Token已过期(因为无法验证有效性)
return true
}
// 检查token是否即将过期(5分钟内)
const isTokenExpiringSoon = () => {
const expire = localStorage.getItem(TOKEN_EXPIRE_KEY)
if (!expire) return false
const expireTime = parseInt(expire) * 1000 // 转换为毫秒
const now = Date.now()
const fiveMinutes = 5 * 60 * 1000 // 5分钟
// 如果已过期,返回false(由isTokenExpired处理)
if (now >= expireTime) return false
// 如果在5分钟内过期,返回true
return (expireTime - now) <= fiveMinutes
}
// 正在刷新token的标志
let isRefreshing = false
// 等待刷新token的请求队列
let refreshSubscribers = []
// 添加请求到队列
const subscribeTokenRefresh = (callback) => {
refreshSubscribers.push(callback)
}
// 刷新token后执行队列中的请求
const onTokenRefreshed = (newToken) => {
refreshSubscribers.forEach(callback => callback(newToken))
refreshSubscribers = []
}
// 执行token刷新
const doRefreshToken = async () => {
try {
const domain = window.location.hostname
// 获取交换token
const refreshTokenRes = await getRefreshToken(domain,{
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
})
if (refreshTokenRes.data?.code === 200 && refreshTokenRes.data?.data?.refresh_token) {
// 使用交换token获取新的access token
const newTokenRes = await refreshAccessToken(refreshTokenRes.data.data.refresh_token)
if (newTokenRes.data?.code === 200 && newTokenRes.data?.data?.token) {
const { token, expire } = newTokenRes.data.data
localStorage.setItem(TOKEN_KEY, token)
if (expire) {
localStorage.setItem(TOKEN_EXPIRE_KEY, expire.toString())
}
return token
}
}
// 刷新失败,触发登出逻辑
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(TOKEN_EXPIRE_KEY)
localStorage.removeItem(USER_INFO_KEY)
ElMessage.warning('登录过期,请重新登录')
router.push('/login')
return null
} catch (error) {
console.error('Token刷新失败:', error)
// 刷新失败,触发登出逻辑
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(TOKEN_EXPIRE_KEY)
localStorage.removeItem(USER_INFO_KEY)
ElMessage.warning('登录过期,请重新登录')
router.push('/login')
return null
}
}
class Request {
@@ -37,7 +115,7 @@ class Request {
(config) => {
// 在发送请求之前做些什么
// 例如:添加 token
const token = localStorage.getItem('token')
const token = localStorage.getItem(TOKEN_KEY)
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
@@ -106,33 +184,78 @@ class Request {
// 创建默认实例
const request = new Request({
baseURL: baseUrl,
timeout: 50000,
timeout: requestTimeout,
headers: {
'Content-Type': 'multipart/form-data'
}
})
export const mainUrl = baseUrl + "/acs"
export const mainUrl = baseUrl + '/acs'
export const baseURL = baseUrl
export const http2 = axios.create({
baseURL: baseUrl,
timeout: 30000,
timeout: acsRequestTimeout,
headers: {},
});
http2.interceptors.request.use(config => {
const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage
if(urlNeedAuth(config.url) && isTokenExpired()){
if (token){
localStorage.removeItem('token');
ElMessage.warning('登陆过期,请重新登陆')
http2.interceptors.request.use(async config => {
const token = localStorage.getItem(TOKEN_KEY)
// 检查是否需要认证
if (urlNeedAuth(config.url)) {
// 检查token是否已过期
if (isTokenExpired()) {
if (token) {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(TOKEN_EXPIRE_KEY)
localStorage.removeItem(USER_INFO_KEY)
ElMessage.warning('登录过期,请重新登录')
}
router.push('/login')
return Promise.reject(new Error('Token已过期'))
}
// 检查token是否即将过期,进行无感刷新
if (isTokenExpiringSoon() && !isRefreshing) {
isRefreshing = true
try {
const newToken = await doRefreshToken()
if (newToken) {
console.log('Token已无感刷新')
onTokenRefreshed(newToken)
config.headers.Authorization = `Bearer ${newToken}`
} else {
// 刷新失败,doRefreshToken已处理登出逻辑,直接拒绝请求
return Promise.reject(new Error('Token刷新失败'))
}
} catch (error) {
console.error('Token刷新异常:', error)
// 刷新异常,doRefreshToken已处理登出逻辑,直接拒绝请求
return Promise.reject(error)
} finally {
isRefreshing = false
}
} else if (isRefreshing) {
// 正在刷新,等待刷新完成
return new Promise((resolve, reject) => {
subscribeTokenRefresh((newToken) => {
if (newToken) {
config.headers.Authorization = `Bearer ${newToken}`
// 重新发送原始请求
resolve(config)
} else {
reject(new Error('Token刷新失败'))
}
})
})
} else {
// 正常情况,直接使用token
config.headers.Authorization = `Bearer ${token}`
}
router.push('/login')
return Promise.reject();
}
config.headers.Authorization = `Bearer ${token}`;
config.url = '/acs' + config.url
// 不需要认证的请求,不添加token
return config
})
@@ -146,7 +269,7 @@ http2.interceptors.response.use(
}
const { status } = error.response;
if (status === 401) {
localStorage.removeItem('token');
localStorage.removeItem(TOKEN_KEY);
ElMessage.warning('登陆过期,请重新登陆')
router.push('/login')
return Promise.reject();
+206 -1
View File
@@ -1,4 +1,209 @@
export const FileName = (data) =>{
let name = data.split("/").pop()
return name
}
}
export const formatTime = (time) => {
return new Date(time).toLocaleString()
}
export const formatDate = (dateStr) => {
if (!dateStr || dateStr === '0001-01-01T00:00:00Z' || dateStr === null) return '-'
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
/**
* 时间格式转 Unix 时间戳(秒级)
* @param {string|Date} time - 输入时间(支持 '2025-10-28 00:00:00'、'2025/10/28'、Date 对象等)
* @returns {number|null} 转换后的毫秒级时间戳(失败返回 null)
*/
export function timeToTimestamp(time) {
let date;
// 处理字符串格式(如 '2025-10-28 00:00:00' 或 '2025/10/28'
if (typeof time === 'string') {
// 替换 '-' 为 '/'(避免 Safari 等浏览器对 '-' 格式解析失败)
const formattedTime = time.replace(/-/g, '/');
date = new Date(formattedTime);
}
// 处理 Date 对象
else if (time instanceof Date) {
date = time;
}
// 无效输入
else {
console.error('无效的时间格式,支持字符串(如 "2025-10-28 00:00:00")或 Date 对象');
return null;
}
// 验证时间是否有效
const timestamp = date.getTime();
if (isNaN(timestamp)) {
console.error(`无法解析时间:${time}`);
return null;
}
return Math.floor(timestamp / 1000); // 返回秒级时间戳(如 1751107200000
}
export function reducenum(num){
return num / 100
}
/**
* 分转元显示(返回 ¥xx.xx 或 '-'
*/
export function formatPrice(fen, fallback = '-') {
if (!fen && fen !== 0) return fallback
return '¥' + (fen / 100).toFixed(2)
}
/**
* 元转分(四舍五入取整)
*/
export function yuanToFen(yuan) {
return Math.round((yuan || 0) * 100)
}
/**
* 格式化到期时间(year < 2000 视为永久)
*/
export function formatExpireTime(t) {
if (!t) return '-'
const d = new Date(t)
if (isNaN(d.getTime())) return '-'
if (d.getFullYear() < 2000) return '永久'
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
/**
* 将 ISO 格式时间字符串转换为毫秒级时间戳(用于时间选择器)
* @param {string|Date|number} time - 输入时间(支持 ISO 格式字符串如 '2023-11-08T01:10:00+08:00'、Date 对象、时间戳等)
* @returns {number|null} 转换后的毫秒级时间戳(失败或无效时间返回 null)
*/
export function isoToMilliseconds(time) {
// 处理空值
if (!time || time === null || time === undefined) {
return null
}
// 处理特殊的无效时间标识
if (typeof time === 'string' && (time === '0001-01-01T00:00:00Z' || time === '0001-01-01T00:00:00+00:00')) {
return null
}
// 如果已经是数字(时间戳),直接返回
if (typeof time === 'number') {
// 如果是秒级时间戳(小于 13 位),转换为毫秒
if (time < 1000000000000) {
return time * 1000
}
return time
}
// 处理 Date 对象
if (time instanceof Date) {
const timestamp = time.getTime()
return isNaN(timestamp) ? null : timestamp
}
// 处理字符串格式
if (typeof time === 'string') {
try {
const date = new Date(time)
const timestamp = date.getTime()
// 检查是否为有效时间
if (isNaN(timestamp)) {
return null
}
return timestamp
} catch (error) {
console.error('时间转换失败:', error)
return null
}
}
return null
}
/**
* 格式化时间为 "YYYY-MM-DD HH:mm:ss" 格式(用于接口提交)
* @param {string|Date|number} time
* @returns {string} 格式化后的时间字符串,无效时返回 ''
*/
export function formatToApiTime(time) {
if (!time) return ''
const d = time instanceof Date ? time : new Date(time)
if (isNaN(d.getTime())) return ''
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
// ========== 虚拟机状态映射 ==========
const VM_STATUS_MAP = {
pending: { label: '等待中', type: 'info' },
creating: { label: '创建中', type: 'warning' },
ready: { label: '就绪', type: 'success' },
running: { label: '运行中', type: 'success' },
stopped: { label: '已停止', type: 'danger' },
stop: { label: '已停止', type: 'danger' },
shutoff: { label: '已关闭', type: 'danger' },
error: { label: '错误', type: 'danger' },
paused: { label: '已暂停', type: 'warning' },
reboot: { label: '重启中', type: 'warning' },
poweroff: { label: '已关机', type: 'info' },
unknown: { label: '未知', type: 'info' }
}
/**
* 获取虚拟机状态标签文字
*/
export function vmStatusLabel(status) {
return VM_STATUS_MAP[status]?.label || status || '-'
}
/**
* 获取虚拟机状态 Tag 类型
*/
export function vmStatusType(status) {
return VM_STATUS_MAP[status]?.type || 'info'
}
// ========== 磁盘状态映射 ==========
const VOLUME_STATUS_MAP = {
pending: { label: '等待中', type: 'info' },
creating: { label: '创建中', type: 'warning' },
ready: { label: '就绪', type: 'success' },
in_use: { label: '使用中', type: 'success' },
attaching: { label: '挂载中', type: 'warning' },
detaching: { label: '卸载中', type: 'warning' },
resizing: { label: '扩容中', type: 'warning' },
deleting: { label: '删除中', type: 'danger' },
error: { label: '错误', type: 'danger' },
unknown: { label: '未知', type: 'info' }
}
/**
* 获取磁盘状态标签文字
*/
export function volumeStatusLabel(status) {
return VOLUME_STATUS_MAP[status]?.label || status || '-'
}
/**
* 获取磁盘状态 Tag 类型
*/
export function volumeStatusType(status) {
return VOLUME_STATUS_MAP[status]?.type || 'info'
}
+11 -2
View File
@@ -105,15 +105,24 @@ const forgetPassword = () => {
const handleLogin = () => {
loginFormRef.value?.validate(async valid =>{
window.localStorage.removeItem('token')
window.localStorage.removeItem('tokenExpire')
window.localStorage.removeItem('userInfo')
if (valid) {
loading.value = true
let resp = await userLogin(loginForm.username, loginForm.password)
console.log("login:",resp)
loading.value = false
if(resp.code === 200){
window.localStorage.setItem('token',resp.data.token)
// 保存token和过期时间
window.localStorage.setItem('token', resp.data.token)
if (resp.data.expire) {
window.localStorage.setItem('tokenExpire', resp.data.expire.toString())
}
let userInfo = await getUserInfo()
if(userInfo.data.is_admin){
// 保存用户信息到localStorage
window.localStorage.setItem('userInfo', JSON.stringify(userInfo.data))
await router.push('/dashboard')
} else {
ElMessage.warning('你不是管理员,不能登陆到后台控制面板')
+166 -179
View File
@@ -1,17 +1,5 @@
<template>
<div class="guacamole-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<div class="left">
<h2 class="title">远程桌面网关管理</h2>
<el-tag type="info" effect="plain" class="count-tag"> {{ guacamoleStats.total }} 个配置</el-tag>
</div>
<div class="actions">
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">添加配置</el-button>
<el-button @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-panel">
<div class="stat-card total-card">
@@ -37,79 +25,91 @@
</div>
</div>
<!-- 搜索和筛选 -->
<div class="filter-section">
<el-input
v-model="filterForm.url"
placeholder="搜索 Guacamole URL"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
class="search-input"
/>
<div class="filter-actions">
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
</div>
</div>
<!-- Guacamole 配置列表 -->
<div class="table-container">
<el-table
v-loading="loading"
:data="guacamoleData"
border
stripe
style="width: 100%"
table-layout="auto"
class="guacamole-table"
>
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
<el-table-column label="密码" width="120" align="center">
<template #default="scope">
<el-button
type="text"
size="small"
@click="togglePasswordVisibility(scope.row)"
:icon="scope.row.showPassword ? View : Hide"
>
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
<el-card class="main-container" shadow="never">
<!-- 搜索和筛选 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="filterForm" class="search-form">
<el-form-item>
<el-input
v-model="filterForm.url"
placeholder="搜索 Guacamole URL"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
style="width: 300px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetFilter">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>添加配置
</el-button>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180" align="center">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="编辑配置" placement="top" :hide-after="1500">
<el-button
type="warning"
:icon="Edit"
circle
@click="handleEdit(scope.row)"
/>
</el-tooltip>
<el-tooltip content="删除配置" placement="top" :hide-after="1500">
<el-button
type="danger"
:icon="Delete"
circle
@click="handleDelete(scope.row)"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<!-- Guacamole 配置列表 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="guacamoleData"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
<el-table-column label="密码" width="120" align="center">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="togglePasswordVisibility(scope.row)"
>
<el-icon style="margin-right: 4px"><component :is="scope.row.showPassword ? View : Hide" /></el-icon>
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180" align="center">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
type="primary"
link
@click="handleEdit(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -119,9 +119,10 @@
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</div>
</el-card>
<!-- 添加/编辑配置对话框 -->
<el-dialog
@@ -510,42 +511,7 @@ onMounted(async () => {
<style scoped>
.guacamole-container {
padding: 20px;
min-height: calc(100vh - 120px);
background-color: #f5f7fa;
}
/* 页面标题样式 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.count-tag {
font-size: 13px;
}
.page-header .actions {
display: flex;
gap: 12px;
align-items: center;
padding: 0;
}
/* 统计卡片 */
@@ -558,18 +524,18 @@ onMounted(async () => {
.stat-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px;
display: flex;
align-items: center;
transition: all 0.3s;
border: 1px solid #ebeef5;
border: 1px solid #e1e8ed;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.stat-icon {
@@ -608,60 +574,64 @@ onMounted(async () => {
font-weight: 600;
margin-bottom: 4px;
line-height: 1.1;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #606266;
color: #909399;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
/* 搜索和筛选部分 */
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
gap: 16px;
margin-bottom: 24px;
justify-content: space-between;
align-items: center;
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-input {
.search-form {
margin: 0;
flex: 1;
max-width: 400px;
}
.filter-actions {
display: flex;
gap: 8px;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
/* 表格容器 */
.table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 24px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.guacamole-table {
margin-bottom: 16px;
}
/* 操作按钮 */
.action-buttons {
.action-bar {
display: flex;
justify-content: center;
gap: 8px;
gap: 12px;
flex-shrink: 0;
}
/* 分页 */
.pagination-container {
display: flex;
justify-content: flex-end;
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
/* 表单样式 */
@@ -676,14 +646,6 @@ onMounted(async () => {
line-height: 1.2;
}
.form-section-title {
font-weight: 600;
margin: 16px 0 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
color: #409EFF;
}
/* 对话框底部 */
.dialog-footer {
display: flex;
@@ -696,6 +658,37 @@ onMounted(async () => {
gap: 8px;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* 响应式设计 */
@media screen and (max-width: 992px) {
.stats-panel {
@@ -708,17 +701,6 @@ onMounted(async () => {
}
@media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-header .actions {
width: 100%;
flex-wrap: wrap;
}
.stats-panel {
grid-template-columns: 1fr;
}
@@ -727,13 +709,18 @@ onMounted(async () => {
grid-column: auto;
}
.filter-section {
.filter-content {
flex-direction: column;
align-items: stretch;
}
.search-input {
max-width: none;
.search-form {
width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
}
}
</style>
+121 -175
View File
@@ -1,61 +1,59 @@
<template>
<div class="container-images-container" v-loading="loading">
<div class="page-header">
<h2>容器镜像</h2>
<div class="header-actions">
<el-button type="primary" @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
<!-- 服务器选择和搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="服务器">
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
<el-option
v-for="server in serverList"
:key="server.server_id"
:label="server.name"
:value="server.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 当前服务器镜像列表 -->
<div v-if="currentServer" class="server-section">
<div class="server-header">
<h3>{{ currentServer.name }}</h3>
<div class="server-actions">
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
<el-icon><plus /></el-icon>上传镜像
</el-button>
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
<el-icon><refresh /></el-icon>同步镜像
</el-button>
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="服务器">
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
<el-option
v-for="server in serverList"
:key="server.server_id"
:label="server.name"
:value="server.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<el-card class="table-card">
<!-- 当前服务器镜像列表 -->
<div v-if="currentServer" class="table-section">
<div class="server-header">
<h3>{{ currentServer.name }}</h3>
<div class="server-actions">
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
<el-icon><plus /></el-icon>上传镜像
</el-button>
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
<el-icon><refresh /></el-icon>同步镜像
</el-button>
</div>
</div>
<el-table
:data="currentMirrorList"
border
style="width: 100%"
row-key="image_id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="镜像信息" min-width="250">
@@ -65,7 +63,7 @@
<div class="image-info-content">
<div class="image-name-row">
<span class="table-image-name">{{ scope.row.name }}</span>
<el-tag>{{ scope.row.tag || '无标签' }}</el-tag>
<el-tag size="small">{{ scope.row.tag || '无标签' }}</el-tag>
</div>
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
</div>
@@ -104,17 +102,17 @@
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="total"
layout="prev, pager, next"
@current-change="handleCurrentPageChange"
/>
</div>
</el-card>
</div>
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="total"
layout="prev, pager, next"
@current-change="handleCurrentPageChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 镜像详情对话框 -->
<el-dialog
@@ -201,12 +199,6 @@
</el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="分类ID" prop="class_id">
<el-input v-model="form.class_id" placeholder="请输入分类ID" />
</el-form-item>
<el-form-item label="分类名称" prop="class_name">
<el-input v-model="form.class_name" placeholder="请输入分类名称" />
</el-form-item> -->
<el-form-item label="图标">
<div class="image-icon-upload">
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
@@ -674,7 +666,7 @@ const toLoad = async (data) => {
})
form.server_id = data
nowserver_id.value = data
let res = await getServerPlan(data)
let res = await getServerPlan({server_id:data,count:10})
planlist.value = res.data.data.map(item => {
return {
name: item.name,
@@ -756,7 +748,7 @@ const fetchCategoryList = async (serverId) => {
// 编辑镜像
const handleEdit = async (data) => {
try {
let res = await getServerPlan({server_id: data.server_id,count: 100})
let res = await getServerPlan({server_id: data.server_id,count: 10})
if (res.data && res.data.data) {
planlist.value = res.data.data.map(item => {
return {
@@ -882,7 +874,7 @@ const getit = async () => {
// 选择图片
const picPagin = reactive({
count: 50,
count: 10,
page: 1,
key: '',
user_type: 1
@@ -986,65 +978,65 @@ onMounted(() => {
<style scoped>
.container-images-container {
padding: 24px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #fff;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.page-header h2 {
margin: 0;
font-size: 22px;
color: #303133;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 12px;
}
.search-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
gap: 16px;
padding: 8px;
}
.server-section {
margin-bottom: 32px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
border-bottom: 1px solid #e1e8ed;
background: #fff;
padding: 16px 24px;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.server-header h3 {
margin: 0;
font-size: 18px;
font-size: 16px;
color: #303133;
font-weight: 600;
display: flex;
@@ -1055,7 +1047,7 @@ onMounted(() => {
content: '';
display: inline-block;
width: 4px;
height: 18px;
height: 16px;
background-color: #409EFF;
margin-right: 10px;
border-radius: 2px;
@@ -1066,12 +1058,6 @@ onMounted(() => {
gap: 12px;
}
.table-card {
margin-bottom: 24px;
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.image-info-cell {
display: flex;
align-items: center;
@@ -1118,11 +1104,12 @@ onMounted(() => {
-webkit-box-orient: vertical;
}
.pagination-container {
.pagination {
margin-top: 20px;
display: flex;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
padding: 0 16px 16px;
}
/* 详情对话框样式 */
@@ -1263,75 +1250,34 @@ onMounted(() => {
background-color: #ecf5ff;
}
/* 对话框样式优化 */
:deep(.el-dialog__header) {
border-bottom: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-dialog__body) {
padding: 24px;
}
:deep(.el-dialog__footer) {
border-top: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table__row:hover) {
background-color: #ecf5ff !important;
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-button--link) {
padding: 4px 8px;
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-button--link):hover {
background-color: #f0f2f5;
border-radius: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.server-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.server-actions {
width: 100%;
}
.image-info-cell {
flex-direction: column;
align-items: flex-start;
}
.image-info-content {
margin-left: 0;
margin-top: 12px;
width: 100%;
}
.table-image-logo {
width: 60px;
height: 60px;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
+147 -173
View File
@@ -1,107 +1,105 @@
<template>
<div class="image-categories-container" v-loading="loading">
<!-- 页面标题 -->
<div class="page-header">
<h2>镜像分类管理</h2>
<p class="page-description">管理不同服务器下的镜像分类</p>
</div>
<!-- 操作栏 -->
<el-card class="search-card">
<el-form :inline="true" class="search-form">
<el-form-item label="服务器">
<el-select
v-model="selectedServer"
placeholder="请选择服务器"
clearable
@change="handleServerChange"
style="width: 220px"
>
<el-option
v-for="item in serverList"
:key="item.server_id"
:label="item.name"
:value="item.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="分类名称">
<el-input
v-model="searchKey"
placeholder="搜索分类名称"
clearable
@input="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleAddCategory"
:disabled="!selectedServer"
>
<el-icon><plus /></el-icon>添加分类
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="filteredCategoryList"
style="width: 100%"
border
stripe
highlight-current-row
>
<el-table-column type="index" width="60" align="center" label="序号" />
<el-table-column prop="name" label="分类名称" min-width="150" />
<el-table-column label="分类图标" align="center" width="100">
<template #default="scope">
<el-avatar
v-if="scope.row.class_ico"
:size="40"
:src="scope.row.class_ico"
fit="cover"
/>
<el-icon v-else :size="20"><picture /></el-icon>
</template>
</el-table-column>
<el-table-column label="所属服务器" min-width="150">
<template #default="scope">
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="180">
<template #default="scope">
{{ scope.row.created_at }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" class="search-form">
<el-form-item label="服务器">
<el-select
v-model="selectedServer"
placeholder="请选择服务器"
clearable
@change="handleServerChange"
style="width: 220px"
>
<el-option
v-for="item in serverList"
:key="item.server_id"
:label="item.name"
:value="item.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="分类名称">
<el-input
v-model="searchKey"
placeholder="搜索分类名称"
clearable
@input="handleSearch"
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button
type="success"
link
@click="handleEditCategory(scope.row)"
type="primary"
@click="handleAddCategory"
:disabled="!selectedServer"
>
<el-icon><edit /></el-icon>编辑
<el-icon><plus /></el-icon>添加分类
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-container">
<!-- 表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="filteredCategoryList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" width="60" align="center" label="序号" />
<el-table-column prop="name" label="分类名称" min-width="150" />
<el-table-column label="分类图标" align="center" width="100">
<template #default="scope">
<el-avatar
v-if="scope.row.class_ico"
:size="40"
:src="scope.row.class_ico"
fit="cover"
style="background-color: #f5f7fa; border: 1px solid #ebeef5;"
/>
<el-icon v-else :size="20" color="#909399"><picture /></el-icon>
</template>
</el-table-column>
<el-table-column label="所属服务器" min-width="150">
<template #default="scope">
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="handleEditCategory(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<el-pagination
background
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
/>
</div>
</el-card>
@@ -264,7 +262,7 @@ const categoryRules = {
//
const picSwitch = ref(false)
const picPagin = reactive({
count: 50,
count: 10,
page: 1,
key: '',
user_type: 1
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
<style scoped>
.image-categories-container {
padding: 24px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
flex-direction: column;
margin-bottom: 24px;
background: #fff;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.page-header h2 {
margin: 0;
font-size: 22px;
color: #303133;
font-weight: 600;
}
.page-description {
margin-top: 8px;
color: #6b7280;
font-size: 14px;
}
.search-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
gap: 16px;
padding: 8px;
}
.table-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.pagination-container {
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
margin-top: 16px;
padding: 0 16px 16px;
}
/* 图片上传区域样式 */
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
background-color: #ecf5ff;
}
/* 对话框样式优化 */
:deep(.el-dialog__header) {
border-bottom: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-dialog__body) {
padding: 24px;
}
:deep(.el-dialog__footer) {
border-top: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table__row:hover) {
background-color: #ecf5ff !important;
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-button--link) {
padding: 4px 8px;
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-button--link):hover {
background-color: #f0f2f5;
border-radius: 4px;
:deep(.el-card__body) {
padding: 0;
}
/* 响应式调整 */
@media (max-width: 768px) {
.image-icon-upload {
flex-direction: column;
align-items: flex-start;
}
.upload-buttons {
margin-top: 12px;
width: 100%;
}
}
</style>
</style>
+825
View File
@@ -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>
+187 -122
View File
@@ -1,103 +1,103 @@
<template>
<div class="image-requests-container">
<div class="page-header">
<h2>申请镜像</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>申请镜像
</el-button>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="镜像类型">
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable style="width: 150px">
<el-option label="Docker镜像" value="docker" />
<el-option label="Windows镜像" value="windows" />
</el-select>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
<el-option label="已通过" value="approved" />
<el-option label="审核中" value="pending" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>申请镜像
</el-button>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
</div>
<!-- 提示信息 -->
<el-alert
type="info"
show-icon
:closable="false"
class="info-alert"
>
<el-icon><info-filled /></el-icon>
申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
</el-alert>
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
</el-form-item>
<el-form-item label="镜像类型">
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable>
<el-option label="Docker镜像" value="docker" />
<el-option label="Windows镜像" value="windows" />
</el-select>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="已通过" value="approved" />
<el-option label="审核中" value="pending" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
<!-- 提示信息 -->
<el-alert
type="info"
show-icon
:closable="false"
class="info-alert"
style="margin: 20px 20px 0; width: auto;"
>
<el-table-column prop="id" label="申请ID" width="150" align="center" />
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看详情
</el-button>
<el-button
v-if="scope.row.status === 'rejected'"
type="primary"
link
@click="handleResubmit(scope.row)"
>
<el-icon><refresh /></el-icon>重新提交
</el-button>
</template>
</el-table-column>
</el-table>
<template #title>
申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
</template>
</el-alert>
<!-- 分页 -->
<div class="pagination-container">
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="申请ID" width="150" align="center" />
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看详情
</el-button>
<el-button
v-if="scope.row.status === 'rejected'"
type="primary"
link
@click="handleResubmit(scope.row)"
>
<el-icon><refresh /></el-icon>重新提交
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -106,6 +106,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -589,41 +591,58 @@ onMounted(() => {
<style scoped>
.image-requests-container {
padding: 20px;
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-actions {
display: flex;
gap: 10px;
}
.info-alert {
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
@@ -637,12 +656,16 @@ onMounted(() => {
/* 环境变量配置样式 */
.env-vars-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.env-vars-header {
display: flex;
margin-bottom: 10px;
font-weight: bold;
font-weight: 600;
color: #606266;
}
.env-vars-item {
@@ -665,17 +688,23 @@ onMounted(() => {
.add-env-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 端口配置样式 */
.ports-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.ports-header {
display: flex;
margin-bottom: 10px;
font-weight: bold;
font-weight: 600;
color: #606266;
}
.ports-item {
@@ -702,6 +731,8 @@ onMounted(() => {
.add-port-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 详情样式 */
@@ -710,11 +741,13 @@ onMounted(() => {
}
.request-reason {
background-color: #f8f8f8;
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
color: #606266;
line-height: 1.6;
}
.review-comment {
@@ -724,5 +757,37 @@ onMounted(() => {
margin-top: 10px;
white-space: pre-wrap;
border-left: 4px solid #67c23a;
color: #606266;
}
</style>
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
File diff suppressed because it is too large Load Diff
+161 -108
View File
@@ -1,99 +1,93 @@
<template>
<div class="announcements-container">
<div class="page-header">
<h2>官方公告</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布公告
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="公告标题">
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
<el-option label="已发布" value="published" />
<el-option label="草稿" value="draft" />
<el-option label="已下线" value="offline" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布公告
</el-button>
</div>
</div>
</div>
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="公告标题">
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable />
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="已发布" value="published" />
<el-option label="草稿" value="draft" />
<el-option label="已下线" value="offline" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="publisher" label="发布人" width="120" />
<el-table-column prop="publishTime" label="发布时间" width="180" />
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="warning"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status !== 'offline'"
>下线</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'offline'"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="primary"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status !== 'offline'"
>
<el-icon><turn-off /></el-icon>下线
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'offline'"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -102,6 +96,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -365,32 +361,58 @@ onMounted(() => {
<style scoped>
.announcements-container {
padding: 20px;
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
@@ -420,4 +442,35 @@ onMounted(() => {
line-height: 1.8;
color: #606266;
}
</style>
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
+142 -92
View File
@@ -1,58 +1,61 @@
<template>
<div class="news-container">
<div class="page-header">
<h2>新闻咨询</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布新闻
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="新闻标题">
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="新闻分类">
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable style="width: 150px">
<el-option label="产品动态" value="product" />
<el-option label="技术干货" value="technology" />
<el-option label="行业资讯" value="industry" />
<el-option label="活动公告" value="activity" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布新闻
</el-button>
</div>
</div>
</div>
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="新闻标题">
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable />
</el-form-item>
<el-form-item label="新闻分类">
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable>
<el-option label="产品动态" value="product" />
<el-option label="技术干货" value="technology" />
<el-option label="行业资讯" value="industry" />
<el-option label="活动公告" value="activity" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 新闻列表卡片 -->
<div v-loading="loading" class="news-list">
<el-card class="table-card">
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="newsData"
border
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
@@ -68,33 +71,27 @@
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">
<el-icon><delete /></el-icon>删除
</el-button>
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 新闻详情对话框 -->
<el-dialog
@@ -400,36 +397,58 @@ onMounted(() => {
<style scoped>
.news-container {
padding: 20px;
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.news-list {
margin-bottom: 20px;
}
.table-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
@@ -487,4 +506,35 @@ onMounted(() => {
margin-right: 5px;
color: #E6A23C;
}
</style>
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
+170 -117
View File
@@ -1,108 +1,102 @@
<template>
<div class="policies-container">
<div class="page-header">
<h2>官方政策</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布政策
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="政策标题">
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="政策类型">
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable style="width: 150px">
<el-option label="服务条款" value="terms" />
<el-option label="定价政策" value="pricing" />
<el-option label="隐私政策" value="privacy" />
<el-option label="合规政策" value="compliance" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布政策
</el-button>
</div>
</div>
</div>
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="政策标题">
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable />
</el-form-item>
<el-form-item label="政策类型">
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable>
<el-option label="服务条款" value="terms" />
<el-option label="定价政策" value="pricing" />
<el-option label="隐私政策" value="privacy" />
<el-option label="合规政策" value="compliance" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="type" label="政策类型" width="120" align="center">
<template #default="scope">
<el-tag :type="getPolicyTypeTag(scope.row.type)">
{{ getPolicyTypeText(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="warning"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status === 'active'"
>下线</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'inactive'"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="type" label="政策类型" width="120" align="center">
<template #default="scope">
<el-tag :type="getPolicyTypeTag(scope.row.type)">
{{ getPolicyTypeText(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="primary"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status === 'active'"
>
<el-icon><turn-off /></el-icon>下线
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'inactive'"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -111,6 +105,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -435,32 +431,58 @@ onMounted(() => {
<style scoped>
.policies-container {
padding: 20px;
padding: 0;
}
.page-header {
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
@@ -495,4 +517,35 @@ onMounted(() => {
line-height: 1.8;
color: #606266;
}
</style>
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
File diff suppressed because it is too large Load Diff
+847
View File
@@ -0,0 +1,847 @@
<template>
<div class="server-form-container">
<!-- 顶部导航 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" class="back-btn" circle>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<div class="header-title-area">
<h1 class="page-title">{{ isEdit ? '编辑服务器' : '新建服务器' }}</h1>
<span class="page-subtitle">{{ isEdit ? '修改服务器配置信息' : '配置并部署新的服务器节点' }}</span>
</div>
</div>
<div class="header-actions">
<el-button @click="goBack" size="large">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
{{ isEdit ? '保存修改' : '立即创建' }}
</el-button>
</div>
</div>
<!-- 主表单区域 -->
<div class="form-wrapper">
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
<!-- 左侧主要配置 -->
<div class="form-main-col">
<el-card class="premium-card" shadow="never">
<div class="section-header">
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
<div class="section-info">
<h3>基础信息</h3>
<p>配置服务器的基本标识与网络信息</p>
</div>
</div>
<div class="form-grid-2">
<el-form-item label="服务器名称" prop="name">
<el-input v-model="form.name" placeholder="例如:生产环境-Web节点-01" />
</el-form-item>
<el-form-item label="IP地址" prop="server_ip">
<el-input v-model="form.server_ip" placeholder="例如:192.168.1.100" />
</el-form-item>
</div>
<el-form-item label="所在地区" prop="location">
<el-cascader
v-model="locationArray"
:options="regionsBuff"
:props="optionProps"
placeholder="选择服务器所在的物理位置"
style="width: 100%"
clearable
/>
</el-form-item>
<el-divider />
<div class="section-header">
<div class="section-icon"><el-icon><Cpu /></el-icon></div>
<div class="section-info">
<h3>硬件规格</h3>
<p>定义服务器的计算资源配额</p>
</div>
</div>
<div class="resource-cards">
<div class="resource-item">
<div class="resource-label">CPU核心</div>
<el-input v-model="form.cpu" placeholder="0">
<template #suffix></template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">内存容量</div>
<el-input v-model="form.memory" placeholder="0">
<template #suffix>MB</template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">硬盘空间</div>
<el-input v-model="form.disk" placeholder="0">
<template #suffix>GB</template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">网络带宽</div>
<el-input v-model="form.bandwidth" placeholder="0">
<template #suffix>Mbps</template>
</el-input>
</div>
</div>
</el-card>
<el-card class="premium-card" shadow="never" style="margin-top: 24px;">
<div class="section-header">
<div class="section-icon"><el-icon><Connection /></el-icon></div>
<div class="section-info">
<h3>连接与认证</h3>
<p>配置服务器的访问方式与凭证</p>
</div>
</div>
<el-form-item label="服务器类型" prop="server_type" style="margin-bottom: 24px;">
<div class="type-selector">
<div
class="type-card"
:class="{ active: form.server_type === 'dockerContainer' }"
@click="form.server_type = 'dockerContainer'"
>
<div class="type-icon"><el-icon><Box /></el-icon></div>
<div class="type-info">
<div class="type-name">容器云服务器</div>
<div class="type-desc">基于Docker容器技术的轻量级实例</div>
</div>
<div class="type-check" v-if="form.server_type === 'dockerContainer'">
<el-icon><Check /></el-icon>
</div>
</div>
<div
class="type-card"
:class="{ active: form.server_type === 'hyperV' }"
@click="form.server_type = 'hyperV'"
>
<div class="type-icon"><el-icon><Platform /></el-icon></div>
<div class="type-info">
<div class="type-name">虚拟机云服务器</div>
<div class="type-desc">基于Hyper-V技术的完整虚拟化实例</div>
</div>
<div class="type-check" v-if="form.server_type === 'hyperV'">
<el-icon><Check /></el-icon>
</div>
</div>
</div>
</el-form-item>
<!-- 容器云特有 -->
<template v-if="form.server_type === 'dockerContainer'">
<el-form-item label="Auth-ID" prop="auth_id">
<el-input v-model="form.auth_id" placeholder="输入服务器管理ID" />
</el-form-item>
</template>
<!-- 虚拟机特有 -->
<template v-if="form.server_type === 'hyperV'">
<el-form-item label="Guacamole网关" prop="guacamole_id">
<el-select
v-model="form.guacamole_id"
placeholder="选择Guacamole连接配置"
filterable
clearable
:loading="guacamoleLoading"
@change="handleGuacamoleChange"
style="width: 100%"
>
<el-option
v-for="item in guacamoleList"
:key="item.id"
:label="item.url"
:value="item.id"
>
<div class="guacamole-option">
<span class="url">{{ item.url }}</span>
<span class="user">{{ item.username }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<div class="form-grid-2">
<el-form-item label="登录用户名" prop="username">
<el-input v-model="form.username" placeholder="例如:Administrator" />
</el-form-item>
<el-form-item label="登录密码" prop="password">
<el-input
v-model="form.password"
placeholder="输入登录密码"
type="password"
show-password
/>
</el-form-item>
</div>
<el-form-item>
<div class="feature-switch">
<div class="switch-info">
<span class="switch-title">端口映射</span>
<span class="switch-desc">允许外部网络访问该服务器的特定端口</span>
</div>
<el-switch
v-model="form.allow_port_forward"
:active-value="1"
:inactive-value="0"
/>
</div>
</el-form-item>
</template>
<el-form-item label="管理Token" prop="server_token">
<el-input
v-model="form.server_token"
placeholder="节点服务器管理员Token"
type="password"
show-password
/>
</el-form-item>
</el-card>
</div>
<!-- 右侧高级设置 -->
<div class="form-side-col">
<el-card class="premium-card" shadow="never">
<div class="section-header small">
<div class="section-info">
<h3>高级设置</h3>
</div>
</div>
<el-form-item label="控制台连接">
<el-input v-model="form.console_url" placeholder="可选,https需反代">
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="展示卡片HTML">
<el-input
v-model="form.html"
type="textarea"
:rows="6"
placeholder="自定义购买页面的展示样式代码"
resize="none"
/>
</el-form-item>
<el-divider />
<el-form-item>
<div class="feature-switch">
<div class="switch-info">
<span class="switch-title">购物车显示</span>
<span class="switch-desc">在前端购买页面展示此节点</span>
</div>
<el-switch
v-model="form.hide"
:active-value="0"
:inactive-value="1"
/>
</div>
</el-form-item>
</el-card>
</div>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElNotification } from 'element-plus'
import {
ArrowLeft, Monitor, Cpu, Link, Connection,
Box, Platform, Check
} from '@element-plus/icons-vue'
import { addServer, editServer, getServer } from '@/utils/acs/server'
import { getGuacamoleList } from '@/utils/acs/guacamole'
import regions from '@/utils/regions.json'
const route = useRoute()
const router = useRouter()
const formRef = ref(null)
const submitting = ref(false)
const isEdit = computed(() => !!route.query.id)
const form = reactive({
server_id: '',
name: '',
server_ip: '',
location: '',
bandwidth: '',
disk: '',
memory: '',
cpu: '',
state: '',
auth_id: '',
server_token: '',
server_type: 'dockerContainer',
html: '',
hide: 0,
console_url: '',
guacamole_id: '',
username: '',
password: '',
allow_port_forward: 0
})
const rules = {
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
server_ip: [
{ required: true, message: '请输入IP地址', trigger: 'blur' },
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
],
guacamole_id: [
{ required: false, message: '请输入Guacamole服务ID', trigger: 'blur' }
],
username: [
{ required: false, message: '请输入登录用户名', trigger: 'blur' }
],
password: [
{ required: false, message: '请输入登录密码', trigger: 'blur' }
]
}
// Guacamole
const guacamoleList = ref([])
const guacamoleLoading = ref(false)
const fetchGuacamoleList = async () => {
if (guacamoleLoading.value) return
guacamoleLoading.value = true
try {
const res = await getGuacamoleList()
if (res && res.data && res.data.code === 200) {
guacamoleList.value = res.data.data || []
} else {
guacamoleList.value = []
}
} catch (error) {
console.error('获取Guacamole列表失败:', error)
guacamoleList.value = []
} finally {
guacamoleLoading.value = false
}
}
const handleGuacamoleChange = (selectedId) => {
if (!selectedId) {
form.username = ''
form.password = ''
return
}
}
//
const regionsBuff = ref(regions)
const optionProps = {
label: 'label',
value: 'value',
children: 'children',
checkStrictly: false,
emitPath: true
}
const findValueByLabel = (label, options) => {
for (const option of options) {
if (option.label === label) return option.value
if (option.children) {
const result = findValueByLabel(label, option.children)
if (result) return result
}
}
return undefined
}
const findLabelByValue = (value, options) => {
for (const option of options) {
if (option.value === value) return option.label
if (option.children) {
const result = findLabelByValue(value, option.children)
if (result) return result
}
}
return undefined
}
const locationArray = computed({
get: () => {
if (form.location) {
try {
const labels = form.location.split(' ')
const values = labels.map(label => findValueByLabel(label, regionsBuff.value))
return values.filter(value => value !== undefined)
} catch (error) {
return []
}
}
return []
},
set: (newArray) => {
try {
if (Array.isArray(newArray) && newArray.length > 0) {
const labels = newArray.map(value => {
const label = findLabelByValue(value, regionsBuff.value)
return label || value
})
form.location = labels.join(' ')
} else {
form.location = ''
}
} catch (error) {
form.location = ''
}
}
})
//
const initData = async () => {
if (isEdit.value) {
const id = route.query.id
if (id) {
try {
const stateData = history.state.params
if (stateData) {
Object.keys(form).forEach(key => {
if (key in stateData) {
form[key] = stateData[key]
}
})
} else {
const res = await getServer(1, 100, '', route.query.type || 'dockerContainer')
if (res && res.data && res.data.data) {
const found = res.data.data.find(item => item.server_id == id)
if (found) {
Object.keys(form).forEach(key => {
if (key in found) {
form[key] = found[key]
}
})
}
}
}
} catch (e) {
console.error(e)
}
}
} else {
form.server_type = route.query.type || 'dockerContainer'
}
if (form.server_type === 'hyperV') {
fetchGuacamoleList()
}
}
const goBack = () => {
router.back()
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
try {
const formData = { ...form }
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward']
numericFields.forEach(field => {
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
formData[field] = Number(formData[field])
}
})
let res
if (!isEdit.value) {
res = await addServer(formData)
} else {
res = await editServer(formData)
}
if (res && res.data && res.data.code === 200) {
ElNotification({
title: !isEdit.value ? '添加成功' : '更新成功',
message: `服务器"${formData.name}"已${!isEdit.value ? '添加' : '更新'}成功`,
type: 'success',
duration: 3000
})
goBack()
} else {
ElMessage.error(res?.data?.msg || '操作失败')
}
} catch (error) {
console.error('提交表单失败:', error)
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
})
}
watch(() => form.server_type, (val) => {
if (val === 'hyperV') {
fetchGuacamoleList()
}
})
onMounted(() => {
initData()
})
</script>
<style scoped>
.server-form-container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
/* 顶部导航 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #ffffff;
padding: 20px 32px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
border: none;
background: #f2f3f5;
color: #606266;
width: 36px;
height: 36px;
transition: all 0.3s;
}
.back-btn:hover {
background-color: #e6e8eb;
color: #303133;
}
.header-title-area {
display: flex;
flex-direction: column;
justify-content: center;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.2;
}
.page-subtitle {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.submit-btn {
padding: 10px 24px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
/* 表单布局 */
.form-wrapper {
display: flex;
gap: 24px;
align-items: flex-start;
}
.main-form {
display: flex;
width: 100%;
gap: 24px;
}
.form-main-col {
flex: 1;
min-width: 0;
}
.form-side-col {
width: 320px;
flex-shrink: 0;
}
/* 卡片样式 */
.premium-card {
border: 1px solid #e4e7ed;
border-radius: 12px;
background: #ffffff;
transition: all 0.3s;
}
.premium-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.premium-card :deep(.el-card__body) {
padding: 32px;
}
/* 章节标题 */
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f2f5;
}
.section-header.small {
margin-bottom: 20px;
padding-bottom: 12px;
}
.section-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
color: #409EFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.section-info h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: #303133;
}
.section-info p {
margin: 2px 0 0 0;
font-size: 12px;
color: #909399;
}
/* 表单网格 */
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* 资源卡片 */
.resource-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.resource-item {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #ebeef5;
transition: all 0.3s;
}
.resource-item:hover {
border-color: #c6e2ff;
background: #f2f6fc;
}
.resource-label {
font-size: 12px;
color: #606266;
margin-bottom: 8px;
font-weight: 500;
}
/* 类型选择器 */
.type-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.type-card {
position: relative;
border: 1px solid #dcdfe6;
border-radius: 8px;
padding: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
background: #fff;
}
.type-card:hover {
border-color: #409EFF;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.type-card.active {
border-color: #409EFF;
background: #ecf5ff;
}
.type-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: #f2f6fc;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #606266;
}
.type-card.active .type-icon {
background: #fff;
color: #409EFF;
}
.type-info {
flex: 1;
}
.type-name {
font-weight: 600;
color: #303133;
font-size: 14px;
margin-bottom: 2px;
}
.type-desc {
font-size: 12px;
color: #909399;
}
.type-check {
position: absolute;
top: 8px;
right: 8px;
color: #409EFF;
font-size: 16px;
}
/* 开关样式 */
.feature-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #ebeef5;
}
.switch-info {
display: flex;
flex-direction: column;
}
.switch-title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.switch-desc {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.guacamole-option {
display: flex;
justify-content: space-between;
width: 100%;
}
.guacamole-option .url {
font-weight: 500;
}
.guacamole-option .user {
color: #909399;
font-size: 12px;
}
/* 响应式 */
@media screen and (max-width: 992px) {
.main-form {
flex-direction: column;
}
.form-side-col {
width: 100%;
}
.resource-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 768px) {
.server-form-container {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
.header-actions {
width: 100%;
display: flex;
gap: 12px;
}
.header-actions .el-button {
flex: 1;
}
.form-grid-2 {
grid-template-columns: 1fr;
}
.type-selector {
grid-template-columns: 1fr;
}
}
</style>
+37 -10
View File
@@ -15,6 +15,7 @@
<div class="actions">
<el-button @click="goBack" :icon="Back">返回</el-button>
<el-button type="primary" @click="refreshData" :icon="Refresh">刷新数据</el-button>
<el-button type="info" @click="clearAllCache" plain>清除缓存</el-button>
</div>
</div>
@@ -99,7 +100,7 @@
<div class="info-content">
<div class="info-item">
<div class="info-label">链接端口号</div>
<div class="info-value">{{ vmInfo.link_port || '10006' }}</div>
<div class="info-value">{{ vmInfo.node_port || (portsList.length > 0 ? portsList[0].node_port : '10006') }}</div>
</div>
<div class="info-item">
<div class="info-label">虚拟机用户名</div>
@@ -466,7 +467,7 @@
<h3 class="tab-title">数据卷列表</h3>
<el-button
type="primary"
@click="showAddVolumeDialog = true"
@click="handleAddVolume"
:icon="Plus"
:disabled="vmInfo.state != 2"
>
@@ -670,8 +671,11 @@
width="500px"
>
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
<el-form-item label="大小(GB)" prop="size">
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
<el-form-item label="大小" prop="size">
<div class="unit-input-row">
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -692,8 +696,11 @@
>
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
<el-form-item label="大小(GB)" prop="size">
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" />
<el-form-item label="大小" prop="size">
<div class="unit-input-row">
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -825,7 +832,7 @@ const setCachedData = (instanceId, data) => {
console.log(`缓存虚拟机数据: ${instanceId}`, cacheData);
};
const isCacheValid = (cachedData, maxAge = 5 * 60 * 1000) => { // 5
const isCacheValid = (cachedData, maxAge = 10 * 60 * 1000) => { // 10
if (!cachedData || !cachedData.timestamp) return false;
return (Date.now() - cachedData.timestamp) < maxAge;
};
@@ -1066,6 +1073,7 @@ const showMigrateVolumeDialog = ref(false);
const currentVolumeToEdit = ref(null);
const volumeForm = reactive({
size: 10,
_sizeUnit: 'GB'
});
const volumeFormRef = ref(null);
const volumeRules = {
@@ -1379,12 +1387,17 @@ const fetchVmInfo = async (instanceId = null, useCache = true) => {
const serverRes = await selectServer({server_id:res.data.data.server_id})
const planRes = await selectServerPlan({plan_id:res.data.data.plan_id,server_type:"hyperV"})
const imageRes= await Mirrorinfo({image_id:res.data.data.image_id,server_type:"hyperV"})
//
const firstPort = portsList.value.length > 0 ? portsList.value[0].node_port : null;
const data = {
...res.data.data,
server_name:serverRes.data.data.name,
server_ip:serverRes.data.data.server_ip,
plan_name:planRes.data.data.name,
image_name:imageRes.data.data.name
image_name:imageRes.data.data.name,
node_port: res.data.data.node_port || firstPort // 使API
}
if (res && res.data && res.data.code === 200) {
vmInfo.value = data || {};
@@ -1554,6 +1567,13 @@ const refreshData = () => {
loadAllData(instanceId, false);
};
//
const clearAllCache = () => {
console.log('清除所有虚拟机缓存');
dataCache.value.clear();
ElMessage.success('已清除所有缓存');
};
//
const handleStart = async () => {
try {
@@ -2358,6 +2378,7 @@ const handleAddVolume = () => {
showAddVolumeDialog.value = true;
//
volumeForm.size = 10;
volumeForm._sizeUnit = 'GB';
};
//
@@ -2365,6 +2386,7 @@ const handleEditVolume = (volume) => {
currentVolumeToEdit.value = volume;
//
volumeForm.size = volume.size;
volumeForm._sizeUnit = 'GB';
showEditVolumeDialog.value = true;
};
@@ -2391,9 +2413,10 @@ const submitAddVolume = async () => {
if (valid) {
addingVolume.value = true;
try {
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
const res = await addVolume({
instance_id: route.query.instance_id,
size: String(volumeForm.size),
size: String(sizeGb),
user_id: user_id.value
});
console.log("添加数据卷112",res)
@@ -2425,9 +2448,10 @@ const submitEditVolume = async () => {
editingVolume.value = true;
try {
// API
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
const res = await updateVolume({
volume_id: currentVolumeToEdit.value.id,
size: volumeForm.size
size: sizeGb
});
console.log("编辑数据卷数据:",res)
@@ -2757,4 +2781,7 @@ const fetchServersList = async () => {
font-weight: 600;
color: #303133;
}
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.unit-select { width: 90px; flex-shrink: 0; }
</style>
+4 -4
View File
@@ -223,9 +223,9 @@
:value="mirror.id"
>
<span style="float: left">{{ mirror.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
{{ mirror.size }}MB
</span>
<!-- <span style="float: right; color: #8492a6; font-size: 13px">-->
<!-- {{ mirror.size }}MB-->
<!-- </span>-->
</el-option>
</el-select>
</el-form-item>
@@ -618,7 +618,7 @@ const fetchPlanList = async () => {
try {
const response = await getServerPlan({
server_id: props.ID,
count: 100
count: 10
});
if (response && response.data && response.data.code === 200) {
+5 -1
View File
@@ -315,7 +315,11 @@
class="data-table"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="size" label="空间大小(MB)" width="140" />
<el-table-column prop="size" label="空间大小(MB)" width="140">
<template #default="{ row }">
{{ row.size != null && row.size !== '' ? `${row.size} MB` : '-' }}
</template>
</el-table-column>
<el-table-column prop="mount_path" label="挂载路径" min-width="200" />
<el-table-column prop="created_at" label="创建时间" min-width="160" />
</el-table>

Some files were not shown because too many files have changed in this diff Show More