Compare commits

...
218 Commits
Author SHA1 Message Date
zhongzm c0b794afeb feat:验收问题修复 2025-07-03 10:12:18 +08:00
zhongzm 0ac1ec9ca1 feat:告警日期默认当月 2025-06-20 17:37:05 +08:00
zhongzm e4b00931c1 feat:澳元符号修改 2025-06-20 17:33:21 +08:00
zhongzm 842be01104 Merge remote-tracking branch 'origin/main' 2025-06-20 15:34:02 +08:00
zhongzm 75e03f93ac feat:白名单提交增加成功提示 2025-06-20 15:33:51 +08:00
caiyuchao 89e696abc8 feat: 补充版权系统名 2025-06-19 15:40:52 +08:00
zhongzm d137f49ca0 feat:终端设备免认证状态添加 2025-06-19 15:28:06 +08:00
zhongzm 5da91e48c8 feat:告警删除bug修复 2025-06-19 15:16:01 +08:00
zhongzm de5fc010f7 feat:澳币配置添加 2025-06-19 14:02:43 +08:00
zhongzm 5f22ef60a9 feat:IP格式校验 2025-06-19 13:56:32 +08:00
zhongzm de7445cd99 Merge remote-tracking branch 'origin/main' 2025-06-19 12:27:33 +08:00
zhongzm abda1ebe54 feat:白名单和告警界面中英适配 2025-06-19 12:27:23 +08:00
caiyuchao c96e307060 feat: 统一修改系统名 2025-06-18 17:58:15 +08:00
zhongzm e8c98e77d3 feat:隐藏nonstick 2025-06-17 18:52:26 +08:00
zhongzm d0cca3bbfb feat:配置参数问题修复,中英文适配 2025-06-12 17:25:30 +08:00
zhongzm c9b4e430e9 feat:隐藏网关、交换机、OLTs项,增加告警跳转 2025-06-11 11:14:31 +08:00
zhongzm bc89546ec6 feat:组网配置 2025-06-10 19:10:31 +08:00
zhongzm 83a8fb9a98 feat:白名单界面 2025-06-10 19:10:05 +08:00
zhongzm c518ebd921 feat:告警界面 2025-06-10 19:09:39 +08:00
zhongzm ed3e583f71 feat:kyc界面状态标签颜色修改 2025-05-07 17:43:09 +08:00
zhongzm 6a694d479a feat:kyc界面状态信息错误修复 2025-05-07 17:34:44 +08:00
zhongzm c56ac9ec62 feat:错误信息判断修改 2025-05-07 16:45:36 +08:00
zhongzm d894349479 feat:修改密码提示信息修复 2025-04-29 10:51:09 +08:00
caiyuchao 324ed9d114 feat: 修改密码静态路由目录调整 2025-04-28 23:50:09 +08:00
zhongzm 07ebbf45b1 feat:wlan中英文修复 2025-04-28 18:23:58 +08:00
zhongzm d8f283da3f Merge remote-tracking branch 'origin/main' 2025-04-28 16:48:58 +08:00
zhongzm c23a76069d feat:默认选择paypal和stripe 2025-04-28 16:48:49 +08:00
caiyuchao 87d2e1adeb feat: 菜单管理和日志管理super权限控制 2025-04-28 10:37:31 +08:00
caiyuchao dcd0c706fa B
Merge branch 'main' of http://192.168.2.166:3180/WFC/fe.wfc
2025-04-28 10:33:56 +08:00
caiyuchao b6fed795b2 feat: 屏蔽菜单和修改密码接口参数 2025-04-28 10:33:32 +08:00
zhongzm 84e23675ab Merge remote-tracking branch 'origin/main' 2025-04-27 19:19:15 +08:00
zhongzm 4422a2bab2 feat:多货币以及支付方式配置 2025-04-27 19:19:03 +08:00
caiyuchao db72e6cec0 feat: 用户中心路由调整 2025-04-21 16:41:10 +08:00
zhongzm 59d54185f5 feat:wlan增加密码配置以及band限制提示 2025-04-15 18:52:40 +08:00
zhongzm b27aebed73 feat:站点列表功能拓展、绑定邮箱、修改密码 2025-04-01 15:21:08 +08:00
zhongzm 42d10809e7 fix:登录界面增加中英文切换 2025-03-20 11:45:19 +08:00
caiyuchao 9690b9e1f8 feat: 更换和统一logo 2025-03-19 15:01:18 +08:00
caiyuchao 8ce4c19962 feat: 国际化语言默认为英语 2025-03-19 11:06:06 +08:00
zhongzm 2508613b35 fix:wlan界面增加字段 2025-03-13 17:55:57 +08:00
zhongzm 21969b9a33 fix:按钮样式调整 2025-03-12 16:21:06 +08:00
zhongzm 04c076272a fix:套餐金额不显示默认金额 2025-03-12 15:10:48 +08:00
zhongzm a342cfc64e fix:仪表盘跳转(AP、注册用户、装置) 2025-03-11 18:36:29 +08:00
zhongzm 946ae91d99 fix:限制价格最低0.01元 2025-03-11 16:19:38 +08:00
zhongzm ccccfd347c fix:限制价格最低0.01元 2025-03-11 16:15:55 +08:00
lai 23949528a7 增加菜单悬浮 2025-02-27 18:33:50 +08:00
zhongzm 11e1e4bcea fix:添加portal参数配置 2025-02-27 16:12:55 +08:00
lai 21f372c9b5 修改年龄计算 2025-02-27 14:52:34 +08:00
TsMask 69b28f3a66 style: 使用默认布局设置 2025-02-27 11:56:17 +08:00
zhongzm c17b16ac33 Merge remote-tracking branch 'origin/main' 2025-02-26 20:56:31 +08:00
zhongzm b0aa5ec475 fix:删除多余的每日限额参数 2025-02-26 20:56:22 +08:00
lai 0ff91fb085 样式挤兑 2025-02-26 20:50:23 +08:00
zhongzm 9550d9e3ce fix:仪表盘分页器中英文 2025-02-26 20:02:16 +08:00
zhongzm 172357487c Merge remote-tracking branch 'origin/main' 2025-02-26 18:43:21 +08:00
zhongzm 8189b8152f fix:ap设备移除功能中英文补充 2025-02-26 18:41:16 +08:00
lai 5a2f8ecb17 登入页忘记密码 2025-02-26 18:30:44 +08:00
lai f6a7ca1883 中英文 2025-02-26 18:30:21 +08:00
zhongzm 1c3bff4f6f fix:性别统一 2025-02-26 16:58:06 +08:00
zhongzm 8a15e933ac fix:限速限制1到1024的整数 2025-02-26 16:22:10 +08:00
zhongzm 292a7c0ab0 Merge remote-tracking branch 'origin/main' 2025-02-26 14:32:21 +08:00
zhongzm 089ce77974 fix:套餐界面错误信息修复 2025-02-26 14:32:13 +08:00
TsMask c38b67ff57 fix: 布局horizontal-mix选择目录跳转首个子菜单 2025-02-26 14:25:47 +08:00
TsMask 6ce34dd63c fix: 多语言重复key/图标引用缺失 2025-02-26 14:24:26 +08:00
zhongzm 1457c3d39d fix:限速界面错误信息修复 2025-02-26 12:13:45 +08:00
zhongzm de9e6fea12 fix:仪表盘路由中英文 2025-02-26 12:02:40 +08:00
zhongzm 39a43ab75d fix:计费规则界面错误提示修复 2025-02-26 12:00:14 +08:00
zhongzm 654362b2d9 fix:kyc界面错误提示修复 2025-02-26 11:51:14 +08:00
zhongzm 9c17330555 fix:cdrRecord界面错误提示修复 2025-02-26 11:41:47 +08:00
zhongzm d586e54c51 fix:用户信息界面错误提示修复 2025-02-26 11:34:40 +08:00
zhongzm eed336cb00 fix:Ap设备界面model信息隐藏 2025-02-26 11:24:37 +08:00
zhongzm 5c3734945f fix:Ap设备界面设备重启 2025-02-26 11:19:47 +08:00
zhongzm 2110a60a6b fix:终端设备界面错误提示修复 2025-02-26 10:28:55 +08:00
zhongzm 1b3b2d7497 fix:仪表盘错误提示修复 2025-02-26 10:19:14 +08:00
zhongzm 59f77adcb3 fix:AP设备错误提示修复 2025-02-25 17:01:33 +08:00
zhongzm 7432c02ddf fix:清除登录界面默认账号密码 2025-02-25 14:54:24 +08:00
zhongzm 8b6e4b27e6 fix:ap设备界面状态修改和wlan中英修复 2025-02-25 14:50:37 +08:00
zhangsz c707a338c6 fix: en ts 2025-02-24 20:18:09 +08:00
zhangsz 425c4cf38d fix: ssid 2025-02-24 20:06:23 +08:00
zhongzm 10e21ea516 fix:portal门户界面 2025-02-24 19:45:38 +08:00
caiyuchao ec83482e99 fix: 邮箱url 2025-02-21 18:13:50 +08:00
zhangsz 02c0ae6ff7 fix: ... 2025-02-21 16:48:04 +08:00
zhangsz 1f55660ed8 fix: sn field en/cn message 2025-02-21 16:40:42 +08:00
zhongzm be3c39dbe5 Merge remote-tracking branch 'origin/main' 2025-02-21 16:32:53 +08:00
zhongzm fa866cff38 fix:WLAN界面按钮风格统一 2025-02-21 16:32:44 +08:00
zhangsz 345e6f7177 fix: cn update 2025-02-21 16:22:30 +08:00
zhongzm d9dacf63fa fix:WLAN界面 2025-02-21 15:49:25 +08:00
lai 051b5bda56 去除记住我 2025-02-21 15:04:06 +08:00
lai 12472f5a2e 特殊符号引发编译器报错 2025-02-21 14:52:25 +08:00
zhongzm b4f48789df fix:ap设备界面功能增加 2025-02-20 16:58:57 +08:00
zhongzm d8c665b8ae fix:ap设备界面功能增加 2025-02-20 15:56:58 +08:00
zhongzm 4ef332f5e9 fix:ap设备界面功能增加 2025-02-20 15:20:25 +08:00
caiyuchao 298d5ef363 fix: 修复用户信息查询出生日期 2025-02-19 16:35:35 +08:00
zhangsz 4987e76843 fix: some EN message 2025-02-19 16:11:16 +08:00
lai 5561d924ab 修改重置密码界面 2025-02-19 14:22:27 +08:00
zhongzm 8729c2d1f7 fix:空白页标签增加 2025-02-19 11:44:36 +08:00
lai 0864a040d9 新增重置密码以及密码复杂度 2025-02-19 11:34:24 +08:00
zhongzm 41fbdafc54 fix:界面文件预创建 2025-02-18 19:20:19 +08:00
zhangsz 2ac813ec8f fix: en update 2025-02-17 18:45:30 +08:00
zhangsz fd1a6edc85 fix: i18n change 2025-02-17 18:12:01 +08:00
zhongzm 631fd1d5ad fix:账号管理删除dept字段 2025-02-17 11:40:36 +08:00
zhongzm f9ecdfcb71 Merge remote-tracking branch 'origin/main' 2025-02-15 11:36:44 +08:00
zhongzm 3b08706ca9 fix:AP设备界面publicIp字段改ip 2025-02-15 11:36:36 +08:00
lai e3032bb850 修复BUG 2025-02-14 19:21:51 +08:00
zhongzm 28ab9dceeb fix:仪表盘站点搜索功能 2025-02-14 17:42:46 +08:00
zhongzm e6a5d91b5e fix:任务管理界面搜索栏重置功能修复 2025-02-14 17:28:27 +08:00
zhongzm f13dcac7db fix:账号管理界面搜索栏重置功能修复 2025-02-14 17:03:46 +08:00
zhongzm 35eed2393e fix:日志管理界面搜索栏重置功能修复 2025-02-14 15:36:45 +08:00
zhongzm 4f35b17b03 fix:话单管理界面搜索栏重置功能修复 2025-02-14 14:48:46 +08:00
zhongzm 410b6c40cb fix:用户信息界面搜索栏重置功能修复 2025-02-14 14:35:34 +08:00
zhongzm bf149afbbb fix:终端设备界面搜索栏重置功能修复 2025-02-14 11:59:42 +08:00
zhongzm f587e4f673 fix:AP设备界面搜索栏重置功能修复 2025-02-14 11:48:47 +08:00
zhongzm c1ad6b4e9d fix:中英显示修复 2025-02-13 21:03:19 +08:00
zhongzm 56748228bf Merge remote-tracking branch 'origin/main' 2025-02-13 20:25:17 +08:00
zhongzm 37989c71ec fix:cdr添加结束时间范围搜索 2025-02-13 20:25:06 +08:00
lai fd0d541fdf 修复BUG 2025-02-13 17:53:31 +08:00
zhongzm 7f60eafc6d fix:用户信息界面中英修复 2025-02-13 15:33:28 +08:00
lai a428b973df key修改 2025-02-13 14:12:34 +08:00
zhongzm 6532f2a776 fix:修复kyc图片请求问题 2025-02-12 17:11:46 +08:00
zhongzm acf802e6c0 fix:修复kyc图片请求问题 2025-02-12 16:07:28 +08:00
zhongzm 02255d5be3 fix:话单管理界面添加排序 2025-02-12 15:25:28 +08:00
zhongzm 72343da432 fix:kyc界面图片溢出容器问题修复 2025-02-12 11:32:00 +08:00
zhongzm b6961c9cfd fix:终端界面状态中英修复 2025-02-11 18:24:24 +08:00
zhongzm eb883385e8 fix:所有界面按钮风格统一 2025-02-11 18:12:13 +08:00
zhongzm 32aa82a599 fix:角色管理中英修复 2025-02-11 17:42:21 +08:00
zhongzm d94c35ed49 fix:动态菜单中英适配(刷新界面实现) 2025-02-11 17:04:02 +08:00
zhongzm e74db62c27 fix:增加流量最大1024限制 2025-02-11 16:39:24 +08:00
zhongzm 06dcb2f36b fix:修复套餐管理和格式化工具的中英适配 2025-02-11 16:32:13 +08:00
zhongzm ada516208f fix:修复账号管理中英适配 2025-02-11 16:10:58 +08:00
zhongzm a2e1194d44 fix:菜单管理界面中英适配 2025-02-11 16:07:02 +08:00
zhongzm ba535e442f fix:字典管理界面中英适配 2025-02-10 15:21:11 +08:00
zhongzm dd390a8a0f fix:套餐管理界面中英适配 2025-02-08 19:24:23 +08:00
zhongzm 66b705ab17 fix:限速设置界面中英适配 2025-02-08 18:36:18 +08:00
zhongzm 196ff4bcdc fix:计费规则界面中英适配 2025-02-08 18:03:00 +08:00
zhongzm 1c2fa63615 fix:kyc审核界面中英适配 2025-02-08 17:12:02 +08:00
zhongzm f2e03e70ea fix:话单管理界面中英适配 2025-02-08 16:26:41 +08:00
zhongzm 13c910e2e5 fix:终端设备mac字段修改 2025-02-08 16:06:04 +08:00
zhongzm 7d12ae5011 Merge remote-tracking branch 'origin/main' 2025-02-08 15:56:16 +08:00
zhongzm 8546cfc615 fix:用户信息界面中英适配 2025-02-08 15:56:08 +08:00
zhangsz 2775348585 updae .gitignore 2025-02-08 14:35:38 +08:00
zhangsz 5387933de7 fix: remove the temp build file components.d.ts 2025-02-08 14:30:54 +08:00
zhongzm 5eeaa824ff fix:终端界面中英适配以及字段修改 2025-02-07 19:24:01 +08:00
zhongzm 5b4f4b4ae1 fix:AP设备界面中英适配 2025-02-07 18:30:15 +08:00
zhongzm a974a042b0 fix:补充上一个提交 2025-02-07 15:03:37 +08:00
zhongzm 8171c2b975 fix:账单管理添加时间以及中英适配 2025-02-07 15:03:19 +08:00
zhongzm a8f3e247c6 fix:仪表盘中英文适配 2025-02-07 09:52:10 +08:00
zhongzm 489d4c8603 fix:token修复 2025-01-23 21:05:02 +08:00
caiyuchao 219584525f fix: 中英文切换 2025-01-23 19:37:29 +08:00
zhongzm e6aa12bcac fix:仪表盘Alert和client显示修复 2025-01-21 19:50:28 +08:00
zhongzm 080f6b9964 fix:仪表盘显示修复 2025-01-21 19:42:35 +08:00
zhongzm 7fbaf0d67f fix:流量限制仅为整数输入 2025-01-21 16:15:38 +08:00
zhongzm 37fee941d2 fix:修复计费单位显示 2025-01-21 15:02:40 +08:00
zhongzm 06629f6bb2 fix:kyc筛选条件修改字段 2025-01-20 16:17:05 +08:00
zhongzm 77a40d12e6 fix:首页删除多余显示 2025-01-17 19:25:43 +08:00
zhongzm a72217a5ea fix:kyc界面图片处理和搜索条件修改 2025-01-17 19:22:49 +08:00
zhongzm d07189e936 fix:速率换算修改 2025-01-17 17:31:58 +08:00
zhongzm ed721374fe fix:速率修复 2025-01-17 17:17:30 +08:00
zhongzm e6cd74498b fix:速率修复 2025-01-17 16:43:15 +08:00
zhongzm 0b20cec9bd fix:终端设备界面信息增加字段修改 2025-01-16 20:45:33 +08:00
lai a9a6fd9b88 完善日志管理 2025-01-16 14:23:10 +08:00
lai c0c912f08b 修正字典数据 2025-01-16 14:22:47 +08:00
zhongzm 5fad65960c fix:速率换算修改 2025-01-15 18:09:07 +08:00
zhongzm 134a417eb1 fix:首页仪表盘 2025-01-14 14:09:27 +08:00
zhongzm 28200f4931 fix:仪表盘界面(未接通) 2025-01-10 14:28:59 +08:00
lai 4708df2a6f 首页置换成仪表盘 2025-01-09 14:38:34 +08:00
caiyuchao 48a69c8b4b fix:修复登录菜单不刷新问题 2025-01-08 19:24:09 +08:00
caiyuchao 8e69a7ed7e fix: 修复退出登录问题 2025-01-08 19:22:21 +08:00
zhongzm 4c709fdb95 fix:终端设备界面修改 2025-01-08 17:57:00 +08:00
zhongzm 462dd40e92 fix:AP设备界面修改 2025-01-08 17:49:20 +08:00
caiyuchao 168e4bf1b8 fix: 修复修改角色的菜单权限bug 2025-01-08 15:34:07 +08:00
zhongzm b9026be4b1 fix:接口修复 2025-01-08 10:41:23 +08:00
zhongzm a2a92ed4da fix:套餐界面按钮隐藏 2025-01-07 18:54:40 +08:00
zhongzm 0977880d05 feat:终端设备界面以及接通 2025-01-07 18:19:01 +08:00
zhongzm bbc4f8669b feat:AP设备界面接通 2025-01-07 18:18:12 +08:00
zhongzm fd49c64c76 feat:计费规则界面接通 2025-01-07 17:39:07 +08:00
zhongzm 6dee934e9a fix:隐藏添加 2025-01-03 18:11:13 +08:00
zhongzm 8b2edce4e1 feat:账单管理界面 2025-01-03 18:01:37 +08:00
zhongzm 61c62f48b0 feat:cdr界面 2025-01-03 18:01:22 +08:00
lai 1267648ebc 后续更改为动态路由 2024-12-30 16:49:04 +08:00
lai 4b292b8a74 后续与后端协商增加标签颜色字段 2024-12-30 16:46:53 +08:00
lai 3e2d8faf5e 增加字典数据界面 2024-12-30 16:46:12 +08:00
lai 0a1f0d94d8 取消勾选后重置 2024-12-30 16:44:40 +08:00
zhongzm 6860c035c6 feat:修改获取套餐信息接口 2024-12-27 18:33:23 +08:00
caiyuchao fc40451b50 fix: 修改请求超时时间为30秒 2024-12-27 17:38:51 +08:00
lai 0f94a5b80d 修复分页器失效 2024-12-26 19:48:03 +08:00
lai 3e2bc5ab9d 完善 2024-12-26 16:14:06 +08:00
lai 52ad7dbe05 完善任务日志 2024-12-26 16:09:14 +08:00
lai b503b85472 完善日志管理 2024-12-26 16:07:31 +08:00
lai f65cb5058e 完善任务管理 2024-12-26 16:07:04 +08:00
lai de19d9f400 二进制内容直接返回内容 2024-12-26 16:05:59 +08:00
lai 8ac4d3f9df 增加导出插件 2024-12-26 16:05:34 +08:00
lai 1000f591ef 按钮权限加上super 2024-12-26 16:04:42 +08:00
zhongzm 59c80cd7a7 feat:套餐管理传参KB改B 2024-12-25 19:55:37 +08:00
zhongzm 3bb0b9d85c feat:工具类更新 2024-12-25 18:49:31 +08:00
zhongzm caa97e7783 feat:套餐界面参数修改 2024-12-25 18:49:16 +08:00
zhongzm 4ff8bcd475 feat:带宽配置限速判断修改 2024-12-25 11:30:38 +08:00
zhongzm 2707dc3643 feat:套餐界面带宽限速判断修改 2024-12-25 11:30:20 +08:00
zhongzm 19fd64daa0 feat:带宽限速界面(已联调) 2024-12-24 16:52:17 +08:00
zhongzm 756f42a92f feat:套餐管理界面(已联调) 2024-12-24 16:51:41 +08:00
zhongzm f98fdf7e27 feat:格式转换工具类 2024-12-24 16:50:41 +08:00
zhongzm 0939032e9b fix:用户管理用户信息界面 2024-12-18 17:30:52 +08:00
zhongzm 609edf74bc fix:用户账单管理界面 2024-12-18 17:30:30 +08:00
zhongzm c581ad8718 fix:历史账单界面 2024-12-18 17:29:50 +08:00
simonzhangsz a8df414a2b update components ts 2024-12-14 13:36:15 +08:00
zhongzm 5118479630 fix:修复报错 2024-12-13 19:29:26 +08:00
zhongzm 42fad1ca96 Merge remote-tracking branch 'origin/main' 2024-12-13 19:20:44 +08:00
zhongzm 7649d22630 feat:用户信息界面以及话单管理界面 2024-12-13 19:20:32 +08:00
lai e0d991da15 日志管理+任务管理 2024-12-13 19:14:32 +08:00
TsMask 57e33781d1 fix: 默认布局设置 2024-12-04 17:48:39 +08:00
TsMask 55d50ef8a5 fix: 设置默认语言为en-US 2024-12-04 17:35:17 +08:00
simonzhangsz d2f25695d8 hide fe menu 2024-12-04 11:07:18 +08:00
simonzhangsz b2b32fb3d4 update package name 2024-12-04 10:07:03 +08:00
TsMask 0d25f1fa0e perf: 合并用户平台框架修改 2024-12-03 18:14:32 +08:00
TsMask c6b0c60392 perf: 合并用户平台框架修改 2024-12-03 16:46:39 +08:00
lai 13ee5a3849 调整接口 2024-12-03 10:29:50 +08:00
lai 1627feb9a4 Merge remote-tracking branch 'origin/wfc-modules-user' 2024-11-29 18:32:00 +08:00
TsMask 212cf9d4c0 feat: 修改生产API标识 2024-11-28 11:11:43 +08:00
TsMask b82f71b763 fix: 登录接口参数变更 2024-11-27 15:37:03 +08:00
caiyuchao 074f9282dd fix: fix login page 2024-11-26 10:07:56 +08:00
caiyuchao a1dbcd1f00 feat:add i18n header 2024-11-25 19:24:01 +08:00
144 changed files with 16987 additions and 1636 deletions

No files matched your search

+1 -3
View File
@@ -1,6 +1,6 @@
VITE_BASE_URL=/
VITE_APP_TITLE="WANFi Platform"
VITE_APP_TITLE="Platform"
# the prefix of the icon name
VITE_ICON_PREFIX=icon
@@ -35,5 +35,3 @@ VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
# when the route mode is static, the defined super role
VITE_STATIC_SUPER_ROLE=R_SUPER
+2 -2
View File
@@ -2,10 +2,10 @@
VITE_ROUTER_HISTORY_MODE=hash
# frontend service base url, prod environment
VITE_BASE_URL=/u
VITE_BASE_URL=/sys
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=/u-api
VITE_SERVICE_BASE_URL=/sys-api
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
+1
View File
@@ -34,3 +34,4 @@ yarn.lock
.VSCodeCounter
**/.vitepress/cache
src/typings/components.d.ts
+11
View File
@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
}
+1 -1
View File
@@ -1,4 +1,4 @@
# WANFI 用户平台
# WANFI 客户平台
## 使用
+2
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/langs/zh-cn.js"></script>
<script src="/langs/en-us.js"></script>
<title>%VITE_APP_TITLE%</title>
</head>
<body>
+6 -2
View File
@@ -1,5 +1,5 @@
{
"name": "vue-antd",
"name": "wanfi-system",
"type": "module",
"version": "1.0.0",
"scripts": {
@@ -12,6 +12,7 @@
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@sa/axios": "workspace:*",
@@ -23,16 +24,19 @@
"clipboard": "2.0.11",
"dayjs": "1.11.11",
"echarts": "5.5.0",
"file-saver": "^2.0.5",
"lodash-es": "4.17.21",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"vue": "3.4.27",
"vue-draggable-plus": "0.5.0",
"vue-i18n": "9.13.1",
"vue-router": "4.3.2"
"vue-router": "4.3.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@elegant-router/vue": "0.3.7",
"@types/file-saver": "^2.0.7",
"@iconify/json": "2.2.217",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.3.6",
+1 -3
View File
@@ -48,7 +48,7 @@ function createCommonRequest<ResponseData = any>(
instance.interceptors.response.use(
async response => {
if (opts.isBackendSuccess(response)) {
if (opts.isBackendSuccess(response) || response.config?.responseType === "blob") {
return Promise.resolve(response);
}
@@ -117,7 +117,6 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
return opts.transformBackendResponse(response);
}
@@ -154,7 +153,6 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
const data = opts.transformBackendResponse(response);
+1 -1
View File
@@ -29,7 +29,7 @@ export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
}
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
const TEN_SECONDS = 10 * 1000;
const TEN_SECONDS = 30 * 1000;
const axiosConfig: CreateAxiosDefaults = {
timeout: TEN_SECONDS,
+7 -9
View File
@@ -59,17 +59,16 @@ export type TableConfig<A extends ApiFn, T, C> = {
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
export default function useHookTable(config: any) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
const searchParams: any = reactive({ ...apiParams });
const allColumns = ref(config.columns()) as Ref<any>;
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const data: Ref<any> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
@@ -82,7 +81,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
columnChecks.value = defaultChecks.map((col:any) => ({
...col,
checked: checkMap.get(col.key) ?? col.checked
}));
@@ -92,10 +91,9 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
const transformed = transformer(response as Awaited<any>);
data.value = transformed.rows;
@@ -123,7 +121,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
function updateSearchParams(params: any) {
Object.assign(searchParams, params);
}
+91
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.4.27(typescript@5.4.5))
'@better-scroll/core':
specifier: 2.5.1
version: 2.5.1
@@ -41,6 +44,9 @@ importers:
echarts:
specifier: 5.5.0
version: 5.5.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
lodash-es:
specifier: 4.17.21
version: 4.17.21
@@ -62,6 +68,9 @@ importers:
vue-router:
specifier: 4.3.2
version: 4.3.2(vue@3.4.27(typescript@5.4.5))
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies:
'@elegant-router/vue':
specifier: 0.3.7
@@ -75,6 +84,9 @@ importers:
'@soybeanjs/eslint-config':
specifier: 1.3.6
version: 1.3.6(@types/eslint@8.56.10)(@unocss/eslint-config@0.60.4(eslint@9.4.0)(typescript@5.4.5))(eslint-plugin-vue@9.26.0(eslint@9.4.0))(eslint@9.4.0)(typescript@5.4.5)(vue-eslint-parser@9.4.3(eslint@9.4.0))
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
'@types/lodash-es':
specifier: 4.17.12
version: 4.17.12
@@ -910,6 +922,9 @@ packages:
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1251,6 +1266,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1462,6 +1481,10 @@ packages:
caniuse-lite@1.0.30001629:
resolution: {integrity: sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chai@4.4.1:
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
engines: {node: '>=4'}
@@ -1523,6 +1546,10 @@ packages:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collection-visit@1.0.0:
resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
engines: {node: '>=0.10.0'}
@@ -1601,6 +1628,11 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@@ -2121,6 +2153,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
fill-range@4.0.0:
resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
engines: {node: '>=0.10.0'}
@@ -2172,6 +2207,10 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'}
@@ -3533,6 +3572,10 @@ packages:
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
engines: {node: '>=0.10.0'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stable@0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
@@ -4060,10 +4103,18 @@ packages:
engines: {node: '>=8'}
hasBin: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -4079,6 +4130,11 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
@@ -4734,6 +4790,8 @@ snapshots:
'@types/estree@1.0.5': {}
'@types/file-saver@2.0.7': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
@@ -5230,6 +5288,8 @@ snapshots:
acorn@8.11.3: {}
adler-32@1.3.1: {}
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -5478,6 +5538,11 @@ snapshots:
caniuse-lite@1.0.30001629: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chai@4.4.1:
dependencies:
assertion-error: 1.1.0
@@ -5563,6 +5628,8 @@ snapshots:
clone@2.1.2: {}
codepage@1.15.0: {}
collection-visit@1.0.0:
dependencies:
map-visit: 1.0.0
@@ -5623,6 +5690,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crc-32@1.2.2: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@@ -6370,6 +6439,8 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-saver@2.0.5: {}
fill-range@4.0.0:
dependencies:
extend-shallow: 2.0.1
@@ -6423,6 +6494,8 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
frac@1.1.2: {}
fragment-cache@0.2.1:
dependencies:
map-cache: 0.2.2
@@ -7839,6 +7912,10 @@ snapshots:
dependencies:
extend-shallow: 3.0.2
ssf@0.11.2:
dependencies:
frac: 1.1.2
stable@0.1.8: {}
stackback@0.0.2: {}
@@ -8445,8 +8522,12 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wmf@1.0.2: {}
word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -8467,6 +8548,16 @@ snapshots:
wrappy@1.0.2: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-name-validator@4.0.0: {}
y18n@5.0.8: {}
+10 -1
View File
@@ -1 +1,10 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="#646cff"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="#646cff"/></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742355837219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2674" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M512 794.666667m-58.666667 0a58.666667 58.666667 0 1 0 117.333334 0 58.666667 58.666667 0 1 0-117.333334 0Z" p-id="2675" fill="#646cff" >
</path>
<path d="M512 170.666667A622.56 622.56 0 0 0 46.24 379.413333a5.333333 5.333333 0 0 0 0.533333 7.626667l48.266667 42.346667a5.333333 5.333333 0 0 0 7.466667-0.48 548.533333 548.533333 0 0 1 818.986666 0 5.333333 5.333333 0 0 0 7.466667 0.48l48-42.346667a5.333333 5.333333 0 0 0 0.48-7.626667A622.186667 622.186667 0 0 0 512 170.666667z" p-id="2676" fill="#646cff" >
</path>
<path d="M512 371.946667A421.6 421.6 0 0 0 197.76 512a5.333333 5.333333 0 0 0 0.48 7.68l48 42.24a5.333333 5.333333 0 0 0 7.466667-0.48 347.2 347.2 0 0 1 516.16 0 5.333333 5.333333 0 0 0 7.466666 0.48l48-42.24a5.333333 5.333333 0 0 0 0.48-7.68A421.6 421.6 0 0 0 512 371.946667z" p-id="2677" fill="#646cff" >
</path>
<path d="M512 555.733333a238.293333 238.293333 0 0 0-176 77.44 5.333333 5.333333 0 0 0 0.48 7.68l48 42.186667a5.333333 5.333333 0 0 0 7.466667-0.426667 163.893333 163.893333 0 0 1 240 0 5.333333 5.333333 0 0 0 7.466666 0.426667l48-42.186667a5.333333 5.333333 0 0 0 0.48-7.68A238.293333 238.293333 0 0 0 512 555.733333z" p-id="2678" fill="#646cff" >
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="#646cff"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="#646cff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+5
View File
@@ -0,0 +1,5 @@
const enUS = {
system: {
title: 'MacroHub'
}
};
+5
View File
@@ -0,0 +1,5 @@
const zhCN = {
system: {
title: 'MacroHub'
}
};
+1
View File
@@ -13,6 +13,7 @@ const appStore = useAppStore();
const themeStore = useThemeStore();
const antdLocale = computed(() => {
appStore.changeLocale(appStore.locale);
return antdLocales[appStore.locale];
});
</script>
+10 -1
View File
@@ -1 +1,10 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="currentColor"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="currentColor"/></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742355837219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2674" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M512 794.666667m-58.666667 0a58.666667 58.666667 0 1 0 117.333334 0 58.666667 58.666667 0 1 0-117.333334 0Z" p-id="2675" fill="currentColor" >
</path>
<path d="M512 170.666667A622.56 622.56 0 0 0 46.24 379.413333a5.333333 5.333333 0 0 0 0.533333 7.626667l48.266667 42.346667a5.333333 5.333333 0 0 0 7.466667-0.48 548.533333 548.533333 0 0 1 818.986666 0 5.333333 5.333333 0 0 0 7.466667 0.48l48-42.346667a5.333333 5.333333 0 0 0 0.48-7.626667A622.186667 622.186667 0 0 0 512 170.666667z" p-id="2676" fill="currentColor" >
</path>
<path d="M512 371.946667A421.6 421.6 0 0 0 197.76 512a5.333333 5.333333 0 0 0 0.48 7.68l48 42.24a5.333333 5.333333 0 0 0 7.466667-0.48 347.2 347.2 0 0 1 516.16 0 5.333333 5.333333 0 0 0 7.466666 0.48l48-42.24a5.333333 5.333333 0 0 0 0.48-7.68A421.6 421.6 0 0 0 512 371.946667z" p-id="2677" fill="currentColor" >
</path>
<path d="M512 555.733333a238.293333 238.293333 0 0 0-176 77.44 5.333333 5.333333 0 0 0 0.48 7.68l48 42.186667a5.333333 5.333333 0 0 0 7.466667-0.426667 163.893333 163.893333 0 0 1 240 0 5.333333 5.333333 0 0 0 7.466666 0.426667l48-42.186667a5.333333 5.333333 0 0 0 0.48-7.68A238.293333 238.293333 0 0 0 512 555.733333z" p-id="2678" fill="currentColor" >
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="currentColor"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+150
View File
@@ -0,0 +1,150 @@
<script setup lang="ts">
import { reactive, watch, onBeforeMount } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: String,
default: '*',
},
});
/**指定列表初始数据 */
const optionsSpecific = Array.from({ length: 31 }, (_, i) => {
let idx = i + 1;
return {
label: `${idx}`,
value: `${idx}`,
};
});
/**数据 */
let data = reactive({
type: '1',
/**间隔 */
increment: 2,
incrementStart: 1,
/**周期 */
rangeStart: 1,
rangeEnd: 2,
/**指定 */
specific: ['1'],
});
/**监听数据,将数值格式化 */
watch(data, () => {
let reultValue = '*';
let val = data.type;
// 每一
if (val === '1') {
reultValue = '*';
}
// 间隔
if (val === '2') {
let start = data.incrementStart;
let increment = data.increment;
reultValue = `${start ?? 0}/${increment ?? 0}`;
}
// 周期
if (val === '3') {
let start = data.rangeStart;
let end = data.rangeEnd;
reultValue = `${start ?? 0}-${end ?? 0}`;
}
// 指定
if (val === '4') {
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
}
emit('update:value', reultValue);
});
/**挂载前初始属性 */
onBeforeMount(() => {
const val = props.value;
if (val === '*') {
data.type = '1';
}
if (val.includes('/')) {
const arr = val.split('/');
data.incrementStart = Number(arr[0]);
data.increment = Number(arr[1]);
data.type = '2';
}
if (val.includes('-')) {
const arr = val.split('-');
data.rangeStart = Number(arr[0]);
data.rangeEnd = Number(arr[1]);
data.type = '3';
}
if (val.includes(',')) {
data.specific = val.split(',').sort((a, b) => +a - +b);
data.type = '4';
}
});
</script>
<template>
<a-radio-group size="small" v-model:value="data.type">
<a-space direction="vertical" :size="18">
<a-radio value="1">
{{ t('components.CronModal.day1') }}
</a-radio>
<a-radio value="2">
{{ t('components.CronModal.day21') }}
<a-input-number
size="small"
v-model:value="data.increment"
:min="1"
:max="31"
placeholder="1-31"
></a-input-number>
{{ t('components.CronModal.day22') }}
<a-input-number
size="small"
v-model:value="data.incrementStart"
:min="1"
:max="31"
placeholder="1-31"
></a-input-number>
{{ t('components.CronModal.day23') }}
</a-radio>
<a-radio value="3">
{{ t('components.CronModal.day31') }}
<a-input-number
size="small"
v-model:value="data.rangeStart"
:min="1"
:max="31"
placeholder="1-31"
></a-input-number>
{{ t('components.CronModal.day32') }}
<a-input-number
size="small"
v-model:value="data.rangeEnd"
:min="1"
:max="31"
placeholder="1-31"
></a-input-number>
{{ t('components.CronModal.day33') }}
</a-radio>
<a-radio value="4">
{{ t('components.CronModal.day4') }}
</a-radio>
<a-select
v-model:value="data.specific"
size="small"
mode="multiple"
style="width: 100%"
:placeholder="t('components.CronModal.day4')"
:options="optionsSpecific"
></a-select>
<a-radio value="5">
{{ t('components.CronModal.day5') }}
</a-radio>
</a-space>
</a-radio-group>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { reactive, watch, onBeforeMount } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: String,
default: '*',
},
});
/**指定列表初始数据 */
const optionsSpecific = Array.from({ length: 24 }, (_, i) => ({
label: `${i}`.padStart(2, '0'),
value: `${i}`,
}));
/**数据 */
let data = reactive({
type: '1',
/**间隔 */
increment: 2,
incrementStart: 0,
/**周期 */
rangeStart: 0,
rangeEnd: 2,
/**指定 */
specific: ['0'],
});
/**监听数据,将数值格式化 */
watch(data, () => {
let reultValue = '*';
let val = data.type;
// 每一
if (val === '1') {
reultValue = '*';
}
// 间隔
if (val === '2') {
let start = data.incrementStart;
let increment = data.increment;
reultValue = `${start ?? 0}/${increment ?? 0}`;
}
// 周期
if (val === '3') {
let start = data.rangeStart;
let end = data.rangeEnd;
reultValue = `${start ?? 0}-${end ?? 0}`;
}
// 指定
if (val === '4') {
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
}
emit('update:value', reultValue);
});
/**挂载前初始属性 */
onBeforeMount(() => {
const val = props.value;
if (val === '*') {
data.type = '1';
}
if (val.includes('/')) {
const arr = val.split('/');
data.incrementStart = Number(arr[0]);
data.increment = Number(arr[1]);
data.type = '2';
}
if (val.includes('-')) {
const arr = val.split('-');
data.rangeStart = Number(arr[0]);
data.rangeEnd = Number(arr[1]);
data.type = '3';
}
if (val.includes(',')) {
data.specific = val.split(',').sort((a, b) => +a - +b);
data.type = '4';
}
});
</script>
<template>
<a-radio-group size="small" v-model:value="data.type">
<a-space direction="vertical" :size="18">
<a-radio value="1">
{{ t('components.CronModal.hour1') }}
</a-radio>
<a-radio value="2">
{{ t('components.CronModal.hour21') }}
<a-input-number
size="small"
v-model:value="data.increment"
:min="0"
:max="23"
placeholder="0-23"
></a-input-number>
{{ t('components.CronModal.hour22') }}
<a-input-number
size="small"
v-model:value="data.incrementStart"
:min="0"
:max="23"
placeholder="0-23"
></a-input-number>
{{ t('components.CronModal.hour23') }}
</a-radio>
<a-radio value="3">
{{ t('components.CronModal.hour31') }}
<a-input-number
size="small"
v-model:value="data.rangeStart"
:min="0"
:max="23"
placeholder="1-23"
></a-input-number>
{{ t('components.CronModal.hour32') }}
<a-input-number
size="small"
v-model:value="data.rangeEnd"
:min="0"
:max="23"
placeholder="0-23"
></a-input-number>
{{ t('components.CronModal.hour33') }}
</a-radio>
<a-radio value="4">
{{ t('components.CronModal.hour4') }}
</a-radio>
<a-select
v-model:value="data.specific"
size="small"
mode="multiple"
style="width: 100%"
:placeholder="t('components.CronModal.hour4')"
:options="optionsSpecific"
></a-select>
</a-space>
</a-radio-group>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,146 @@
<script setup lang="ts">
import { reactive, watch, onBeforeMount } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: String,
default: '*',
},
});
/**指定列表初始数据 */
const optionsSpecific = Array.from({ length: 60 }, (_, i) => {
return {
label: `${i}`.padStart(2, '0'),
value: `${i}`,
};
});
/**数据 */
let data = reactive({
type: '1',
/**间隔 */
increment: 1,
incrementStart: 0,
/**周期 */
rangeStart: 1,
rangeEnd: 2,
/**指定 */
specific: ['0'],
});
/**监听数据,将数值格式化 */
watch(data, () => {
let reultValue = '*';
let val = data.type;
// 每一
if (val === '1') {
reultValue = '*';
}
// 间隔
if (val === '2') {
let start = data.incrementStart;
let increment = data.increment;
reultValue = `${start ?? 0}/${increment ?? 0}`;
}
// 周期
if (val === '3') {
let start = data.rangeStart;
let end = data.rangeEnd;
reultValue = `${start ?? 0}-${end ?? 0}`;
}
// 指定
if (val === '4') {
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
}
emit('update:value', reultValue);
});
/**挂载前初始属性 */
onBeforeMount(() => {
const val = props.value;
if (val === '*') {
data.type = '1';
}
if (val.includes('/')) {
const arr = val.split('/');
data.incrementStart = Number(arr[0]);
data.increment = Number(arr[1]);
data.type = '2';
}
if (val.includes('-')) {
const arr = val.split('-');
data.rangeStart = Number(arr[0]);
data.rangeEnd = Number(arr[1]);
data.type = '3';
}
if (val.includes(',')) {
data.specific = val.split(',').sort((a, b) => +a - +b);
data.type = '4';
}
});
</script>
<template>
<a-radio-group size="small" v-model:value="data.type">
<a-space direction="vertical" :size="18">
<a-radio value="1">
{{ t('components.CronModal.minute1') }}
</a-radio>
<a-radio value="2">
{{ t('components.CronModal.minute21') }}
<a-input-number
size="small"
v-model:value="data.increment"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.minute22') }}
<a-input-number
size="small"
v-model:value="data.incrementStart"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.minute23') }}
</a-radio>
<a-radio value="3">
{{ t('components.CronModal.minute31') }}
<a-input-number
size="small"
v-model:value="data.rangeStart"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.minute32') }}
<a-input-number
size="small"
v-model:value="data.rangeEnd"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.minute33') }}
</a-radio>
<a-radio value="4">
{{ t('components.CronModal.minute4') }}
</a-radio>
<a-select
v-model:value="data.specific"
size="small"
mode="multiple"
style="width: 100%"
:placeholder="t('components.CronModal.minute4')"
:options="optionsSpecific"
></a-select>
</a-space>
</a-radio-group>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,148 @@
<script setup lang="ts">
import { reactive, watch, onBeforeMount } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: String,
default: '*',
},
});
/**指定列表初始数据 */
const optionsSpecific = Array.from({ length: 12 }, (_, i) => {
let idx = i + 1;
return {
label: `${idx}`,
value: `${idx}`,
};
});
/**构建数据 */
let data = reactive({
/**类型 */
type: '1',
/**间隔 */
increment: 1,
incrementStart: 1,
/**周期 */
rangeStart: 1,
rangeEnd: 2,
/**指定秒 */
specific: ['1'],
});
/**监听数据,将数值格式化 */
watch(data, () => {
let reultValue = '*';
let val = data.type;
// 每一
if (val === '1') {
reultValue = '*';
}
// 间隔
if (val === '2') {
let start = data.incrementStart;
let increment = data.increment;
reultValue = `${start ?? 0}/${increment ?? 0}`;
}
// 周期
if (val === '3') {
let start = data.rangeStart;
let end = data.rangeEnd;
reultValue = `${start ?? 0}-${end ?? 0}`;
}
// 指定
if (val === '4') {
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
}
emit('update:value', reultValue);
});
/**挂载前初始属性 */
onBeforeMount(() => {
const val = props.value;
if (val === '*') {
data.type = '1';
}
if (val.includes('/')) {
const arr = val.split('/');
data.incrementStart = Number(arr[0]);
data.increment = Number(arr[1]);
data.type = '2';
}
if (val.includes('-')) {
const arr = val.split('-');
data.rangeStart = Number(arr[0]);
data.rangeEnd = Number(arr[1]);
data.type = '3';
}
if (val.includes(',')) {
data.specific = val.split(',').sort((a, b) => +a - +b);
data.type = '4';
}
});
</script>
<template>
<a-radio-group size="small" v-model:value="data.type">
<a-space direction="vertical" :size="18">
<a-radio value="1">
{{ t('components.CronModal.month1') }}
</a-radio>
<a-radio value="2">
{{ t('components.CronModal.month21') }}
<a-input-number
size="small"
v-model:value="data.increment"
:min="1"
:max="12"
placeholder="1-12"
></a-input-number>
{{ t('components.CronModal.month22') }}
<a-input-number
size="small"
v-model:value="data.incrementStart"
:min="1"
:max="12"
placeholder="1-12"
></a-input-number>
{{ t('components.CronModal.month23') }}
</a-radio>
<a-radio value="3">
{{ t('components.CronModal.month31') }}
<a-input-number
size="small"
v-model:value="data.rangeStart"
:min="1"
:max="12"
placeholder="1-12"
></a-input-number>
{{ t('components.CronModal.month32') }}
<a-input-number
size="small"
v-model:value="data.rangeEnd"
:min="1"
:max="12"
placeholder="1-12"
></a-input-number>
{{ t('components.CronModal.month33') }}
</a-radio>
<a-radio value="4">
{{ t('components.CronModal.month4') }}
</a-radio>
<a-select
v-model:value="data.specific"
size="small"
mode="multiple"
style="width: 100%"
:placeholder="t('components.CronModal.month4')"
:options="optionsSpecific"
></a-select>
</a-space>
</a-radio-group>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,145 @@
<script setup lang="ts">
import { reactive, watch, onBeforeMount } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: String,
default: '*',
},
});
/**指定列表初始数据 */
const optionsSpecific = Array.from({ length: 60 }, (_, i) => ({
label: `${i}`.padStart(2, '0'),
value: `${i}`,
}));
/**构建数据 */
let data = reactive({
/**类型 */
type: '1',
/**间隔 */
increment: 2,
incrementStart: 0,
/**周期 */
rangeStart: 1,
rangeEnd: 2,
/**指定秒 */
specific: ['0'],
});
/**监听数据,将数值格式化 */
watch(data, () => {
let reultValue = '*';
let val = data.type;
// 每一
if (val === '1') {
reultValue = '*';
}
// 间隔
if (val === '2') {
let start = data.incrementStart;
let increment = data.increment;
reultValue = `${start ?? 0}/${increment ?? 0}`;
}
// 周期
if (val === '3') {
let start = data.rangeStart;
let end = data.rangeEnd;
reultValue = `${start ?? 0}-${end ?? 0}`;
}
// 指定
if (val === '4') {
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
}
emit('update:value', reultValue);
});
/**挂载前初始属性 */
onBeforeMount(() => {
const val = props.value;
if (val === '*') {
data.type = '1';
}
if (val.includes('/')) {
const arr = val.split('/');
data.incrementStart = Number(arr[0]);
data.increment = Number(arr[1]);
data.type = '2';
}
if (val.includes('-')) {
const arr = val.split('-');
data.rangeStart = Number(arr[0]);
data.rangeEnd = Number(arr[1]);
data.type = '3';
}
if (val.includes(',')) {
data.specific = val.split(',').sort((a, b) => +a - +b);
data.type = '4';
}
});
</script>
<template>
<a-radio-group size="small" v-model:value="data.type">
<a-space direction="vertical" :size="18">
<a-radio value="1">
{{ t('components.CronModal.second1') }}
</a-radio>
<a-radio value="2">
{{ t('components.CronModal.second21') }}
<a-input-number
size="small"
v-model:value="data.increment"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.second22') }}
<a-input-number
size="small"
v-model:value="data.incrementStart"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.second23') }}
</a-radio>
<a-radio value="3">
{{ t('components.CronModal.second31') }}
<a-input-number
size="small"
v-model:value="data.rangeStart"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.second32') }}
<a-input-number
size="small"
v-model:value="data.rangeEnd"
:min="0"
:max="59"
placeholder="0-59"
></a-input-number>
{{ t('components.CronModal.second33') }}
</a-radio>
<a-radio value="4">
{{ t('components.CronModal.second4') }}
</a-radio>
<a-select
v-model:value="data.specific"
size="small"
mode="multiple"
style="width: 100%"
:placeholder="t('components.CronModal.second4')"
:options="optionsSpecific"
></a-select>
</a-space>
</a-radio-group>
</template>
<style lang="scss" scoped></style>
+114
View File
@@ -0,0 +1,114 @@
<template>
<a-modal
:destroyOnClose="true"
:title="t('components.CronModal.title')"
:open="props.open"
:body-style="{ padding: '0 24px' }"
@cancel="fnCronModal(false)"
@ok="fnCronModal(true)"
>
<a-tabs tab-position="top" type="line">
<a-tab-pane key="1" :tab="t('common.units.second')">
<CronSecond v-model:value="cronValue.second"></CronSecond>
</a-tab-pane>
<a-tab-pane key="2" :tab="t('common.units.minute')">
<CronMinute v-model:value="cronValue.minute"></CronMinute>
</a-tab-pane>
<a-tab-pane key="3" :tab="t('common.units.hour')">
<CronHour v-model:value="cronValue.hour"></CronHour>
</a-tab-pane>
<a-tab-pane key="4" :tab="t('common.units.day')">
<CronDay v-model:value="cronValue.day"></CronDay>
</a-tab-pane>
<a-tab-pane key="5" :tab="t('common.units.month')">
<CronMonth v-model:value="cronValue.month"></CronMonth>
</a-tab-pane>
</a-tabs>
<a-input
class="reultBox"
:addon-before="t('components.CronModal.addon')"
v-model:value="cronStr"
disabled
/>
</a-modal>
</template>
<script lang="ts" setup>
import CronSecond from './components/Second.vue';
import CronMinute from './components/Minute.vue';
import CronHour from './components/Hour.vue';
import CronDay from './components/Day.vue';
import CronMonth from './components/Month.vue';
import { reactive, computed, watch } from 'vue';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const emit = defineEmits(['cancel', 'ok', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
required: true,
},
cron: {
type: String,
default: '* * * * * ?',
},
});
/**cron属性值 */
let cronValue = reactive({
second: '*',
minute: '*',
hour: '*',
day: '*',
month: '*',
week: '?',
});
/**组合cron结果 */
const cronStr = computed(() => {
let time = `${cronValue.second} ${cronValue.minute} ${cronValue.hour}`;
let date = `${cronValue.day} ${cronValue.month} ${cronValue.week}`;
return `${time} ${date}`;
});
/**监听是否显示,初始cron属性 */
watch(
() => props.open,
val => {
if (!val) return;
const arr = props.cron.split(' ');
// 6 位以上是合法表达式
if (arr.length >= 6) {
Object.assign(cronValue, {
second: arr[0],
minute: arr[1],
hour: arr[2],
day: arr[3],
month: arr[4],
week: arr[5],
});
}
}
);
/**
* 窗口事件
* @param val modal触发事件
*/
function fnCronModal(val: boolean) {
emit('update:open', false);
if (val) {
emit('ok', cronStr.value);
} else {
emit('cancel');
}
}
</script>
<style lang="scss" scoped>
.reultBox {
margin-top: 48px;
margin-bottom: 24px;
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { computed, PropType } from 'vue';
const props = defineProps({
/**数据 */
options: {
type: Array as PropType<DictType[]>,
},
/**当前的值对应数据中的项字段 */
valueField: {
type: String as PropType<keyof DictType>,
default: 'value',
},
/**当前的值 */
value: {
type: [Number, String],
default: '',
},
/**数据默认值,当前值不存在时 */
valueDefault: {
type: [Number, String],
},
});
/**遍历找到对应值数据项 */
const item = computed(() => {
if (Array.isArray(props.options) && props.options.length > 0) {
let option = props.options.find(
item => `${item[props.valueField]}` === `${props.value}`
);
// 数据默认值
if (!option && props.valueDefault != undefined) {
option = props.options.find(
item => `${item[props.valueField]}` === `${props.valueDefault}`
);
}
return option;
}
return undefined;
});
</script>
<template>
<template v-if="item">
<a-tag v-if="item.tagType" :class="!item.tagClass?'':item.tagClass" :color="!item.tagType?'':item.tagType">
{{ item.label }}
</a-tag>
<a-tag v-else :class="!item.tagClass?'':item.tagClass">
{{ item.label }}
</a-tag>
</template>
<span v-else>{{ value }}</span>
</template>
<style lang="less" scoped></style>
@@ -1,5 +1,7 @@
<script setup lang="ts">
import { isShowBtn } from '@/utils/permission';
import { ContainerOutlined } from '@ant-design/icons-vue';
defineOptions({
name: 'TableHeaderOperation'
@@ -10,6 +12,9 @@ interface Props {
loading?: boolean;
tableType?: string;
showDelete?: boolean;
showExport?: boolean;
notShowAdd?: boolean;
showView?: boolean;
}
defineProps<Props>();
@@ -18,6 +23,8 @@ interface Emits {
(e: 'add'): void;
(e: 'delete'): void;
(e: 'refresh'): void;
(e: 'export'): void;
(e: 'view'): void;
}
const emit = defineEmits<Emits>();
@@ -37,24 +44,30 @@ function batchDelete() {
function refresh() {
emit('refresh');
}
function handleExport() {
emit('export');
}
function handleView() {
emit('view');
}
</script>
<template>
<div class="flex flex-wrap justify-end gap-x-12px gap-y-8px lt-sm:(w-200px py-12px)">
<slot name="prefix"></slot>
<slot name="default">
<AButton v-if="isShowBtn(`system:${tableType}:add`)" size="small" ghost type="primary" @click="add">
<AButton v-if="isShowBtn(`system:${tableType}:add`) && !notShowAdd" size="small" ghost type="primary"
@click="add">
<div class="flex-y-center gap-8px">
<icon-ic-round-plus class="text-icon" />
<span>{{ $t('common.add') }}</span>
</div>
</AButton>
<APopconfirm
v-if="isShowBtn(`system:${tableType}:remove`) && showDelete"
:title="$t('common.confirmDelete')"
:disabled="disabledDelete"
@confirm="batchDelete"
>
<APopconfirm v-if="isShowBtn(`system:${tableType}:remove`) && showDelete" :title="$t('common.confirmDelete')"
:disabled="disabledDelete" @confirm="batchDelete">
<AButton size="small" danger :disabled="disabledDelete">
<div class="flex-y-center gap-8px">
<icon-ic-round-delete class="text-icon" />
@@ -69,6 +82,19 @@ function refresh() {
<span>{{ $t('common.refresh') }}</span>
</div>
</AButton>
<AButton size="small" type="primary" ghost @click="handleExport" v-show="showExport">
<div class="flex-y-center gap-8px">
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
<span>{{ $t('common.export') }}</span>
</div>
</AButton>
<AButton size="small" type="default" @click.prevent="handleView" v-show="showView">
<template #icon>
<ContainerOutlined />
</template>
{{ $t('route.manage_dictData') }}
</AButton>
<TableColumnSetting v-model:columns="columns" />
<slot name="suffix"></slot>
</div>
+2 -2
View File
@@ -1,13 +1,13 @@
import { transformRecordToOption } from '@/utils/common';
export const enableStatusRecord: Record<Api.Common.EnableStatus, App.I18n.I18nKey> = {
export const enableStatusRecord: any= {
'0': 'page.manage.common.status.enable',
'1': 'page.manage.common.status.disable'
};
export const enableStatusOptions = transformRecordToOption(enableStatusRecord);
export const menuIconTypeRecord: Record<Api.SystemManage.IconType, App.I18n.I18nKey> = {
export const menuIconTypeRecord: any = {
'1': 'page.manage.menu.iconType.iconify',
'2': 'page.manage.menu.iconType.local'
};
+1 -1
View File
@@ -9,7 +9,7 @@ export const REG_PHONE = /^.{3,}$/;
*
* 6-18 characters, including letters, numbers, and underscores
*/
export const REG_PWD = /^\w{6,18}$/;
export const REG_PWD = /^[\w.]{6,18}$/;
/** Email reg */
export const REG_EMAIL = ///^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
+314
View File
@@ -0,0 +1,314 @@
// 地区选项
export const regionOptions = [
{ label: 'Afghanistan', value: 'Afghanistan/AF/+93' },
{ label: 'Albania', value: 'Albania/AL/+355' },
{ label: 'Algeria', value: 'Algeria/DZ/+213' },
{ label: 'Angola', value: 'Angola/AO/+244' },
{ label: 'Anguilla', value: 'Anguilla/AI/+1264' },
{ label: 'Antigua and Barbuda', value: 'Antigua and Barbuda/AG/+268' },
{ label: 'Argentina', value: 'Argentina/AR/+54' },
{ label: 'Armenia', value: 'Armenia/AM/+374' },
{ label: 'Aruba', value: 'Aruba/AW/+297' },
{ label: 'Australia', value: 'Australia/AU/+61' },
{ label: 'Austria', value: 'Austria/AT/+43' },
{ label: 'Azerbaijan', value: 'Azerbaijan/AZ/+994' },
{ label: 'Bahamas', value: 'Bahamas/BS/+1242' },
{ label: 'Bahrain', value: 'Bahrain/BH/+973' },
{ label: 'Bangladesh', value: 'Bangladesh/BD/+880' },
{ label: 'Barbados', value: 'Barbados/BB/+1246' },
{ label: 'Belarus', value: 'Belarus/BY/+375' },
{ label: 'Belgium', value: 'Belgium/BE/+32' },
{ label: 'Belize', value: 'Belize/BZ/+501' },
{ label: 'Benin', value: 'Benin/BJ/+229' },
{ label: 'Bermuda', value: 'Bermuda/BM/+1441' },
{ label: 'Bhutan', value: 'Bhutan/BT/+975' },
{ label: 'Bolivia', value: 'Bolivia/BO/+591' },
{ label: 'Botswana', value: 'Botswana/BW/+267' },
{ label: 'Brasil', value: 'Brasil/BR/+55' },
{ label: 'Brunei', value: 'Brunei/BN/+673' },
{ label: 'Burkina Faso', value: 'Burkina Faso/BF/+226' },
{ label: 'Burundi', value: 'Burundi/BI/+257' },
{ label: 'Cabo Verde', value: 'Cabo Verde/CV/+238' },
{ label: 'Cambodia', value: 'Cambodia/KH/+855' },
{ label: 'Cameroon', value: 'Cameroon/CM/+237' },
{ label: 'Canada', value: 'Canada/CA/+1' },
{ label: 'Cayman Islands', value: 'Cayman Islands/KY/+1345' },
{ label: 'Central African', value: 'Central African/CF/+236' },
{ label: 'Chad', value: 'Chad/TD/+235' },
{ label: 'Chile', value: 'Chile/CL/+56' },
{ label: 'China', value: 'China/CN/+86' },
{ label: 'Christmas Island', value: 'Christmas Island/CX/+618' },
{ label: 'Colombia', value: 'Colombia/CO/+57' },
{ label: 'Comoros', value: 'Comoros/KM/+269' },
{ label: 'Congo', value: 'Congo/CD/+243' },
{ label: 'Congo', value: 'Congo/CG/+242' },
{ label: 'Costa Rica', value: 'Costa Rica/CR/+506' },
{ label: "Cote d'Ivoire", value: "Cote d'Ivoire/CI/+225" },
{ label: 'Croatia', value: 'Croatia/HR/+385' },
{ label: 'Cuba', value: 'Cuba/CU/+53' },
{ label: 'Cyprus', value: 'Cyprus/CY/+357' },
{ label: 'Czech', value: 'Czech/CZ/+420' },
{ label: 'Denmark', value: 'Denmark/DK/+45' },
{ label: 'Djibouti', value: 'Djibouti/DJ/+253' },
{ label: 'Dominica', value: 'Dominica/DM/+1767' },
{ label: 'Dominican', value: 'Dominican/DO/+1809' },
{ label: 'Ecuador', value: 'Ecuador/EC/+593' },
{ label: 'Egypt', value: 'Egypt/EG/+20' },
{ label: 'Equatorial Guinea', value: 'Equatorial Guinea/GQ/+240' },
{ label: 'Eritrea', value: 'Eritrea/ER/+291' },
{ label: 'Estonia', value: 'Estonia/EE/+372' },
{ label: 'Ethiopia', value: 'Ethiopia/ET/+251' },
{ label: 'Falkland Islands', value: 'Falkland Islands/FK/+500' },
{ label: 'Fiji', value: 'Fiji/FJ/+679' },
{ label: 'Finland', value: 'Finland/FI/+358' },
{ label: 'France', value: 'France/FR/+33' },
{ label: 'French Guiana', value: 'French Guiana/GF/+594' },
{ label: 'French Polynesia', value: 'French Polynesia/PF/+689' },
{ label: 'Gabon', value: 'Gabon/GA/+241' },
{ label: 'Gambia', value: 'Gambia/GM/+220' },
{ label: 'Georgia', value: 'Georgia/GE/+995' },
{ label: 'Germany', value: 'Germany/DE/+49' },
{ label: 'Ghana', value: 'Ghana/GH/+233' },
{ label: 'Greece', value: 'Greece/GR/+30' },
{ label: 'Greenland', value: 'Greenland/GL/+45' },
{ label: 'Grenada', value: 'Grenada/GD/+1473' },
{ label: 'Guam', value: 'Guam/GU/+1' },
{ label: 'Guatemala', value: 'Guatemala/GT/+502' },
{ label: 'Guinea', value: 'Guinea/GN/+224' },
{ label: 'Guinea-Bissau', value: 'Guinea-Bissau/GW/+245' },
{ label: 'Guyana', value: 'Guyana/GY/+592' },
{ label: 'Haiti', value: 'Haiti/HT/+509' },
{ label: 'Honduras', value: 'Honduras/HN/+504' },
{ label: 'Hong Kong', value: 'Hong Kong/HK/+852' },
{ label: 'Hungary', value: 'Hungary/HU/+36' },
{ label: 'Iceland', value: 'Iceland/IS/+354' },
{ label: 'India', value: 'India/IN/+91' },
{ label: 'Indonesia', value: 'Indonesia/ID/+62' },
{ label: 'Iran', value: 'Iran/IR/+98' },
{ label: 'Iraq', value: 'Iraq/IQ/+964' },
{ label: 'Ireland', value: 'Ireland/IE/+353' },
{ label: 'Israel', value: 'Israel/IL/+972' },
{ label: 'Italy', value: 'Italy/IT/+39' },
{ label: 'Jamaica', value: 'Jamaica/JM/+876' },
{ label: 'Japan', value: 'Japan/JP/+81' },
{ label: 'Jordan', value: 'Jordan/JO/+962' },
{ label: 'Kazakhstan', value: 'Kazakhstan/KZ/+7' },
{ label: 'Kenya', value: 'Kenya/KE/+254' },
{ label: 'Kiribati', value: 'Kiribati/KI/+686' },
{ label: 'Korea', value: 'Korea/KR/+82' },
{ label: 'Kuwait', value: 'Kuwait/KW/+965' },
{ label: 'Kyrgyzstan', value: 'Kyrgyzstan/KG/+996' },
{ label: 'Lao', value: 'Lao/LA/+856' },
{ label: 'Latvia', value: 'Latvia/LV/+371' },
{ label: 'Lebanon', value: 'Lebanon/LB/+961' },
{ label: 'Lesotho', value: 'Lesotho/LS/+266' },
{ label: 'Liberia', value: 'Liberia/LR/+231' },
{ label: 'Libya', value: 'Libya/LY/+218' },
{ label: 'Liechtenstein', value: 'Liechtenstein/LI/+423' },
{ label: 'Lithuania', value: 'Lithuania/LT/+370' },
{ label: 'Luxembourg', value: 'Luxembourg/LU/+352' },
{ label: 'Macao', value: 'Macao/MO/+853' },
{ label: 'Macedonia', value: 'Macedonia/MK/+389' },
{ label: 'Madagascar', value: 'Madagascar/MG/+261' },
{ label: 'Malawi', value: 'Malawi/MW/+265' },
{ label: 'Malaysia', value: 'Malaysia/MY/+60' },
{ label: 'Maldives', value: 'Maldives/MV/+960' },
{ label: 'Mali', value: 'Mali/ML/+223' },
{ label: 'Malta', value: 'Malta/MT/+356' },
{ label: 'Marshall Islands', value: 'Marshall Islands/MH/+692' },
{ label: 'Mauritania', value: 'Mauritania/MR/+222' },
{ label: 'Mauritius', value: 'Mauritius/MU/+230' },
{ label: 'Mexico', value: 'Mexico/MX/+52' },
{ label: 'Micronesia', value: 'Micronesia/FM/+691' },
{ label: 'Moldova', value: 'Moldova/MD/+373' },
{ label: 'Monaco', value: 'Monaco/MC/+377' },
{ label: 'Mongolia', value: 'Mongolia/MN/+976' },
{ label: 'Montenegro', value: 'Montenegro/ME/+382' },
{ label: 'Morocco', value: 'Morocco/MA/+212' },
{ label: 'Mozambique', value: 'Mozambique/MZ/+258' },
{ label: 'Myanmar', value: 'Myanmar/MM/+95' },
{ label: 'Namibia', value: 'Namibia/NA/+264' },
{ label: 'Nauru', value: 'Nauru/NR/+674' },
{ label: 'Nepal', value: 'Nepal/NP/+977' },
{ label: 'Netherlands', value: 'Netherlands/NL/+31' },
{ label: 'Netherlands Antilles', value: 'Netherlands Antilles/AN/+599' },
{ label: 'New Caledonia', value: 'New Caledonia/NC/+687' },
{ label: 'New Zealand', value: 'New Zealand/NZ/+64' },
{ label: 'Nicaragua', value: 'Nicaragua/NI/+505' },
{ label: 'Niger', value: 'Niger/NE/+227' },
{ label: 'Nigeria', value: 'Nigeria/NG/+234' },
{ label: 'Niue', value: 'Niue/NU/+683' },
{ label: 'North Korea', value: 'North Korea/KP/+850' },
{ label: 'Norway', value: 'Norway/NO/+47' },
{ label: 'Oman', value: 'Oman/OM/+968' },
{ label: 'Pakistan', value: 'Pakistan/PK/+92' },
{ label: 'Palau', value: 'Palau/PW/+680' },
{ label: 'Palestine', value: 'Palestine/PS/+970' },
{ label: 'Panama', value: 'Panama/PA/+507' },
{ label: 'Papua New Guinea', value: 'Papua New Guinea/PG/+675' },
{ label: 'Paraguay', value: 'Paraguay/PY/+595' },
{ label: 'Peru', value: 'Peru/PE/+51' },
{ label: 'Philippines', value: 'Philippines/PH/+63' },
{ label: 'Pitcairn Islands', value: 'Pitcairn Islands/PN/+64' },
{ label: 'Poland', value: 'Poland/PL/+48' },
{ label: 'Portuguese', value: 'Portuguese/PT/+351' },
{ label: 'Puerto Rico', value: 'Puerto Rico/PR/+1787' },
{ label: 'Qatar', value: 'Qatar/QA/+974' },
{ label: 'Romania', value: 'Romania/RO/+40' },
{ label: 'Russia', value: 'Russia/RU/+7' },
{ label: 'Rwanda', value: 'Rwanda/RW/+250' },
{ label: 'Saint Kitts and Nevis', value: 'Saint Kitts and Nevis/KN/+1869' },
{ label: 'Saint Lucia', value: 'Saint Lucia/LC/+1758' },
{ label: 'Saint Vincent and the Grenadines', value: 'Saint Vincent and the Grenadines/VC/+1784' },
{ label: 'Saint-Martin', value: 'Saint-Martin/MF/+1721' },
{ label: 'Salvador', value: 'Salvador/SV/+503' },
{ label: 'Samoa', value: 'Samoa/WS/+685' },
{ label: 'San Marino', value: 'San Marino/SM/+378' },
{ label: 'Sao Tome and Principe', value: 'Sao Tome and Principe/ST/+239' },
{ label: 'Saudi Arabia', value: 'Saudi Arabia/SA/+966' },
{ label: 'Senegal', value: 'Senegal/SN/+221' },
{ label: 'Serbia', value: 'Serbia/RS/+381' },
{ label: 'Seychelles', value: 'Seychelles/SC/+248' },
{ label: 'Sierra Leone', value: 'Sierra Leone/SL/+232' },
{ label: 'Singapore', value: 'Singapore/SG/+65' },
{ label: 'Slovak', value: 'Slovak/SK/+421' },
{ label: 'Slovenia', value: 'Slovenia/SI/+386' },
{ label: 'Solomon Islands', value: 'Solomon Islands/SB/+677' },
{ label: 'Somalia', value: 'Somalia/SO/+252' },
{ label: 'South Africa', value: 'South Africa/ZA/+27' },
{ label: 'South Sudan', value: 'South Sudan/SS/+211' },
{ label: 'Spain', value: 'Spain/ES/+34' },
{ label: 'Sri Lanka', value: 'Sri Lanka/LK/+94' },
{ label: 'Sudan', value: 'Sudan/SD/+249' },
{ label: 'Suriname', value: 'Suriname/SR/+597' },
{ label: 'Swaziland', value: 'Swaziland/SZ/+268' },
{ label: 'Sweden', value: 'Sweden/SE/+46' },
{ label: 'Swiss', value: 'Swiss/CH/+41' },
{ label: 'Syria', value: 'Syria/SY/+963' },
{ label: 'Taiwan', value: 'Taiwan/TW/+886' },
{ label: 'Tajikistan', value: 'Tajikistan/TJ/+992' },
{ label: 'Tanzania', value: 'Tanzania/TZ/+255' },
{ label: 'Thailand', value: 'Thailand/TH/+66' },
{ label: 'The Cook Islands', value: 'The Cook Islands/CK/+682' },
{ label: 'The Turks and Caicos Islands', value: 'The Turks and Caicos Islands/TC/+1649' },
{ label: 'The United States Virgin Islands', value: 'The United States Virgin Islands/VI/+1340' },
{ label: 'Timor-Leste', value: 'Timor-Leste/TL/+670' },
{ label: 'Togo', value: 'Togo/TG/+228' },
{ label: 'Tokelau', value: 'Tokelau/TK/+690' },
{ label: 'Tonga', value: 'Tonga/TO/+676' },
{ label: 'Trinidad and Tobago', value: 'Trinidad and Tobago/TT/+1868' },
{ label: 'Tunisia', value: 'Tunisia/TN/+216' },
{ label: 'Turkey', value: 'Turkey/TR/+90' },
{ label: 'Turkmenistan', value: 'Turkmenistan/TM/+993' },
{ label: 'Tuvalu', value: 'Tuvalu/TV/+688' },
{ label: 'Uganda', value: 'Uganda/UG/+256' },
{ label: 'Ukraine', value: 'Ukraine/UA/+380' },
{ label: 'United Arab Emirates', value: 'United Arab Emirates/AE/+971' },
{ label: 'United Kingdom', value: 'United Kingdom/UK/+44' },
{ label: 'United States', value: 'United States/US/+1' },
{ label: 'Uruguay', value: 'Uruguay/UY/+598' },
{ label: 'Uzbekistan', value: 'Uzbekistan/UZ/+998' },
{ label: 'Vanuatu', value: 'Vanuatu/VU/+678' },
{ label: 'Vatican', value: 'Vatican/VA/+379' },
{ label: 'Venezuela', value: 'Venezuela/VE/+58' },
{ label: 'Vietnam', value: 'Vietnam/VN/+84' },
{ label: 'Yemen', value: 'Yemen/YE/+967' },
{ label: 'Zambia', value: 'Zambia/ZM/+260' },
{ label: 'Zimbabwe', value: 'Zimbabwe/ZW/+263' }
];
// 时区选项
export const timeZoneOptions = [
{ label: 'Pacific/Wake', value: 'Pacific/Wake' },
{ label: 'Pacific/Midway', value: 'Pacific/Midway' },
{ label: 'Pacific/Honolulu', value: 'Pacific/Honolulu' },
{ label: 'America/Anchorage', value: 'America/Anchorage' },
{ label: 'America/Los_Angeles', value: 'America/Los_Angeles' },
{ label: 'America/Phoenix', value: 'America/Phoenix' },
{ label: 'America/Chihuahua', value: 'America/Chihuahua' },
{ label: 'America/Denver', value: 'America/Denver' },
{ label: 'America/Tegucigalpa', value: 'America/Tegucigalpa' },
{ label: 'America/Chicago', value: 'America/Chicago' },
{ label: 'America/Mexico_City', value: 'America/Mexico_City' },
{ label: 'Canada/Saskatchewan', value: 'Canada/Saskatchewan' },
{ label: 'America/Bogota', value: 'America/Bogota' },
{ label: 'America/New_York', value: 'America/New_York' },
{ label: 'America/Indiana/Indianapolis', value: 'America/Indiana/Indianapolis' },
{ label: 'America/Caracas', value: 'America/Caracas' },
{ label: 'America/Asuncion', value: 'America/Asuncion' },
{ label: 'America/Halifax', value: 'America/Halifax' },
{ label: 'America/Cuiaba', value: 'America/Cuiaba' },
{ label: 'America/La_Paz', value: 'America/La_Paz' },
{ label: 'Canada/Newfoundland', value: 'Canada/Newfoundland' },
{ label: 'America/Sao_Paulo', value: 'America/Sao_Paulo' },
{ label: 'America/Buenos_Aires', value: 'America/Buenos_Aires' },
{ label: 'America/Cayenne', value: 'America/Cayenne' },
{ label: 'America/Godthab', value: 'America/Godthab' },
{ label: 'America/Montevideo', value: 'America/Montevideo' },
{ label: 'America/Santiago', value: 'America/Santiago' },
{ label: 'Atlantic/South_Georgia', value: 'Atlantic/South_Georgia' },
{ label: 'Atlantic/Azores', value: 'Atlantic/Azores' },
{ label: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' },
{ label: 'Africa/Casablanca', value: 'Africa/Casablanca' },
{ label: 'UTC', value: 'UTC' },
{ label: 'Europe/London', value: 'Europe/London' },
{ label: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' },
{ label: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
{ label: 'Europe/Belgrade', value: 'Europe/Belgrade' },
{ label: 'Europe/Brussels', value: 'Europe/Brussels' },
{ label: 'Europe/Sarajevo', value: 'Europe/Sarajevo' },
{ label: 'Africa/Algiers', value: 'Africa/Algiers' },
{ label: 'Europe/Athens', value: 'Europe/Athens' },
{ label: 'Asia/Beirut', value: 'Asia/Beirut' },
{ label: 'Africa/Cairo', value: 'Africa/Cairo' },
{ label: 'Asia/Damascus', value: 'Asia/Damascus' },
{ label: 'Africa/Harare', value: 'Africa/Harare' },
{ label: 'Europe/Vilnius', value: 'Europe/Vilnius' },
{ label: 'Asia/Jerusalem', value: 'Asia/Jerusalem' },
{ label: 'Asia/Amman', value: 'Asia/Amman' },
{ label: 'Asia/Baghdad', value: 'Asia/Baghdad' },
{ label: 'Europe/Minsk', value: 'Europe/Minsk' },
{ label: 'Asia/Kuwait', value: 'Asia/Kuwait' },
{ label: 'Africa/Nairobi', value: 'Africa/Nairobi' },
{ label: 'Asia/Istanbul', value: 'Asia/Istanbul' },
{ label: 'Asia/Tehran', value: 'Asia/Tehran' },
{ label: 'Asia/Muscat', value: 'Asia/Muscat' },
{ label: 'Asia/Baku', value: 'Asia/Baku' },
{ label: 'Europe/Moscow', value: 'Europe/Moscow' },
{ label: 'Asia/Tbilisi', value: 'Asia/Tbilisi' },
{ label: 'Asia/Yerevan', value: 'Asia/Yerevan' },
{ label: 'Asia/Kabul', value: 'Asia/Kabul' },
{ label: 'Asia/Karachi', value: 'Asia/Karachi' },
{ label: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' },
{ label: 'Asia/Tashkent', value: 'Asia/Tashkent' },
{ label: 'Asia/Kolkata', value: 'Asia/Kolkata' },
{ label: 'Asia/Colombo', value: 'Asia/Colombo' },
{ label: 'Asia/Katmandu', value: 'Asia/Katmandu' },
{ label: 'Asia/Dhaka', value: 'Asia/Dhaka' },
{ label: 'Asia/Rangoon', value: 'Asia/Rangoon' },
{ label: 'Asia/Bangkok', value: 'Asia/Bangkok' },
{ label: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' },
{ label: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' },
{ label: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
{ label: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' },
{ label: 'Australia/Perth', value: 'Australia/Perth' },
{ label: 'Asia/Taipei', value: 'Asia/Taipei' },
{ label: 'Asia/Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
{ label: 'Asia/Irkutsk', value: 'Asia/Irkutsk' },
{ label: 'Asia/Tokyo', value: 'Asia/Tokyo' },
{ label: 'Asia/Seoul', value: 'Asia/Seoul' },
{ label: 'Australia/Adelaide', value: 'Australia/Adelaide' },
{ label: 'Australia/Darwin', value: 'Australia/Darwin' },
{ label: 'Australia/Brisbane', value: 'Australia/Brisbane' },
{ label: 'Australia/Canberra', value: 'Australia/Canberra' },
{ label: 'Pacific/Guam', value: 'Pacific/Guam' },
{ label: 'Australia/Hobart', value: 'Australia/Hobart' },
{ label: 'Asia/Yakutsk', value: 'Asia/Yakutsk' },
{ label: 'Pacific/Noumea', value: 'Pacific/Noumea' },
{ label: 'Asia/Vladivostok', value: 'Asia/Vladivostok' },
{ label: 'Pacific/Auckland', value: 'Pacific/Auckland' },
{ label: 'Pacific/Fiji', value: 'Pacific/Fiji' },
{ label: 'Asia/Magadan', value: 'Asia/Magadan' },
{ label: 'Asia/Kamchatka', value: 'Asia/Kamchatka' },
{ label: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' }
];
+17 -4
View File
@@ -1,4 +1,4 @@
import { useAuthStore } from '@/store/modules/auth';
import {useAuthStore} from '@/store/modules/auth';
export function useAuth() {
const authStore = useAuthStore();
@@ -9,13 +9,26 @@ export function useAuth() {
}
if (typeof codes === 'string') {
return authStore.userInfo.buttons.includes(codes);
return authStore.userInfo.permissions.includes(codes);
}
return codes.some(code => authStore.userInfo.buttons.includes(code));
return codes.some(code => authStore.userInfo.permissions.includes(code));
}
function hasRole(role: string | string[]) {
if (!authStore.isLogin) {
return false;
}
if (typeof role === 'string') {
return authStore.userInfo.roles.includes(role);
}
return role.some(code => authStore.userInfo.roles.includes(code));
}
return {
hasAuth
hasAuth,
hasRole,
};
}
+7 -9
View File
@@ -1,8 +1,8 @@
import { computed } from 'vue';
import { useCountDown, useLoading } from '@sa/hooks';
import { $t } from '@/locales';
import {REG_EMAIL} from '@/constants/reg';
import {useAuthStore} from "@/store/modules/auth";
import { REG_EMAIL } from '@/constants/reg';
import { useAuthStore } from '@/store/modules/auth';
export function useCaptcha() {
const { loading, startLoading, endLoading } = useLoading();
@@ -39,20 +39,17 @@ export function useCaptcha() {
return true;
}
//获取验证码方法
async function getCaptcha(email: string) {
console.log(email)
//const valid = isPhoneValid(phone);
const valid = isEmailValid(email);
const valid = isEmailValid(email);
if (!valid || loading.value) {
return;
return null;
}
startLoading();
await authStore.captcha(
email,
);
const data = await authStore.captcha(email);
await new Promise(resolve => {
setTimeout(resolve, 500);
});
@@ -62,6 +59,7 @@ export function useCaptcha() {
start();
endLoading();
return data;
}
return {
+11 -2
View File
@@ -4,6 +4,9 @@ import type { FormInstance } from 'ant-design-vue';
import {REG_CODE_FOUR, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME} from '@/constants/reg';
import { $t } from '@/locales';
const regExpPasswd =/^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/;
export function useFormRules() {
const patternRules = {
username: {
@@ -30,7 +33,12 @@ export function useFormRules() {
pattern: REG_EMAIL,
message: $t('form.email.invalid'),
trigger: 'change'
}
},
userPwd:{
pattern: regExpPasswd,
message: $t('form.pwd.invalid'),
trigger: 'change'
},
} satisfies Record<string, App.Global.FormRule>;
const formRules = {
@@ -38,7 +46,8 @@ export function useFormRules() {
phone: [createRequiredRule($t('form.phone.required')), patternRules.phone],
pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd],
code: [createRequiredRule($t('form.code.required')), patternRules.code],
email: [createRequiredRule($t('form.email.required')), patternRules.email]
email: [createRequiredRule($t('form.email.required')), patternRules.email],
userPwd: [createRequiredRule($t('form.pwd.required')), patternRules.userPwd],
} satisfies Record<string, App.Global.FormRule[]>;
/** the default required rule */
+6 -7
View File
@@ -1,7 +1,7 @@
import { useRouter } from 'vue-router';
import type { RouteLocationRaw } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { router as globalRouter } from '@/router';
import type {RouteLocationRaw} from 'vue-router';
import {useRouter} from 'vue-router';
import type {RouteKey} from '@elegant-router/types';
import {router as globalRouter} from '@/router';
/**
* Router push
@@ -24,7 +24,7 @@ export function useRouterPush(inSetup = true) {
}
async function routerPushByKey(key: RouteKey, options?: RouterPushOptions) {
const { query, params } = options || {};
const {query, params} = options || {};
const routeLocation: RouteLocationRaw = {
name: key
@@ -53,7 +53,6 @@ export function useRouterPush(inSetup = true) {
*/
async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) {
const module = loginModule || 'pwd-login';
const options: RouterPushOptions = {
params: {
module
@@ -77,7 +76,7 @@ export function useRouterPush(inSetup = true) {
async function toggleLoginModule(module: UnionKey.LoginModule) {
const query = route.value.query as Record<string, string>;
return routerPushByKey('login', { query, params: { module } });
return routerPushByKey('login', {query, params: {module}});
}
/** Redirect from login */
+22 -21
View File
@@ -6,10 +6,8 @@ import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
type TableData<T = object> = AntDesign.TableData<T>;
type GetTableData<A extends AntDesign.TableApiFn> = AntDesign.GetTableData<A>;
type TableColumn<T> = AntDesign.TableColumn<T>;
export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDesignTableConfig<A>) {
export function useTable(config: any) {
const scope = effectScope();
const appStore = useAppStore();
@@ -26,21 +24,21 @@ export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDe
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<AntDesign.TableDataWithIndex<GetTableData<A>>>>({
} = useHookTable({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
transformer: (res:any) => {
const { rows = [], total = 0 } = res.data || {};
return {
rows: rows.map((row, index) => ({ ...row, id: rowKey ? row[rowKey] : index })),
rows: rows.map((row:any, index:any) => ({ ...row, id: rowKey ? row[rowKey] : index })),
total
};
},
getColumnChecks: cols => {
getColumnChecks: (cols:any) => {
const checks: AntDesign.TableColumnCheck[] = [];
cols.forEach(column => {
cols.forEach((column:any) => {
if (column.key) {
checks.push({
key: column.key as string,
@@ -52,22 +50,22 @@ export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDe
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
getColumns: (cols:any, checks:any) => {
const columnMap = new Map<string, any>();
cols.forEach(column => {
cols.forEach((column:any) => {
if (column.key) {
columnMap.set(column.key as string, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
.filter((item:any) => item.checked)
.map((check:any) => columnMap.get(check.key) );
return filteredColumns;
},
onFetched: async transformed => {
onFetched: async (transformed:any) => {
const { total } = transformed;
updatePagination({
@@ -79,29 +77,32 @@ export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDe
const pagination: TablePaginationConfig = reactive({
showSizeChanger: true,
pageSizeOptions: [10, 15, 20, 25, 30],
pageSizeOptions: ['10', '15', '20', '25', '30'],
total: 0,
simple: false,
// size: 'f',
current: 1,
pageSize: 10,
defaultPageSize: 10,
responsive: true,
onChange: async (current: number, pageSize: number) => {
pagination.current = current;
pagination.pageSize = pageSize;
updateSearchParams({
pageNum: current,
pageSize
});
getData();
}
},
showTotal: (total: number) =>
$t('common.tablePaginationTotal', { total: total })
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: TablePaginationConfig = {
...pagination,
simple: appStore.isMobile
simple: appStore.isMobile
};
return p;
@@ -150,10 +151,10 @@ export function useTableOperate<T extends TableData<{ [key: string]: any }>>(
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<AntDesign.TableOperateType>('add');
const operateType = ref<any>('add');
const { getData, idKey = 'id' } = options;
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
const editingData: any = ref(null);
function handleAdd() {
operateType.value = 'add';
+5 -3
View File
@@ -1,4 +1,8 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
defineOptions({
name: 'GlobalFooter'
});
@@ -6,9 +10,7 @@ defineOptions({
<template>
<DarkModeContainer class="h-full flex-center">
<a href="https://github.com/honghuangdc/soybean-admin/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">
Copyright MIT © 2021 Soybean
</a>
<a href="#" target="_blank" rel="noopener noreferrer">Copyright © 2024 {{ $t('system.title') }}</a>
</DarkModeContainer>
</template>
@@ -3,13 +3,16 @@ import { Modal } from 'ant-design-vue';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import {useRouter} from "vue-router";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
defineOptions({
name: 'UserAvatar'
});
const authStore = useAuthStore();
const { routerPushByKey, toLogin } = useRouterPush();
const { toLogin } = useRouterPush();
function loginOrRegister() {
toLogin();
@@ -22,7 +25,7 @@ function logout() {
okText: $t('common.confirm'),
cancelText: $t('common.cancel'),
onOk: () => {
authStore.resetStore();
authStore.resetStore(true);
}
});
}
@@ -37,10 +40,14 @@ function logout() {
</ButtonIcon>
<template #overlay>
<AMenu>
<AMenuItem @click="routerPushByKey('user-center')">
<!-- <AMenuItem @click="router.push('/user-center/email')">
<div class="flex-center gap-8px">
<SvgIcon icon="ph:user-circle" class="text-icon" />
{{ $t('common.userCenter') }}
{{ $t('common.setemail') }}
</div>
</AMenuItem> -->
<AMenuItem @click="router.push('/static/resetpwd')">
<div class="flex-center gap-8px">
{{ $t('common.resetpwd') }}
</div>
</AMenuItem>
<AMenuDivider />
+5 -2
View File
@@ -10,6 +10,7 @@ import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
import { useMixMenuContext } from '../../context';
import ThemeButton from './components/theme-button.vue';
import UserAvatar from './components/user-avatar.vue';
import {useAuth} from "@/hooks/business/auth";
defineOptions({
name: 'GlobalHeader'
@@ -29,6 +30,8 @@ defineProps<Props>();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { hasRole } = useAuth();
const { isFullscreen, toggle } = useFullscreen();
const { menus } = useMixMenuContext();
@@ -54,14 +57,14 @@ const headerMenus = computed(() => {
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" "/>
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:is-dark="themeStore.darkMode"
@switch="themeStore.toggleThemeScheme"
/>
<ThemeButton />
<ThemeButton v-if="hasRole('super')" />
<UserAvatar />
</div>
</DarkModeContainer>
@@ -82,6 +82,7 @@ function handleClickMixMenu(menu: App.Global.Menu) {
<MixMenuItem
v-for="menu in routeStore.menus"
:key="menu.key"
:title="menu.label"
:label="menu.label"
:icon="menu.icon"
:active="menu.key === activeMenuKey"
@@ -12,7 +12,10 @@ const { routerPushByKey } = useRouterPush();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
// 选择首个子菜单
if (Array.isArray(menu.children) && menu.children.length > 0) {
routerPushByKey(menu.children[0].routeKey);
}
if (!menu.children?.length) {
routerPushByKey(menu.routeKey);
}
+2 -2
View File
@@ -8,13 +8,13 @@ import { localStg } from '@/utils/storage';
*
* @param lang
*/
export function setDayjsLocale(lang: App.I18n.LangType = 'zh-CN') {
export function setDayjsLocale(lang: App.I18n.LangType = 'en-US') {
const localMap = {
'zh-CN': 'zh-cn',
'en-US': 'en'
} satisfies Record<App.I18n.LangType, string>;
const l = lang || localStg.get('lang') || 'zh-CN';
const l = lang || localStg.get('lang') || 'en-US';
locale(localMap[l]);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { localStg } from '@/utils/storage';
import messages from './locale';
const i18n = createI18n({
locale: localStg.get('lang') || 'zh-CN',
locale: localStg.get('lang') || 'en-US',
fallbackLocale: 'en',
messages,
legacy: false
+786 -13
View File
@@ -1,3 +1,13 @@
const viewEn: any = {
"view.dashboard":"dashboard",
"view.endpoint": "endpoint",
"view.endpoint_access": "current",
"view.endpoint_records": "Historical",
"view.billing": "Billing",
"view.billing_histories": "Historical",
"view.set-meal": "Package",
};
const local: any = {
system: {
title: 'WANFi Platform'
@@ -14,9 +24,15 @@ const local: any = {
columnSetting: 'Column Setting',
config: 'Config',
confirm: 'Confirm',
submit:'Confirm',
delete: 'Delete',
view: 'View',
exportOk: "Export Completed",
exportTip: "Confirm exporting xlsx table files based on search criteria?",
deleteSuccess: 'Delete Success',
confirmDelete: 'Are you sure you want to delete?',
confirmClean: 'Are you sure you want to Clean?',
clearText: "Clean",
edit: 'Edit',
index: 'Index',
keywordSearch: 'Please enter keyword',
@@ -26,7 +42,7 @@ const local: any = {
modify: 'Modify',
modifySuccess: 'Modify Success',
noData: 'No Data',
operate: 'Operate',
operate: 'Action',
pleaseCheckValue: 'Please check whether the value is valid',
refresh: 'Refresh',
reset: 'Reset',
@@ -37,11 +53,94 @@ const local: any = {
update: 'Update',
updateSuccess: 'Update Success',
userCenter: 'User Center',
setemail:'Bind Email',
resetpwd:'Reset Password',
yesOrNo: {
yes: 'Yes',
no: 'No'
},
ms: 'ms',
normal: 'Nomal',
abnormal: 'Abnormal',
export: 'Export',
loading: 'Loading',
selectPlease: 'please select',
disable: 'Disable',
default: 'Default',
system:'System',
record: 'Record',
noRecord: 'No Record',
msgSuccess: 'Success {msg}',
errorFields: 'Please fill in the required information in {num} correctly!',
tipTitle: 'Prompt',
units: {
second: 'Second',
minute: 'Minute',
hour: 'Hour',
day: 'Day',
week: 'Week',
month: 'Month',
year: 'Year',
core: 'Core',
},
tablePaginationTotal: 'Total {total} items',
inputPlease: 'Please input',
time:{
second: 's',
minute: 'Min',
hour: 'Hour',
day: 'Day'
}
},
// 组件
components: {
CronModal: {
title: "Cron Expression Generator",
addon: "Expression Preview:",
day1: "Every day",
day21: "Execute every",
day22: "days, starting from the",
day23: "day",
day31: "Cycle from",
day32: "to",
day33: "days",
day4: "Designated day (optional)",
day5: "Last day of the month",
hour1: "Hourly",
hour21: "Execute every",
hour22: "hours, starting from the",
hour23: "hour",
hour31: "Cycle time from",
hour32: "to",
hour33: "hours",
hour4: "Specified hours (multiple options available)",
minute1: "Every minute",
minute21: "Execute every",
minute22: "minutes, starting from the",
minute23: "minute",
minute31: "Cycle time from",
minute32: "to",
minute33: "minutes",
minute4: "Specified minutes (multiple options available)",
month1: "Every month",
month21: "Execute every",
month22: "months, starting from the",
month23: "month",
month31: "Cycle time from",
month32: "to",
month33: "months",
month4: "Specified months (multiple options available)",
second1: "Every second",
second21: "Execute every",
second22: "seconds, starting from the",
second23: "second",
second31: "Cycle time from",
second32: "to",
second33: "seconds",
second4: "Specify the number of seconds (multiple selectable)",
},
},
request: {
logout: 'Logout user after request failed',
logoutMsg: 'User status is invalid, please log in again',
@@ -137,7 +236,7 @@ const local: any = {
403: 'No Permission',
404: 'Page Not Found',
500: 'Server Error',
home: 'Home',
home: 'Dashboard',
'user-center': 'User Center',
about: 'About',
function: 'System Function',
@@ -162,7 +261,9 @@ const local: any = {
manage_dept: 'Dept Manage',
manage_route: 'Route Manage',
manage_post: 'Post Manage',
manage_dict: 'Dict Manage'
manage_dict: 'Dict Manage',
manage_dictData:'Dict Data',
manage_task_log: 'Task Log',
},
page: {
login: {
@@ -210,6 +311,8 @@ const local: any = {
agreeTerms: 'I have read and agree to the User Agreement and Privacy Policy',
code:'Code',
password:'Password',
newPassword:'New Password',
oldPassword:'Old Password',
confirmPassword:'ConfirmPassword',
basicInfo: 'BasicInfo',
terms: 'Terms',
@@ -228,6 +331,18 @@ const local: any = {
birthDate: 'Birth Date',
birthDatePlaceholder: 'Please select birth date',
birthDateRequired: 'Please select birth date',
genderRequired:'Please select gender',
usernameLengthLimit:"Username is too short",
usernameExists:"The username is already registered",
usernameRequired:"The username cannot be empty",
phoneInvalid:"The mobile phone number is incorrect",
phoneExists:"The mobile phone number has been registered",
emailInvalid:"The email is incorrect",
emailExists:"The email has been registered",
emailRequired:"The email cannot be empty",
codeRequired:"The code cannot be empty",
passwordRequired:"The password cannot be empty",
passwordLength:"The password is too short",
},
resetPwd: {
title: 'Reset Password'
@@ -239,7 +354,7 @@ const local: any = {
about: {
title: 'About',
introduction: `Soybean Admin is an elegant and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite5, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. Soybean Admin provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.`,
introduction: `Admin is an elegant and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite5, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. WANFI Admin provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.`,
projectInfo: {
title: 'Project Info',
version: 'Version',
@@ -269,11 +384,11 @@ const local: any = {
projectNews: {
title: 'Project News',
moreNews: 'More News',
desc1: 'Soybean created the open source project soybean-admin on May 28, 2021!',
desc2: 'zyh submitted a bug to soybean-admin, the multi-tab bar will not adapt.',
desc3: 'Soybean is ready to do sufficient preparation for the release of soybean-admin!',
desc4: 'Soybean is busy writing project documentation for soybean-admin!',
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
desc1: 'created the open source project WANFI-admin on May 28, 2021!',
desc2: 'zyh submitted a bug to WANFI-admin, the multi-tab bar will not adapt.',
desc3: 'WANFI is ready to do sufficient preparation for the release of WANFI-admin!',
desc4: 'WANFI is busy writing project documentation for WANFI-admin!',
desc5: 'WANFI just wrote some of the workbench pages casually, and it was enough to see!'
},
creativity: 'Creativity'
},
@@ -348,6 +463,7 @@ const local: any = {
password: 'Password',
post: 'Post',
role: 'Role',
createtime:'Create time',
form: {
userName: 'Please enter username',
email: 'Please enter email',
@@ -462,9 +578,665 @@ const local: any = {
remark: 'Please enter remark'
},
addDict: 'Add Dictionary',
editDict: 'Edit Dictionary'
editDict: 'Edit Dictionary',
dictDataTitle:'Dictionary Data',
dictData:{
dictType: "Dictionary name",
dictCode: "Data Code",
dictLabel: "Data Key",
dictLabelPleac: "Please enter the data key correctly",
dictValue: "Data Value",
dictValuePleac: "Please enter the data value correctly",
dictSort: "Data Sort",
status: "Status",
createTime: "CreateTime",
tagType: "Tag Type",
tagTypeShow: "Tag Type Show",
tagClass: "Tag Class",
remark: "Remark",
viewInfo: "Dictionary Data Info",
viewInfoErr: "Failed to get dictionary data information",
addInfo: "Add Dictionary Data",
editInfo: "Modifying Dictionary Data",
delTip: "Confirm deleting the data item with dictionary data code [{txt}]?",
delOk: "Deleted successfully",
exportTip: "Confirm exporting xlsx table files based on search criteria?",
exportOk: "Completed export",
typeDataErr: "Failed to get dictionary type information",
}
},
log:{
logId:'Log ID',
module:'Module Title',
operType:'Business Type',
operName:'Operator',
operIp:'Operation Address',
operArea:'Operation Location',
operStatus:'Status',
operTime:'Operation Time',
useTime:'Time Consumption',
backUser:'Back User',
phoneUser:'Phone User',
other:'Other',
operUrl:'Operation URL',
operParam:'Operation Parameters',
jsonResult:"Result",
title:"Log Management",
},
task:{
title:'Task Management',
taskId:'ID',
taskName:'Name',
logTitle:'Task Log',
group:'Group',
invoke:'Invoke',
cron:'Cron',
status:'Status',
log:'Log',
createTime:'Create Time',
targetParams:'Arguments',
remark:'Remark',
viewJob:'View Job',
addJob:'Add Job',
editJob:'Edit Job',
viewInfoErr:'Failed to get job information',
getInfoError:'Get Info Error',
jobNamePlease: 'Please enter the task name correctly, limited to 2-50 characters',
invokeTargetPlease: 'Please enter the call target correctly, limited to 2-50 characters.',
cronExpressionPlease: 'Please enter or generate a cron execution expression',
jobGroup: "Job Group",
saveLog: "Save Log",
invokeTargetTip: "Parameter description: support for preset incoming parameters, serialized processing parameters in the processor",
cronExpressionTip: "Example of an expression:0/20 * * * * ?",
cronExpressionTip1: "Illustrative example: Execute tasks every 20 seconds",
cronExpressionNew: "Generating Expression",
targetParamsPlease: 'Call target incoming parameters, only support json strings',
delTip: "Are you sure you want to delete task number [{num}]?",
delLogTip: "Are you sure you want to delete Log number [{num}]?",
runOneTip: "Are you sure you want to perform a [{num}] task right away?",
runOneOk: "{num} Executed successfully",
runOne:'execute once',
jobLog: "Task Log",
jobMessage: "Task Message",
clearTip: "Confirm that all dispatch log data items are cleared?",
clearOk: "Cleared Successfully",
statusChange: "Are you sure you want the {text} {num} job?",
}
}
},
carddata:{
sitelist:'Site List',
sitename:'NAME',
country:'COUNTRY/REGION',
alert:'ALERTS',
gateway:'GATEWAY',
switches:'SWITCHES',
clients:'CLIENTS',
search:'Search site name',
total:'Total',
addsite:'Add New Site',
region:'Country/Region',
timezone:'TimeZone',
scenario:'Scenario',
username:'Username',
password:'Password',
deleteConfirmTitle:'Are you sure to delete',
deleteConfirmContent:'Are you sure to delete {name}?',
confirm:'Confirm',
cancel:'Cancel',
deleteSuccess:'Delete Success',
editsite:'Edit Site',
office:'Office',
hotel:'Hotel',
education:'Education',
retail:'Retail',
other:'Other',
updateSuccess:'Update Success',
wireless:'Wireless Features',
autoFailover:'Auto Failover',
conDetection:'Connectivity Detection',
autoRec:'Auto(Recommended)',
custom:'Custom',
fastRoaming:'Fast Roaming',
aiRoaming:'AI Roaming',
passwordInvalid:'The password format is incorrect, please include uppercase and lowercase letters and symbols.',
addSuccess:'Add success',
meshtext:'If the IP Address of the Wireless Device is in different network segments from the Custom IP Address, the Wireless Device will use the default gateway IP address for the detection.'
},
headerbanner:{
controller:'Controller Overview',
connected:'Connected',
cloud:'Cloud Access',
sites:'Sites',
totalsite:'Total Sites',
gateway:'Gateways',
switches:'Switches',
register:'Register Users',
online:'Online Users',
client:'Client',
alerts:'Alerts',
connect:'Connected',
disconnect:'Disconnected',
registuser:'Register',
onlineuser:'Online',
isolated:'Isolated',
wired:'Wired client',
wireless:'Wireless client',
guest:'Wireless Guest',
alert:'Alerts',
},
bill:{
username:'User Name',
billtime:'Order Time',
billtype:'Order Type',
packagebill:'Package',
rechargebill:'Recharge',
packagename:'Package Name',
amount:'Amount',
status:'Status',
wait:'Pending',
done:'Paid',
close:'Canceled',
billinfo:'Billing information',
total:'Total',
pleusername:'Please enter username',
plebilltype:'Please select order type',
plestatus:'Please select order status',
reset:'Reset',
search:'Search',
},
apdevice:{
aptitle:'AP Device Management',
online:'Online',
outline:'Offline',
pending:'Pending',
apname:'Device Name',
ip:'IP',
mac:'MAC',
model:'Model',
uptime:'Online Time',
status:'Status',
total:'Total',
pledevice:'Please enter device name',
plemac:'Please enter MAC address',
search:'Search',
reset:'Reset',
confirmReboot:'Confirm Restart',
confirmForget:'Confirm Forget',
forgetConfirmContent:'Sure to forget { name }?',
rebootConfirmContent:'Sure to restart { name }?',
rebootSuccess:'Send to restart success',
editConfig:'Edit config',
forget:'Forget device',
restart:'Restart device',
adopt:'Adopt device',
addDevice:'Add device',
sn:'Serial number',
name:'Device name(optional)',
username:'Device username(optional)',
password:'Device password(optional)',
snFormatError:'A-Z or 13-bit characters from 0-9',
nameFormatError:"Cannot start with space, +、-、{test}, = characters,cannot end with space, the length is between 1-128",
forgetConfirm:'Are you sure you want to forget your device?',
restartConfirm:'Are you sure you want to restart your device?',
led:'LED',
useSiteSettings:'Use Site settings',
on:'On',
off:'Off',
longitude:'Longitude(optional)',
latitude:'Latitude(optional)',
address:'Address(optional)',
plesearch:'Search Devicename,IP,Model',
selectSite:'No site',
adoptSuccess:'Adopt success',
adoptError:'Adopt error',
forgetSuccess:'Forget success',
forgetError:'Forget error',
getConfigError:'Get config error',
snRequired:'Serial number',
addSuccess:'Add success',
addError:'Add error',
longitudeError:'Longitude error',
latitudeError:'latitude error',
configSuccess:'Config success',
configError:'config error',
},
wlan:{
title:'WLAN',
selectSite:'No Site',
selectGroup:'No Group',
total:'Total',
name:'SSID Name',
none:'None',
passwordPlaceholder:'Please enter an 8~63-digit password',
passwordRequired:'The password cannot be empty',
passwordLength:'Please enter 8~63 characters or numbers for the password',
security: {
security:'Security',
wpaEnterprise: 'WPA-Enterprise',
wpaPersonal: 'WPA-Personal',
ppskWithoutRadius: 'PPSK without RADIUS',
ppskWithRadius: 'PPSK with RADIUS',
},
password:'Password',
portalEnable:'Portal',
portalName:'Portal Name',
band:'Band',
bandtip:'Only 8 SSIDs can exist for each band type at 2.4 GHz, 5 GHz, and 6 GHz',
guestNetwork:'Guest Network',
enable:'Enable',
disable:'Disable',
deviceType:'Device Type',
broadcast:'SSID Broadcast',
vlan:'VLAN',
addSsid:'Add SSID',
open:'Open',
vlanId:'VLAN ID',
default:'Default',
custom:'Custom',
nameRequired:'Name can not be empty',
nameLength:'Length of name is 1~32 characters',
vlanIdRequired:'VLAN ID can not be empty',
addSuccess:'Add success',
confirmDelete:'Delete device',
deleteConfirmContent:'Do you want to delete the device?',
deleteSuccess:'Delete success',
editSsid:'Edit SSID',
updateSuccess:'Update success',
},
portal: {
title: 'Portal management',
name: 'Name',
selectSite:'no site',
authType: 'Authentication Type',
auth: {
none: 'No Authentication',
simplePassword: 'Simple password',
externalRadius: 'RADIUS server',
externalPortal: 'External Portal server',
hotspot: 'Hotspot',
ldap: 'LDAP'
},
addPortal: 'Add Portal',
enable: 'Enable',
ssid: 'SSID',
selectSsid: 'Please select SSID',
authTimeout: 'Authentication Timeout',
dailyLimit: 'Daily Limit',
enableDailyLimit: 'Enable',
httpsRedirect: 'HTTPS Redirection',
enableHttpsRedirect: 'Enable',
landingPage: 'Landing Page',
landingUrl: 'URL',
enterUrl: 'Please enter URL',
nameRequired: 'Please enter portal name',
ssidRequired: 'Please select SSID',
urlRequired: 'Please enter URL',
addSuccess: 'Add Success',
addError: 'Add Error',
total:'Total',
updateSuccess:'Update success',
editPortal:'Edit Portal',
timeUnit: {
min: 'Min',
hour: 'Hour',
day: 'Day'
},
landing: {
originalUrl: 'The Original URL',
promotionUrl: 'The Promotional URL',
logoutPage: 'The Success Page'
},
externalPortal: 'External Portal server',
ipAddress: 'IP Address',
url: 'URL',
ipRequired: 'Please enter IP',
invalidUrl: 'The URL is not formatted correctly',
confirmDelete: 'Confirm Delete',
deleteConfirmContent: 'Sure to delete portal {name} ?',
deleteSuccess: 'Delete success'
},
alerts:{
alerts:'Alerts',
choosesite: 'Please select a site',
deletesuccess: 'Delete successful',
deletefail: 'Delete failed',
choserecord: 'Please select the record to operate on',
deletetitle: 'Confirm deletion',
deletebody: 'record(s)?',
suredelete: 'Are you sure you want to delete this alarm record?',
sure: 'Confirm',
cancel: 'Cancel',
unresolved: 'Unresolved',
resolved: 'Resolved',
all: 'All',
system: 'System',
device: 'Device',
batchdelete: 'Batch delete',
resovetitle: 'Mark as resolved',
delete: 'Delete',
type:'TYPE',
level:'LEVEL',
content:'CONTENT',
time:'TIME',
action:'ACTION',
},
access:{
title:'Access Control',
plesite:'please select site',
authaccess:'Pre-Authentication Access',
enable:'Enable',
authaccesslist:'Pre-Authentication Access List',
delete:'Delete',
authclient:'Authentication-Free Client',
authclientlist:'Authentication-Free Client List',
apply:'Apply',
cancel:'Cancel',
type:'TYPE',
pleurl:'Please select URL',
addauth:'Add Pre-Authentication Access',
addclient:'Add Authentication-Free Client',
add:'Add',
info:'INFORMATION',
action:'ACTION',
deletetitle:'Delete',
deletebody:'Are you sure you want to delete this record?',
ok:'confirm',
plenumber:'Please enter a valid number',
subnetMask:'Subnet mask must be between 0-32',
iperror:'ip is not right',
applysuccess:'Apply Success',
},
terminal:{
title:'Terminal',
total:'Total',
unknow:'Unknow',
day:'Day',
hour:'Hour',
min:'min',
second:'s',
name:'Device Name',
ip:'IP',
status:'Status',
ssid:'SSID',
apname:'Affiliated AP device',
activity:'Rate',
down:'Traffic Down',
up:'Traffic Up',
uptime:'Online Time',
pledevice:'Please enter device name',
model:'Model',
plemodel:'Please enter type of device',
mac:'MAC',
plemac:'Please enter MAC address',
search:'Search',
reset:'Reset',
pending:'Pending',
authorized:'Authorized',
authfree:'AUTH-FREE',
},
user:{
title:'User Information',
username:'User Name',
fullname:'Full Name',
sex:'Gender',
man:'Male',
woman:'Female',
birthdate:'Birth Date',
age:'Age ',
email:'Email',
phone:'Phone',
kyc:'KYC',
ver:'Verified',
unver:'Unverified',
pending:'Pending',
rejected:'Rejected',
pleusername:'Please enter username',
pleemail:'Please enter email address',
search:'Search',
reset:'Reset',
},
cdr:{
hour:'hour',
min:'min',
sec:'s',
username:'User Name',
clientname:'Client Name',
clientmac:'Client MAC',
up:'Traffic Up',
down:'Traffic Down',
start:'Start Time',
end:'End Time',
duration:'Duration',
title:'CDR Records',
total:'Total',
pleusername:'Please enter username',
pleclientname:'Please enter device name',
search:'Search',
reset:'Reset',
timerange:'End time range',
startTime:'StartTime',
endTime:'EndTime'
},
kyc:{
title:'KYC Authentication Review',
total:'Total',
pass:'Pass',
refuse:'Refuse',
verified:'Verified',
unverified:'Unverified',
pending:'Pending',
approved:'Approved',
rejected:'Rejected',
unknow:'unknow',
username:'User Name',
realname:'Real Name',
type:'Identification type',
file:'Identification photograph',
picture:'Facial photograph',
status:'Status',
createtime:'Create Time',
operate:'Action',
confirmtitle:'Confirmation approved.',
confirmcontent:'Are you sure you want to verify this user\'s real-name authentication?',
confirmsuc:'Successfully approved.',
confirmerr:'Operation failed.',
rejecttitle:'Confirm rejection',
rejectcontent:'Are you sure you want to reject this user\'s real-name authentication?',
rejectpla:'Please enter the reason for refusal.',
rejectsuc:'successfully rejected',
searchtime:'Submission Time',
starttime:'Start Time',
endtime:'End Time',
plestatus:'Please select status.',
search:'Search',
reset:'Reset',
},
rule:{
title:'Configure Billing Rules',
edit:'Edit',
traffic:'Traffic',
pletraffic:'Please enter flow rate',
price:'Price',
pleprice:'Please enter price',
unit:'Unit',
pleunit:'Please select unit',
status:'Status',
close:'Cancel',
confirm:'Confirm',
modaltitle:'Edit rule',
use:'Enable',
unuse:'Not enabled',
action:'Action',
geterr:'Failed to get the billing rule',
updatesuc:'Update successful',
updateerr:'Update failed',
currencySettings: 'Currency Settings',
currencySelect: 'Select Currency',
currentCurrency: 'Current Currency',
currencyName: 'Currency Name',
currencyCode: 'Currency Code',
currencySymbol: 'Currency symbol',
paymentMethods:'Pay Methods',
},
ratelimit:{
title:'Rate limit management',
change:'Modify rate limit configuration',
add:'New rate limit configuration.',
name:'Limit Name',
plename:'Please enter rate limit name.',
upratelimit:'Up Rate Limit',
useuplimit:'Enable upstream rate limiting.',
uplimit:'Upstream bandwidth',
pleuplimit:'Please enter upstream bandwidth.',
downratelimit:'Down Rate Limit',
usedownlimit:'Enable downstream rate limit',
downlimit:'Downstream bandwidth',
pledownlimit:'Please enter downstream bandwidth.',
reject:'Please enter bandwidth value greater than 0.',
confirmdelete:'Confirm deletion.',
deletecontant:'It is confirmed to delete the rate limit configuration',
deletesuc:'Deletion successful.',
idnull:'The editor ID cannot be empty.',
changesuc:'Modification successful.',
addsuc:'Addition successful.',
changeerror:'Modification failed.',
adderror:'Failed to add.',
unlimit:'Unlimited',
operate:'Action',
changed:'Edit',
delete:'Delete',
},
package: {
title: 'Package Management',
add: 'Package',
packagename: 'Package Name',
plepackagename: 'Please enter package name',
period: 'Billing Cycle',
pleperiod: 'Please enter cycle number',
pleunit:'Please enter the billing period unit',
price: 'Price',
pleprice: 'Please enter price',
traffic: 'Traffic Limit',
pletraffic: 'Please enter traffic',
limit: 'Bandwidth Limit',
plelimit: 'Please select bandwidth limit configuration',
duration: 'Duration Limit',
pleduration: 'Please enter duration',
client: 'Device Limit',
pleclient: 'Please enter number of devices',
unit: 'Units',
usepackage: 'Package Status',
day: 'Day',
month: 'Month',
year: 'Year',
unlimit: 'Unlimited',
up: 'Upload',
down: 'Download',
status:'Status',
use: 'Enable',
unuse: 'Disable',
operate: 'Action',
edit: 'Edit',
delete: 'Delete',
hour: 'Hour',
second:'s',
rejtraffic: 'Please enter a traffic value greater than 0 and less than 1025',
rejduration: 'Please enter a duration value greater than 0',
rejclient: 'Please enter a device number greater than 0',
idnull: 'Edit ID cannot be empty',
editsuc: 'Modified successfully',
addsuc: 'Added successfully',
editerr: 'Modification failed',
adderr: 'Add failed',
confirmdelete: 'Confirm Delete',
deletecontent: 'Are you sure to delete this package?',
confirm: 'Confirm',
close: 'Cancel',
deletesuc: 'Deleted successfully',
deleteerr: 'Delete failed',
},
dict:{
dictname:'Dictionary Name',
dicttype:'Dictionary type',
status:'Status',
remark:'Remarks',
createtime:'Creation time',
operate:'Action',
deleteask:'Do you confirm the deletion?',
title:'Dictionary list',
},
menu:{
menuname:'Name',
path:'Path',
component:'Component',
status:'Status',
nomal:'Normal',
disable:'Disable',
menutype:'Type',
table:'Directory',
menu:'Menu',
button:'Button',
ordernum:'Sort',
icon:'Icon',
createtime:'Create Time',
operate:'Action',
edit:'Edit',
delete:'Delete',
modaltitle:'Tips',
deleteask:'Are you sure you want to delete the menu?',
deletesuc:'Deletion successful.',
title:'Menu List',
addmenu:'Add Menu',
editmenu:'Edit Menu',
root:'Root node',
parent:'Parent',
islink:'Is backlink',
yes:'Yes',
no:'No',
menucomponent:'Path',
hidemenu:'Hide Menu',
cache:'Cache',
perms:'Permission Identifier',
confirm:'Determine',
close:'Cancel',
nozero:'The sort must be greater than or equal to 0'
},
resetPwd:{
title:'Change your password',
byEmail:'Change by email',
byPassword:'Change by old password',
email:'Email',
getCode:'Get code',
code:'Code',
oldPassword:'Old password',
newPassword:'New password',
confirmPassword:'confirm password',
emailPlaceholder:'Please enter the email',
codePlaceholder:'Please enter the code',
passwordPlaceholder:'Please enter new password',
oldPasswordPlaceholder:'Please enter old password',
confirmPasswordPlaceholder:'Please enter again',
submit:'Confirm',
emailRequired:'Email can not be empty',
codeSent:'Code enter',
success:'Reset Success',
},
email:{
title:'Bind Email',
currentEmail:'Current Email',
email:'Email',
code:'Code',
getCode:'Get Code',
submit:'Submit',
codeSent:'Code Sent',
codeRequired:'Code can not be empty',
updateSuccess:'Update Success',
updateFailed:'Update Failed',
emailRequired:'Email can not be empty',
emailInvalid:'Email Invalid',
codeLength:'Code Invalid',
},
},
form: {
required: 'Cannot be empty',
@@ -478,7 +1250,7 @@ const local: any = {
},
pwd: {
required: 'Please enter password',
invalid: '6-18 characters, including letters, numbers, and underscores'
invalid: 'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits'
},
confirmPwd: {
required: 'Please enter password again',
@@ -511,7 +1283,8 @@ const local: any = {
expand: 'Expand Menu',
pin: 'Pin',
unpin: 'Unpin'
}
},
...viewEn
};
export default local;
+786 -12
View File
@@ -1,3 +1,13 @@
const viewZh: any = {
"view.dashboard":"仪表盘",
"view.endpoint": "终端设备",
"view.endpoint_access": "当前设备",
"view.endpoint_records": "历史设备",
"view.billing": "账单",
"view.billing_histories": "历史查询",
"view.set-meal": "套餐",
};
const local:any = {
system: {
title: 'WANFi 平台',
@@ -14,9 +24,15 @@ const local:any = {
columnSetting: '列设置',
config: '配置',
confirm: '确认',
submit:'确定',
delete: '删除',
view:'查看',
exportOk: "已完成导出",
exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
deleteSuccess: '删除成功',
confirmDelete: '确认删除吗?',
confirmClean: '确认清空吗?',
clearText: "清空",
edit: '编辑',
index: '序号',
keywordSearch: '请输入关键词搜索',
@@ -37,11 +53,94 @@ const local:any = {
update: '更新',
updateSuccess: '更新成功',
userCenter: '个人中心',
setemail:'绑定邮箱',
resetpwd:'修改密码',
yesOrNo: {
yes: '是',
no: '否'
},
ms: '毫秒',
normal: '正常',
abnormal: '异常',
export:'导出',
loading: '加载中...',
selectPlease: '请选择',
disable: '停用',
default: '默认',
system:'系统',
record: '记录',
noRecord: '不记录',
msgSuccess: '{msg} 成功',
errorFields: '请正确填写 {num} 处必填信息!',
tipTitle: '提示',
units: {
second: '秒',
minute: '分钟',
hour: '小时',
day: '天',
week: '周',
month: '月',
year: '年',
core: '核',
},
tablePaginationTotal: '总共 {total} 条',
inputPlease: '请输入',
time:{
second: '秒',
minute: '分钟',
hour: '小时',
day: '天'
}
},
// 组件
components: {
CronModal: {
title: "Cron表达式生成",
addon: "表达式预览:",
day1: "每一天",
day21: "每隔",
day22: "天执行一次,从",
day23: "日开始",
day31: "周期从",
day32: "到",
day33: "日",
day4: "指定日(可多选)",
day5: "本月最后一天",
hour1: "每一小时",
hour21: "每隔",
hour22: "小时执行一次,从",
hour23: "时开始",
hour31: "周期从",
hour32: "到",
hour33: "小时",
hour4: "指定小时(可多选)",
minute1: "每一分钟",
minute21: "每隔",
minute22: "分钟执行一次,从",
minute23: "分钟开始",
minute31: "周期从",
minute32: "到",
minute33: "分钟",
minute4: "指定分钟(可多选)",
month1: "每一月",
month21: "每隔",
month22: "月执行,从",
month23: "月开始",
month31: "周期从",
month32: "到",
month33: "月之间的每个月",
month4: "指定月(可多选)",
second1: "每一秒钟",
second21: "每隔",
second22: "秒执行一次,从",
second23: "秒开始",
second31: "周期从",
second32: "到",
second33: "秒",
second4: "指定秒数(可多选)",
}
},
request: {
logout: '请求失败后登出用户',
logoutMsg: '用户状态失效,请重新登录',
@@ -137,7 +236,7 @@ const local:any = {
403: '无权限',
404: '页面不存在',
500: '服务器错误',
home: '首页',
home: '仪表盘',
'user-center': '个人中心',
about: '关于',
function: '系统功能',
@@ -162,7 +261,9 @@ const local:any = {
manage_dept: '部门管理',
manage_route: '路由管理',
manage_post: '岗位管理',
manage_dict: '字典管理'
manage_dict: '字典管理',
manage_dictData:'字典数据',
manage_task_log: '任务日志',
},
page: {
login: {
@@ -210,6 +311,8 @@ const local:any = {
agreeTerms: '我已阅读并同意用户协议和隐私政策',
code:'验证码',
password:'密码',
newPassword:'新密码',
oldPassword:'旧密码',
confirmPassword:'再次输入密码',
basicInfo: '基本信息',
terms: '协议条款',
@@ -228,6 +331,18 @@ const local:any = {
birthDate: '出生日期',
birthDatePlaceholder: '请选择出生日期',
birthDateRequired: '请选择出生日期',
genderRequired:'请选择性别',
usernameLengthLimit:"用户名太短",
usernameExists:"用户名已经注册",
usernameRequired:"用户名不能为空",
phoneInvalid:"手机号格式不正确",
phoneExists:"手机号已经注册",
emailInvalid:"邮箱格式不正确",
emailExists:"邮箱已经注册",
emailRequired:"邮箱不能为空",
codeRequired:"验证码不能为空",
passwordRequired:"密码不能为空",
passwordLength:"密码太短",
},
resetPwd: {
title: '重置密码'
@@ -239,7 +354,7 @@ const local:any = {
about: {
title: '关于',
introduction: `Soybean Admin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。Soybean Admin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
introduction: `WANFI Admin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。WANFI Admin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
projectInfo: {
title: '项目信息',
version: '版本',
@@ -269,11 +384,11 @@ const local:any = {
projectNews: {
title: '项目动态',
moreNews: '更多动态',
desc1: 'Soybean 在2021年5月28日创建了开源项目 soybean-admin!',
desc2: 'soybean-admin 提交了一个bug,多标签栏不会自适应。',
desc3: 'Soybean 准备为 soybean-admin 的发布做充分的准备工作!',
desc4: 'Soybean 正在忙于为soybean-admin写项目说明文档!',
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
desc1: 'WANFI 在2021年5月28日创建了开源项目 WANFI-admin!',
desc2: 'WANFI-admin 提交了一个bug,多标签栏不会自适应。',
desc3: 'WANFI 准备为 WANFI-admin 的发布做充分的准备工作!',
desc4: 'WANFI 正在忙于为WANFI-admin写项目说明文档!',
desc5: 'WANFI 刚才把工作台页面随便写了一些,凑合能看了!'
},
creativity: '创意'
},
@@ -348,6 +463,7 @@ const local:any = {
password: '密码',
role: '角色',
post: '岗位',
createtime:'创建时间',
form: {
userName: '请输入用户名',
email: '请输入电子邮件',
@@ -462,9 +578,666 @@ const local:any = {
remark: '请输入备注'
},
addDict: '新增字典',
editDict: '编辑字典'
editDict: '编辑字典',
dictDataTitle:'字典数据',
dictData:{
dictType: "字典名称",
dictCode: "数据代码",
dictLabel: "数据键名",
dictLabelPleac: "请正确输入数据键名",
dictValue: "数据键值",
dictValuePleac: "请正确输入数据键值",
dictSort: "数据排序",
status: "数据状态",
createTime: "创建时间",
tagType: "标签类型",
tagTypeShow: "回显预览",
tagClass: "样式属性",
remark: "数据说明",
viewInfo: "字典数据信息",
viewInfoErr: "获取字典数据信息失败",
addInfo: "添加字典数据",
editInfo: "修改字典数据",
delTip: "确认删除字典数据代码为 【{txt}】 的数据项?",
delOk: "删除成功",
exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
exportOk: "已完成导出",
typeDataErr: "获取字典类型信息失败",
}
},
log:{
logId:'日志编号',
module:'模块标题',
operType:'业务类型',
operName:'操作人员',
operIp:'操作地址',
operArea:'操作地点',
operStatus:'操作状态',
operTime:'操作日期',
useTime:'消耗时间',
backUser:'后台用户',
phoneUser:'手机用户',
other:'其他',
operUrl:'请求URL',
operParam:'请求参数',
jsonResult:"结果",
title:"日志管理",
},
task:{
title:'任务管理',
taskId:'编号',
taskName:'任务名称',
logTitle:'任务日志',
group:'任务组名',
invoke:'调用目标',
cron:'cron表达式',
status:'状态',
log:'记录日志',
createTime:'创建时间',
targetParams:'传入参数',
remark:'备注',
viewJob:'查看任务',
addJob:'新增任务',
editJob:'编辑任务',
viewInfoErr:'查看异常信息',
getInfoError:'获取信息失败',
jobNamePlease: '请正确输入任务名称,限2-50个字符',
invokeTargetPlease: '请正确输入调用目标,限2-50个字符',
cronExpressionPlease: '请输入或生成cron执行表达式',
jobGroup: "任务组名",
saveLog: "记录日志",
invokeTargetTip: "参数说明:支持预设传入参数,在处理器中进行序列化处理参数",
cronExpressionTip: "表达式示例:0/20 * * * * ?",
cronExpressionTip1: "示例说明:每20秒执行任务",
cronExpressionNew: "生成表达式",
targetParamsPlease: '调用目标传入参数,仅支持json字符串',
delTip: "确认删除定时任务编号为 【{num}】 任务吗?",
delLogTip: "确认删除日志编号为 【{num}】 任务日志吗?",
runOneTip: "确定要立即执行一次 【{num}】 任务吗?",
runOneOk: "{num} 执行成功",
runOne:'执行一次',
jobLog: "任务日志",
jobMessage: "执行信息",
clearTip: "确认清空所有调度日志数据项吗?",
clearOk: "清空成功",
statusChange: "确定要{text} {num} 任务吗?",
}
}
},
carddata:{
sitelist:'站点列表',
sitename:'站点名称',
country:'国家/地区',
alert:'警告',
gateway:'网关',
switches:'交换机',
clients:'终端',
search:'输入站点名称',
total:'共',
addsite:'添加站点',
region:'国家/地区',
timezone:'时区',
scenario:'场景',
username:'用户名',
password:'密码',
deleteConfirmTitle:'确定要删除吗',
deleteConfirmContent:'确定要删除站点{name}吗?',
confirm:'确定',
cancel:'取消',
deleteSuccess:'删除成功',
editsite:'修改配置',
office:'办公室',
hotel:'医院',
education:'教育',
retail:'零售业',
other:'其他',
updateSuccess:'更新成功',
wireless:'无线功能',
autoFailover:'自动恢复',
conDetection:'连接检测',
autoRec:'自动(建议选项)',
custom:'自定义',
fastRoaming:'快速漫游',
aiRoaming:'AI漫游',
passwordInvalid:'密码格式不正确,请包含大小写字母和符号',
addSuccess:'添加成功',
meshtext:'如果 Wireless Device 的 IP 地址与自定义 IP 地址位于不同的网段,则 Wireless Device 将使用默认网关 IP 地址进行检测'
},
headerbanner:{
controller:'控制仪表盘',
connected:'连接',
cloud:'云访问',
sites:'站点',
totalsite:'站点数',
gateway:'网关',
switches:'交换机',
register:'注册用户',
online:'在线用户',
client:'装置',
alerts:'警告',
connect:'连接数',
disconnect:'断连数',
registuser:'注册用户数',
onlineuser:'在线用户数',
isolated:'隔离',
wired:'有线客户端',
wireless:'无线客户端',
guest:'无线访客',
alert:'警报数',
},
bill:{
username:'用户名',
billtime:'订单时间',
billtype:'订单类型',
packagebill:'套餐订单',
rechargebill:'充值订单',
packagename:'套餐名称',
amount:'金额',
status:'状态',
wait:'待支付',
done:'已支付',
close:'已取消',
billinfo:'账单信息',
total:'共',
pleusername:'请输入用户名',
plebilltype:'请选择订单类型',
plestatus:'请选择订单状态',
reset:'重置',
search:'查询',
},
apdevice:{
aptitle:'AP设备管理',
online:'在线',
outline:'离线',
pending:'待定',
apname:'设备名称',
ip:'IP地址',
mac:'MAC地址',
model:'型号',
uptime:'在线时间',
status:'状态',
total:'共',
pledevice:'请输入设备名称',
plemac:'请输入MAC地址',
search:'搜索',
reset:'重置',
editConfig:'修改配置',
forget:'移除设备',
restart:'重启设备',
confirmReboot:'确定重启',
confirmForget:'确定移除',
forgetConfirmContent:'确定要移除设备{ name }吗',
rebootConfirmContent:'确定要重启设备{ name }吗',
rebootSuccess:'重启发送成功',
adopt:'纳管设备',
addDevice:'增加设备',
sn:'产品序号',
name:'设备名称',
username:'设备用户名',
password:'设备密码',
snFormatError:'A-Z 或 0-9 的 13 位字符',
nameFormatError:'不能以空格、+、-、{test}、=字符开头,不能以空格结尾,长度在 1-128 之间',
forgetConfirm:'确认要移除设备吗?',
restartConfirm:'确认要重启设备吗?',
led:'LED',
useSiteSettings:'使用站点设置',
on:'打开',
off:'关闭',
longitude:'经度(可选)',
latitude:'纬度(可选)',
address:'地址(可选)',
plesearch:'搜索设备名字,站点名字,IP,型号',
selectSite:'无站点',
adoptSuccess:'纳管成功',
adoptError:'纳管失败',
forgetSuccess:'移除设备成功',
forgetError:'移除设备失败',
getConfigError:'获取配置失败',
snRequired:'产品序号',
addSuccess:'添加成功',
addError:'添加失败',
longitudeError:'经度错误',
latitudeError:'纬度错误',
configSuccess:'配置成功',
configError:'配置失败',
},
wlan:{
title:'无线网络',
selectSite:'无站点',
selectGroup:'无网络组',
total:'共',
name:'SSID名称',
none:'无',
passwordPlaceholder:'请输入8~63位的密码',
passwordRequired:'密码不能为空',
passwordLength:'密码请输入长度8~63位字符或数字',
security: {
security:'安全性',
wpaEnterprise: 'WPA-Enterprise',
wpaPersonal: 'WPA-Personal',
ppskWithoutRadius: 'PPSK without RADIUS',
ppskWithRadius: 'PPSK with RADIUS',
},
password:'密码',
portalEnable:'门户',
portalName:'门户名称',
band:'频段',
bandtip:'2.4GHz、5GHz、6GHz每种Band类型只能存在8个ssid',
guestNetwork:'访客网络',
enable:'开启',
disable:'关闭',
deviceType:'设备类型',
broadcast:'广播',
vlan:'VLAN',
addSsid:'SSID配置',
open:'开启',
vlanId:'VLAN ID',
default:'默认',
custom:'定制',
nameRequired:'名称不能为空',
nameLength:'名称长度1~32字符',
vlanIdRequired:'VLAN ID不能为空',
addSuccess:'添加成功',
confirmDelete:'删除设备',
deleteConfirmContent:'确认要删除设备吗',
deleteSuccess:'删除成功',
editSsid:'修改配置',
updateSuccess:'修改成功',
},
portal: {
title: '门户管理',
name: '名称',
selectSite:'无站点',
authType: '身份验证类型',
auth: {
none: '无认证',
simplePassword: '简单密码',
externalRadius: '外部 Radius 服务器',
externalPortal: '外部 Portal 服务器',
hotspot: '热点',
ldap: 'LDAP 格式'
},
addPortal: '添加门户',
enable: '启用',
ssid: 'SSID',
selectSsid: '请选择SSID',
authTimeout: '身份验证时间',
dailyLimit: '每日限额',
enableDailyLimit: '启用每日限额',
httpsRedirect: 'HTTPS 重定向',
enableHttpsRedirect: '启用 HTTPS 重定向',
landingPage: '登录页面',
landingUrl: '促销 URL',
enterUrl: '请输入 URL',
nameRequired: '请输入门户名称',
ssidRequired: '请选择 SSID',
urlRequired: '请输入 URL',
addSuccess: '添加成功',
addError: '添加失败',
total:'共',
updateSuccess:'更新成功',
editPortal:'修改配置',
timeUnit: {
min: '分钟',
hour: '小时',
day: '天'
},
landing: {
originalUrl: '重定向到原始 URL',
promotionUrl: '重定向到促销 URL',
logoutPage: '重定向到注销页面'
},
externalPortal: '自定义 Portal 服务器',
ipAddress: 'IP地址',
url: 'URL',
ipRequired: '请输入IP地址',
invalidUrl: 'URL 格式不正确',
confirmDelete: '确认删除',
deleteConfirmContent: '确定要删除门户 {name} 吗?',
deleteSuccess: '删除成功'
},
alerts:{
alerts:'警报',
chosesite:'请选择站点',
deletesuccess:'删除成功',
deletefail:'删除失败',
choserecord:'请选择需要操作的记录',
deletetitle:'确认删除',
deletebody:'条记录吗?',
suredelete:'确定要删除该条警报记录吗?',
sure:'确定',
cancel:'取消',
unresolved:'未解决',
resolved:'已解决',
all:'全部',
system:'系统',
device:'设备',
batchdelete:'批量删除',
resovetitle:'标记为已解决',
delete:'删除',
type:'类型',
level:'等级',
content:'说明',
time:'时间',
action:'操作',
},
access:{
title:'白名单控制',
plesite:'请选择站点',
authaccess:'预认证访问',
enable:'开启',
authaccesslist:'预认证访问列表',
delete:'删除',
authclient:'免认证客户端',
authclientlist:'免认证客户端列表',
apply:'提交',
cancel:'取消',
type:'类型',
pleurl:'请输入URL',
addauth:'添加预认证访问',
addclient:'添加免认证客户端',
add:'添加',
info:'信息',
action:'操作',
deletetitle:'确认删除',
deletebody:'确定要删除这条记录吗?',
ok:'确认',
plenumber:'请输入有效数字',
subnetMask:'子网掩码必须在0-32之间',
iperror:'ip地址格式不正确',
applysuccess:'提交成功',
},
terminal:{
title:'终端设备',
total:'共',
unknow:'未知',
day:'天',
hour:'小时',
min:'分钟',
second:'秒',
name:'设备名称',
ip:'IP地址',
status:'状态',
ssid:'网络',
apname:'所属AP设备',
activity:'下载速率',
down:'下载量',
up:'上传量',
uptime:'在线时长',
pledevice:'请输入设备名称',
model:'设备类型',
plemodel:'请输入设备类型',
mac:'MAC地址',
plemac:'请输入MAC地址',
search:'搜索',
reset:'重置',
pending:'未授权',
authorized:'已授权',
authfree:'免认证',
},
user:{
title:'用户信息',
username:'用户名',
fullname:'姓名',
sex:'性别',
man:'男',
woman:'女',
birthdate:'出生日期',
age:'年龄',
email:'邮箱',
phone:'手机号',
kyc:'KYC验证',
ver:'已验证',
unver:'未验证',
pending:'待审核',
rejected:'未批准',
pleusername:'请输入用户名',
pleemail:'请输入邮箱',
search:'搜索',
reset:'重置',
},
cdr:{
hour:'时',
min:'分',
sec:'秒',
username:'用户名',
clientname:'设备名',
clientmac:'设备MAC地址',
up:'上行流量',
down:'下行流量',
start:'开始时间',
end:'结束时间',
duration:'时长',
title:'话单记录',
total:'共',
pleusername:'请输入用户名',
pleclientname:'请输入设备名',
search:'搜索',
reset:'重置',
timerange:'结束时间范围',
startTime:'开始时间',
endTime:'结束时间'
},
kyc:{
title:'KYC实名认证审核',
total:'共',
pass:'通过',
refuse:'拒绝',
verified:'已认证',
unverified:'未认证',
pending:'待审核',
approved:'已通过',
rejected:'已拒绝',
unknow:'未知',
username:'用户名',
realname:'姓名',
type:'证件类型',
file:'证件照片',
picture:'面部照片',
status:'状态',
createtime:'提交时间',
operate:'操作',
confirmtitle:'确认通过',
confirmcontent:'确定要通过该用户的实名认证吗?',
confirmsuc:'审核通过成功',
confirmerr:'操作失败',
rejecttitle:'确认拒绝',
rejectcontent:'确定要拒绝该用户的实名认证吗?',
rejectpla:'请输入拒绝原因',
rejectsuc:'审核拒绝成功',
searchtime:'提交时间',
starttime:'开始时间',
endtime:'结束时间',
plestatus:'请选择状态',
search:'查询',
reset:'重置',
},
rule:{
title:'计费规则配置',
edit:'编辑',
traffic:'流量',
pletraffic:'请输入流量大小',
price:'价格',
pleprice:'请输入价格',
unit:'单位',
pleunit:'请选择单位',
status:'状态',
close:'取消',
confirm:'确定',
modaltitle:'编辑规则',
use:'启用',
unuse:'未启用',
action:'操作',
geterr:'获取计费规则失败',
updatesuc:'更新成功',
updateerr:'更新失败',
currencySettings: '货币设置',
currencySelect: '选择货币',
currentCurrency: '当前货币',
currencyName: '货币名称',
currencyCode: '货币代码',
currencySymbol: '货币符号',
paymentMethods:'支付方式',
},
ratelimit:{
title:'限速管理',
change:'修改限速配置',
add:'新增限速配置',
name:'限速名称',
plename:'请输入限速名称',
upratelimit:'上行限速',
useuplimit:'启用上行限速',
uplimit:'上行带宽',
pleuplimit:'请输入上行带宽',
downratelimit:'下行限速',
usedownlimit:'启用下行限速',
downlimit:'下行带宽',
pledownlimit:'请输入下行带宽',
reject:'请输入大于0的带宽值',
confirmdelete:'确认删除',
deletecontant:'确定要删除限速配置',
deletesuc:'删除成功',
idnull:'编辑ID不能为空',
changesuc:'修改成功',
addsuc:'添加成功',
changeerror:'修改失败',
adderror:'添加失败',
unlimit:'无限制',
operate:'操作',
changed:'编辑',
delete:'删除',
},
package:{
title:'套餐管理',
add:'新增套餐',
packagename:'套餐名称',
plepackagename:'请输入套餐名称',
period:'计费周期',
pleperiod:'请输入周期数',
pleunit:'请输入计费周期单位',
price:'价格',
pleprice:'请输入价格',
traffic:'流量限制',
pletraffic:'请输入流量',
limit:'带宽限制',
plelimit:'请选择带宽限速配置',
duration:'时长限制',
pleduration:'请输入时长',
client:'设备数限制',
pleclient:'请输入设备数',
unit:'台',
usepackage:'套餐启用',
day:'天',
month:'月',
year:'年',
unlimit:'无限制',
up:'上行',
down:'下行',
status:'套餐状态',
use:'启用',
unuse:'禁用',
operate:'操作',
edit:'编辑',
delete:'删除',
hour:'小时',
second:'秒',
rejtraffic:'请输入大于0小于1025的流量值',
rejduration:'请输入大于0的时长值',
rejclient:'请输入大于0的设备数',
idnull:'编辑ID不能为空',
editsuc:'修改成功',
addsuc:'添加成功',
editerr:'修改失败',
adderr:'添加失败',
confirmdelete:'确认删除',
deletecontent:'是否确认删除该套餐?',
confirm:'确认',
close:'取消',
deletesuc:'删除成功',
deleteerr:'删除失败',
},
dict:{
dictname:'字典名称',
dicttype:'字典类型',
status:'状态',
remark:'备注',
createtime:'创建时间',
operate:'操作',
deleteask:'确认删除吗?',
title:'字典列表',
},
menu:{
menuname:'菜单名称',
path:'路由地址',
component:'组件路径',
status:'状态',
nomal:'正常',
disable:'停用',
menutype:'类型',
table:'目录',
menu:'菜单',
button:'按钮',
ordernum:'排序',
icon:'图标',
createtime:'创建时间',
operate:'操作',
edit:'编辑',
delete:'删除',
modaltitle:'提示',
deleteask:'确定删除该菜单吗',
deletesuc:'删除成功',
title:'菜单列表',
addmenu:'新增菜单',
editmenu:'编辑菜单',
root:'根节点',
parent:'上级菜单',
islink:'是否外链',
yes:'是',
no:'否',
menucomponent:'菜单路径',
hidemenu:'隐藏菜单',
cache:'缓存',
perms:'权限标识',
confirm:'确定',
close:'取消',
nozero:'排序必须大于等于0'
},
resetPwd:{
title:'修改密码',
byEmail:'通过邮箱修改密码',
byPassword:'通过原密码修改密码',
email:'邮箱',
getCode:'获取验证码',
code:'验证码',
oldPassword:'原密码',
newPassword:'新密码',
confirmPassword:'确认密码',
emailPlaceholder:'请输入邮箱',
codePlaceholder:'请输入验证码',
passwordPlaceholder:'请输入新密码',
oldPasswordPlaceholder:'请输入旧密码',
confirmPasswordPlaceholder:'请再次输入新密码',
submit:'确认',
emailRequired:'邮箱不能为空',
codeSent:'验证码发送',
success:'修改成功',
},
email:{
title:'绑定邮箱',
currentEmail:'当前邮箱',
email:'新邮箱',
code:'验证码',
getCode:'获取验证码',
submit:'确定',
codeSent:'验证码发送',
codeRequired:'验证码不能为空',
updateSuccess:'修改成功',
updateFailed:'修改失败',
emailRequired:'邮箱不能为空',
emailInvalid:'邮箱格式错误',
codeLength:'验证码长度错误',
},
},
form: {
required: '不能为空',
@@ -478,7 +1251,7 @@ const local:any = {
},
pwd: {
required: '请输入密码',
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线'
invalid: '密码至少包含大小写字母、数字、特殊符号,且不少于6位'
},
confirmPwd: {
required: '请输入确认密码',
@@ -511,7 +1284,8 @@ const local:any = {
expand: '展开菜单',
pin: '固定',
unpin: '取消固定'
}
},
...viewZh
};
export default local;
+7 -4
View File
@@ -1,9 +1,12 @@
import zhCN from './langs/zh-cn';
import enUS from './langs/en-us';
import localeZhCN from './langs/zh-cn';
import localeEnUS from './langs/en-us';
const fullZhCN = { ...localeZhCN, ...zhCN };
const fullEnUS = { ...localeEnUS, ...enUS };
const locales: Record<App.I18n.LangType, App.I18n.Schema> = {
'zh-CN': zhCN,
'en-US': enUS
'zh-CN': fullZhCN,
'en-US': fullEnUS
};
export default locales;
+260 -141
View File
@@ -5,6 +5,11 @@
import type { GeneratedRoute } from '@elegant-router/types';
/**
* custom routes
*
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
export const generatedRoutes: GeneratedRoute[] = [
{
name: '403',
@@ -40,143 +45,14 @@ export const generatedRoutes: GeneratedRoute[] = [
}
},
{
name: 'about',
path: '/about',
component: 'layout.base$view.about',
name: 'dashboard',
path: '/dashboard',
component: 'layout.base$view.dashboard',
meta: {
title: 'about',
i18nKey: 'route.about',
icon: 'fluent:book-information-24-regular',
order: 10
}
},
{
name: 'function',
path: '/function',
component: 'layout.base',
meta: {
title: 'function',
i18nKey: 'route.function',
icon: 'icon-park-outline:all-application',
order: 6
},
children: [
{
name: 'function_hide-child',
path: '/function/hide-child',
meta: {
title: 'function_hide-child',
i18nKey: 'route.function_hide-child',
icon: 'material-symbols:filter-list-off',
order: 2
},
redirect: '/function/hide-child/one',
children: [
{
name: 'function_hide-child_one',
path: '/function/hide-child/one',
component: 'view.function_hide-child_one',
meta: {
title: 'function_hide-child_one',
i18nKey: 'route.function_hide-child_one',
icon: 'material-symbols:filter-list-off',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_three',
path: '/function/hide-child/three',
component: 'view.function_hide-child_three',
meta: {
title: 'function_hide-child_three',
i18nKey: 'route.function_hide-child_three',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_two',
path: '/function/hide-child/two',
component: 'view.function_hide-child_two',
meta: {
title: 'function_hide-child_two',
i18nKey: 'route.function_hide-child_two',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
}
]
},
{
name: 'function_multi-tab',
path: '/function/multi-tab',
component: 'view.function_multi-tab',
meta: {
title: 'function_multi-tab',
i18nKey: 'route.function_multi-tab',
icon: 'ic:round-tab',
multiTab: true,
hideInMenu: true,
activeMenu: 'function_tab'
}
},
{
name: 'function_request',
path: '/function/request',
component: 'view.function_request',
meta: {
title: 'function_request',
i18nKey: 'route.function_request',
icon: 'carbon:network-overlay',
order: 3
}
},
{
name: 'function_super-page',
path: '/function/super-page',
component: 'view.function_super-page',
meta: {
title: 'function_super-page',
i18nKey: 'route.function_super-page',
icon: 'ic:round-supervisor-account',
order: 5,
roles: ['R_SUPER']
}
},
{
name: 'function_tab',
path: '/function/tab',
component: 'view.function_tab',
meta: {
title: 'function_tab',
i18nKey: 'route.function_tab',
icon: 'ic:round-tab',
order: 1
}
},
{
name: 'function_toggle-auth',
path: '/function/toggle-auth',
component: 'view.function_toggle-auth',
meta: {
title: 'function_toggle-auth',
i18nKey: 'route.function_toggle-auth',
icon: 'ic:round-construction',
order: 4
}
}
]
},
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
meta: {
title: 'home',
title: 'dashboard',
i18nKey: 'route.home',
icon: 'mdi:monitor-dashboard',
order: 1
order: 0,
}
},
{
@@ -192,15 +68,258 @@ export const generatedRoutes: GeneratedRoute[] = [
}
},
{
name: 'user-center',
path: '/user-center',
component: 'layout.base$view.user-center',
name: 'static',
path: '/static',
component: 'layout.base',
meta: {
title: 'user-center',
title: 'static',
hideInMenu: true,
constant: true,
keepAlive: false,
i18nKey: 'route.user-center'
},
children: [
{
name: 'static_resetpwd',
path: '/static/resetpwd',
component: 'view.static_resetpwd',
meta: {
title: 'static_resetpwd',
hideInMenu: true,
i18nKey: 'common.resetpwd'
}
},
]
},
// 下面是指定角色才有
// {
// name: 'function',
// path: '/function',
// component: 'layout.base',
// meta: {
// title: 'function',
// i18nKey: 'route.function',
// icon: 'icon-park-outline:all-application',
// order: 1002,
// roles: ['super']
// },
// children: [
// {
// name: 'function_hide-child',
// path: '/function/hide-child',
// meta: {
// title: 'function_hide-child',
// i18nKey: 'route.function_hide-child',
// icon: 'material-symbols:filter-list-off',
// order: 2
// },
// redirect: '/function/hide-child/one',
// children: [
// {
// name: 'function_hide-child_one',
// path: '/function/hide-child/one',
// component: 'view.function_hide-child_one',
// meta: {
// title: 'function_hide-child_one',
// i18nKey: 'route.function_hide-child_one',
// icon: 'material-symbols:filter-list-off',
// hideInMenu: true,
// activeMenu: 'function_hide-child'
// }
// },
// {
// name: 'function_hide-child_three',
// path: '/function/hide-child/three',
// component: 'view.function_hide-child_three',
// meta: {
// title: 'function_hide-child_three',
// i18nKey: 'route.function_hide-child_three',
// hideInMenu: true,
// activeMenu: 'function_hide-child'
// }
// },
// {
// name: 'function_hide-child_two',
// path: '/function/hide-child/two',
// component: 'view.function_hide-child_two',
// meta: {
// title: 'function_hide-child_two',
// i18nKey: 'route.function_hide-child_two',
// hideInMenu: true,
// activeMenu: 'function_hide-child'
// }
// }
// ]
// },
// {
// name: 'function_multi-tab',
// path: '/function/multi-tab',
// component: 'view.function_multi-tab',
// meta: {
// title: 'function_multi-tab',
// i18nKey: 'route.function_multi-tab',
// icon: 'ic:round-tab',
// multiTab: true,
// hideInMenu: true,
// activeMenu: 'function_tab'
// }
// },
// {
// name: 'function_request',
// path: '/function/request',
// component: 'view.function_request',
// meta: {
// title: 'function_request',
// i18nKey: 'route.function_request',
// icon: 'carbon:network-overlay',
// order: 3
// }
// },
// {
// name: 'function_super-page',
// path: '/function/super-page',
// component: 'view.function_super-page',
// meta: {
// title: 'function_super-page',
// i18nKey: 'route.function_super-page',
// icon: 'ic:round-supervisor-account',
// order: 5,
// }
// },
// {
// name: 'function_tab',
// path: '/function/tab',
// component: 'view.function_tab',
// meta: {
// title: 'function_tab',
// i18nKey: 'route.function_tab',
// icon: 'ic:round-tab',
// order: 1
// }
// },
// {
// name: 'function_toggle-auth',
// path: '/function/toggle-auth',
// component: 'view.function_toggle-auth',
// meta: {
// title: 'function_toggle-auth',
// i18nKey: 'route.function_toggle-auth',
// icon: 'ic:round-construction',
// order: 4
// }
// }
// ]
// },
// {
// name: 'about',
// path: '/about',
// component: 'layout.base$view.about',
// meta: {
// title: 'about',
// i18nKey: 'route.about',
// icon: 'fluent:book-information-24-regular',
// order: 1003,
// roles: ['super']
// }
// },
{
name: 'dictData',
path: '/manage/dict/dictData',
component: 'layout.base$view.manage_dict_dictData',
meta: {
title: 'Dict Data',
i18nKey: 'route.manage_dictData',
icon: 'fluent:book-information-24-regular',
order: 1003,
hideInMenu: true
}
}
},
{
name: 'log',
path: '/manage/task/log',
component: 'layout.base$view.manage_task_log',
meta: {
title: 'Task Log',
i18nKey: 'route.manage_task_log',
icon: 'fluent:book-information-24-regular',
order: 1003,
hideInMenu: true
}
},
];
/**自定义静态菜单 */
export const customRoutes: GeneratedRoute[] = [
{
name: 'endpoint',
path: '/endpoint',
component: 'layout.base',
meta: {
title: '终端设备',
i18nKey: 'view.endpoint',
icon: 'icon-park-outline:all-application',
order: 11,
hideInMenu: true
},
children: [
{
name: 'endpoint_access',
path: '/endpoint/access',
component: 'view.endpoint_access',
meta: {
title: '当前',
i18nKey: 'view.endpoint_access',
icon: 'material-symbols:filter-list-off',
order: 1
},
},
{
name: 'endpoint_records',
path: '/endpoint/records',
component: 'view.endpoint_records',
meta: {
title: '历史',
i18nKey: 'view.endpoint_records',
icon: 'ic:round-supervisor-account',
order: 3,
}
},
]
},
{
name: 'billing',
path: '/billing',
component: 'layout.base',
meta: {
title: '账单',
i18nKey: 'view.billing',
icon: 'icon-park-outline:all-application',
order: 13,
hideInMenu: true
},
children: [
{
name: 'billing_histories',
path: '/billing/histories',
component: 'view.billing_histories',
meta: {
title: '历史查询',
i18nKey: 'view.billing_histories',
icon: 'material-symbols:filter-list-off',
order: 2
},
},
]
},
{
name: 'set-meal',
path: '/set-meal',
component: 'layout.base$view.set-meal',
meta: {
title: '套餐',
i18nKey: 'view.set-meal',
icon: 'icon-park-outline:all-application',
order: 15,
hideInMenu: true
}
},
]
+10 -21
View File
@@ -3,9 +3,9 @@
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteRecordRaw } from 'vue-router';
import type { ElegantConstRoute } from '@elegant-router/vue';
import type { RouteKey, RouteMap, RoutePath } from '@elegant-router/types';
import type {RouteRecordRaw} from 'vue-router';
import type {ElegantConstRoute} from '@elegant-router/vue';
import type {RouteKey, RouteMap} from '@elegant-router/types';
import BaseLayout from '@/layouts/base-layout/index.vue';
import BlankLayout from '@/layouts/blank-layout/index.vue';
@@ -84,14 +84,14 @@ function transformElegantRouteToVueRoute(
route.props = true;
}
const { name, path, component, children, ...rest } = route;
const {name, path, component, children, ...rest} = route;
const vueRoute = { name, path, ...rest } as RouteRecordRaw;
const vueRoute = {name, path, ...rest} as RouteRecordRaw;
try {
if (component) {
if (isSingleLevelRoute(route)) {
const { layout, view } = getSingleLevelRouteComponent(component);
const {layout, view} = getSingleLevelRouteComponent(component);
const singleLevelRoute: RouteRecordRaw = {
path,
@@ -198,9 +198,11 @@ const routeMap: RouteMap = {
'function_super-page': '/function/super-page',
'function_tab': '/function/tab',
'function_toggle-auth': '/function/toggle-auth',
'home': '/home',
'dashboard': '/dashboard',
'login': '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
'user-center': '/user-center'
'user-center': '/user-center',
'reset-pwd':'/reset-pwd',
'forgot-pwd':'/user-center/resetpwd'
};
/**
@@ -208,18 +210,5 @@ const routeMap: RouteMap = {
* @param name route name
*/
export function getRoutePath<T extends RouteKey>(name: T) {
console.log(name);
return routeMap[name];
}
/**
* get route name by route path
* @param path route path
*/
export function getRouteName(path: RoutePath) {
const routeEntries = Object.entries(routeMap) as [RouteKey, RoutePath][];
const routeName: RouteKey | null = routeEntries.find(([, routePath]) => routePath === path)?.[0] || null;
return routeName;
}
+4 -4
View File
@@ -30,12 +30,12 @@ export function createRouteGuard(router: Router) {
const loginRoute: RouteKey = 'login';
const noAuthorizationRoute: RouteKey = '403';
const isLogin = Boolean(localStg.get('token'));
const isLogin = Boolean(localStg.get('tokenSys'));
const needLogin = !to.meta.constant;
const routeRoles = to.meta.roles || [];
// const hasRole = authStore.userInfo.roles?.some(role => routeRoles.includes(role));
const hasAuth = authStore.isStaticSuper || !routeRoles.length;
const hasRole = authStore.userInfo.roles?.some(role => routeRoles.includes(role));
const hasAuth = !routeRoles.length || hasRole
const routeSwitches: CommonType.StrategicPattern[] = [
// if it is login route when logged in, then switch to the root page
@@ -147,7 +147,7 @@ async function checkRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
}
// if the auth route is not initialized, then initialize the auth route
const isLogin = Boolean(localStg.get('token'));
const isLogin = Boolean(localStg.get('tokenSys'));
// initialize the auth route requires the user to be logged in, if not, redirect to the login page
if (!isLogin) {
const loginRoute: RouteKey = 'login';
+1 -1
View File
@@ -4,7 +4,7 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
export const ROOT_ROUTE: CustomRoute = {
name: 'root',
path: '/',
redirect: '/home',
redirect: '/dashboard',
meta: {
title: 'root',
constant: true
+4 -55
View File
@@ -1,57 +1,6 @@
import type { ElegantConstRoute } from '@elegant-router/types';
import { generatedRoutes } from '../elegant/routes';
import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
/**
* custom routes
*
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
const customRoutes: any[] = [
{
name: 'exception',
path: '/exception',
component: 'layout.base',
meta: {
title: 'exception',
i18nKey: 'route.exception',
icon: 'ant-design:exception-outlined',
order: 7
},
children: [
{
name: 'exception_403',
path: '/exception/403',
component: 'view._builtin_403',
meta: {
title: 'exception_403',
i18nKey: 'route.exception_403',
icon: 'ic:baseline-block'
}
},
{
name: 'exception_404',
path: '/exception/404',
component: 'view._builtin_404',
meta: {
title: 'exception_404',
i18nKey: 'route.exception_404',
icon: 'ic:baseline-web-asset-off'
}
},
{
name: 'exception_500',
path: '/exception/500',
component: 'view._builtin_500',
meta: {
title: 'exception_500',
i18nKey: 'route.exception_500',
icon: 'ic:baseline-wifi-off'
}
}
]
}
];
import type {ElegantConstRoute} from '@elegant-router/types';
import {customRoutes, generatedRoutes} from '../elegant/routes';
import {transformElegantRoutesToVueRoutes} from '../elegant/transform';
/** create routes when the auth route mode is static */
export function createStaticRoutes() {
@@ -59,7 +8,7 @@ export function createStaticRoutes() {
const authRoutes: any[] = [];
[...customRoutes, ...generatedRoutes].forEach(item => {
[...generatedRoutes, ...customRoutes].forEach(item => {
if (item.meta?.constant) {
constantRoutes.push(item);
} else {
+560 -5
View File
@@ -13,14 +13,23 @@ export function fetchLogin(body: Api.Auth.LoginBody) {
data: body
});
}
//邮箱验证码接口
export function sendCaptcha(body:Api.Auth.EmailCaptcha){
export function sendCaptcha(body: Api.Auth.EmailCaptcha) {
return request({
url:`/system/email/code?email=${body.email}`,
method:'get',
})
url: `/system/email/code?email=${body.email}`,
method: 'get'
});
}
//验证注册
export function doCheckUserRepeat(body: Api.Auth.CheckBody) {
return request<boolean>({
url: '/auth/checkRepeat',
method: 'post',
data: body
});
}
//添加注册
export function fetchRegister(body: Api.Auth.RegisterBody) {
@@ -30,6 +39,7 @@ export function fetchRegister(body: Api.Auth.RegisterBody) {
data: body
});
}
/** logout */
export function doDeleteLogout() {
return request<App.Service.Response<null>>({
@@ -40,7 +50,7 @@ export function doDeleteLogout() {
/** Get user info */
export function doGetUserInfo() {
return request<Api.Auth.UserInfo>({ url: '/u/user/getInfo' });
return request<Api.Auth.UserInfo>({ url: '/system/user/getInfo' });
}
/**
@@ -74,3 +84,548 @@ export function doGetCheckCode() {
url: '/code'
});
}
/** Get rate limit list */
export function fetchRateLimitList() {
return request<Api.Auth.RateLimit[]>({
url: '/system/rateLimit/list',
method: 'get'
});
}
/** Add rate limit config */
export function addRateLimit(data: Api.Auth.RateLimitAdd) {
return request<any>({
url: '/system/rateLimit',
method: 'post',
data
});
}
/** Edit rate limit config */
export function editRateLimit(data: Api.Auth.RateLimitAdd & { id: number }) {
return request<any>({
url: '/system/rateLimit',
method: 'put',
data
});
}
/** Remove rate limit config */
export function removeRateLimit(ids: number | number[]) {
return request<any>({
url: `/system/rateLimit/${ids}`,
method: 'delete'
});
}
/** Get package list */
export function fetchPackageList() {
return request<Api.Auth.Package[]>({
url: '/system/package/list',
method: 'get'
});
}
/** Add package config */
export function addPackage(data: Api.Auth.PackageAdd) {
return request<any>({
url: '/system/package',
method: 'post',
data
});
}
/** 修改套餐 */
export function updatePackage(data: Api.Auth.PackageAdd & { id: string }) {
return request<any>({
url: '/system/package',
method: 'put',
data
});
}
/** 删除套餐 */
export function deletePackage(id: string) {
return request<any>({
url: `/system/package/${id}`,
method: 'delete'
});
}
/** Get CDR history list */
export function fetchCdrHistory(params: Api.Auth.CdrHistoryParams) {
return request<Api.Auth.CdrHistoryResponse>({
url: '/system/cdr/pageHistory',
method: 'get',
params
});
}
/** Get bill list */
export function fetchBillList(params: Api.Auth.BillParams) {
return request<Api.Auth.BillResponse>({
url: '/system/order/page',
method: 'get',
params
});
}
/** 获取AP设备列表 */
export function fetchApDeviceList(siteId: string, params: Omit<Api.Device.ApDeviceParams, 'siteId'>) {
return request<Api.Device.ApDeviceResponse>({
url: `/system/device/page/${siteId}`,
method: 'get',
params
});
}
/** 删除AP设备 */
export function deleteApDevices(ids: string) {
return request<any>({
url: `/system/device/${ids}`,
method: 'delete'
});
}
/** 获取终端设备列表 */
export function fetchTerminalList(params: Api.Device.TerminalDeviceParams) {
return request<Api.Device.TerminalDeviceResponse>({
url: '/system/client/list',
method: 'get',
params
});
}
/** 获取计费规则列表 */
export function fetchBillRuleList() {
return request<Api.Billing.BillRuleResponse>({
url: '/system/billRule/list',
method: 'get'
});
}
/** 更新计费规则 */
export function updateBillRule(data: Api.Billing.BillRuleUpdate) {
return request<any>({
url: '/system/billRule',
method: 'put',
data
});
}
/** 获取仪表盘概览数据 */
export function getDashboardOverview() {
return request<Api.DashboardOverview>({
url: '/system/dashboard/overview',
method: 'get'
});
}
/** 获取仪表盘站点列表 */
export function getDashboardSiteList(params: { pageNum: number; pageSize: number; searchKey?: string }) {
return request<Api.DashboardSiteResponse>({
url: '/system/dashboard/page',
method: 'get',
params
});
}
/** 获取KYC审核列表 */
export function fetchKycList(params: Api.Kyc.KycParams) {
return request<Api.Kyc.KycResponse>({
url: '/system/kyc/page',
method: 'get',
params
});
}
/** KYC审核通过 */
export function approveKyc(id: string, userId: number) {
return request<any>({
url: `/system/kyc/approve`,
method: 'put',
data: { id, userId }
});
}
/** KYC审核拒绝 */
export function rejectKyc(id: string, userId: number, reason: string) {
return request<any>({
url: `/system/kyc/reject`,
method: 'put',
data: {
id,
userId,
description: reason
}
});
}
/** 忘记 AP 设备 */
export function forgetApDevice(siteId: string, deviceMac: string) {
return request<any>({
url: `/system/device/forget/${siteId}/${deviceMac}`,
method: 'post'
});
}
/** 添加 AP 设备 */
export function addApDevice(siteId: string, params: Api.Device.AddApDeviceItem) {
// 确保始终发送包含 devices 数组的请求体
const requestData: Api.Device.AddApDeviceParams = {
devices: [params] // 即使 params 是空对象,也会发送 { devices: [{}] }
};
return request<any>({
url: `/system/device/add/${siteId}`,
method: 'post',
data: requestData
});
}
/** 重启 AP 设备 */
export function rebootApDevice(siteId: string, deviceMac: string) {
return request<any>({
url: `/system/device/reboot/${siteId}/${deviceMac}`,
method: 'post'
});
}
/** 获取站点列表 */
export function fetchSiteList(params: Api.Site.SiteParams) {
return request<Api.Site.SiteResponse>({
url: '/system/site/page',
method: 'get',
params: {
pageNum: params.pageNum,
pageSize: params.pageSize
}
});
}
/** 更新 AP 设备配置 */
export function updateApDeviceConfig(siteId: string, mac: string, config: Api.Device.ApDeviceConfigUpdate) {
return request<any>({
url: `/system/device/updateConfig/${siteId}/${mac}`,
method: 'post',
data: config
});
}
/** 获取 AP 设备配置 */
export function getApDeviceConfig(siteId: string, deviceMac: string) {
return request<Api.Device.ApDeviceConfigResponse>({
url: `/system/device/getConfig/${siteId}/${deviceMac}`,
method: 'post'
});
}
/** 纳管 AP 设备 */
export function adoptApDevice(siteId: string, deviceMac: string, params?: { username?: string; password?: string }) {
return request<any>({
url: `/system/device/startAdopt/${siteId}/${deviceMac}`,
method: 'post',
data: params
});
}
/** 获取 WLAN 群组列表 */
export function fetchWlanGroups(siteId: string) {
return request<Api.Wlan.WlanGroupResponse>({
url: `/system/wlan/group/${siteId}`,
method: 'get'
});
}
/** 获取无线网络列表 */
export function fetchWlanSsidList(siteId: string, wlanId: string, params: { pageNum: number; pageSize: number }) {
return request<Api.Wlan.WlanSsidResponse>({
url: `/system/wlan/ssid/${siteId}/${wlanId}`,
method: 'get',
params: {
pageNum: params.pageNum,
pageSize: params.pageSize
}
});
}
/** 添加无线网络 */
export function addWlanSsid(siteId: string, wlanId: string, params: Api.Wlan.AddWlanSsidParams) {
return request<any>({
url: `/system/wlan/ssid/${siteId}/${wlanId}`,
method: 'post',
data: params
});
}
/** 删除无线网络 */
export function deleteWlanSsid(siteId: string, wlanId: string, ssidId: string) {
return request<any>({
url: `/system/wlan/ssid/${siteId}/${wlanId}/${ssidId}`,
method: 'delete'
});
}
/** 获取无线网络配置 */
export function getWlanSsidConfig(siteId: string, wlanId: string, ssidId: string) {
return request<Api.Wlan.WlanSsidConfigResponse>({
url: `/system/wlan/ssid/${siteId}/${wlanId}/${ssidId}`,
method: 'get'
});
}
/** 修改无线网络配置 */
export function updateWlanSsid(siteId: string, wlanId: string, ssidId: string, params: Api.Wlan.AddWlanSsidParams) {
return request<any>({
url: `/system/wlan/ssid/${siteId}/${wlanId}/${ssidId}`,
method: 'put',
data: params
});
}
/** 获取门户列表 */
export function fetchPortalList(siteId: string, params: { pageNum: number; pageSize: number }) {
return request<Api.Portal.PortalResponse>({
url: `/system/portal/${siteId}`,
method: 'get',
params: {
pageNum: params.pageNum,
pageSize: params.pageSize
}
});
}
/** 获取 SSID 列表 */
export function fetchSsidList(siteId: string) {
return request<Api.Portal.SsidListResponse>({
url: `/system/wlan/ssids/${siteId}`,
method: 'get'
});
}
/** 添加门户 */
export function addPortal(siteId: string, params: Api.Portal.AddPortalParams) {
return request<any>({
url: `/system/portal/${siteId}`,
method: 'post',
data: params
});
}
/** 获取门户配置 */
export function getPortalConfig(siteId: string, portalId: string) {
return request<Api.Portal.AddPortalParams>({
url: `/system/portal/${siteId}/${portalId}`,
method: 'get'
});
}
/** 更新门户配置 */
export function updatePortalConfig(siteId: string, portalId: string, params: Api.Portal.AddPortalParams) {
return request<any>({
url: `/system/portal/${siteId}/${portalId}`,
method: 'put',
data: params
});
}
/** 删除门户 */
export function deletePortal(siteId: string, portalId: string) {
return request<any>({
url: `/system/portal/${siteId}/${portalId}`,
method: 'delete'
});
}
/** 首页邮箱重置密码 */
export function fetcodeReset(data:any) {
return request({
url: '/system/user/profile/forgotPwd',
method: 'put',
data
});
}
/** 获取邮箱验证码 */
export function getEmailCode(email: string) {
return request<any>({
url: `/system/email/code`,
method: 'get',
params: { email }
});
}
/** 重置密码 */
export function resetPassword(data: { email: string; code: string; password: string }) {
return request<any>({
url: '/system/user/profile/forgotPwd',
method: 'put',
data
});
}
/** 通过原密码修改密码 */
export function updatePasswordByOld(params: { oldPassword: string; newPassword: string }) {
return request<any>({
url: '/system/user/profile/updatePwd',
method: 'put',
params
});
}
/** 添加站点 */
export function addSite(data: Api.Site.AddSiteParams) {
return request<any>({
url: '/system/site',
method: 'post',
data
});
}
/** 删除站点 */
export function deleteSite(siteId: string) {
return request<any>({
url: `/system/site/${siteId}`,
method: 'delete'
});
}
/** 获取站点配置 */
export function getSiteConfig(siteId: string) {
return request<Api.Site.SiteConfig>({
url: `/system/site/${siteId}`,
method: 'get'
});
}
/** 更新站点配置 */
export function updateSite(siteId: string, data: Api.Site.UpdateSiteParams) {
return request<any>({
url: `/system/site/${siteId}`,
method: 'put',
data
});
}
/** 获取用户信息 */
export function getUserProfile() {
return request<any>({
url: '/system/user/profile',
method: 'get'
});
}
/** 更新用户信息 */
export function updateUserProfile(data: { email: string; code: string }) {
return request<any>({
url: '/system/user/profile',
method: 'put',
data
});
}
/** 更新支付配置 */
export function updatePaymentConfig(data: {
currency: string;
currencySymbol: string;
paymentMethods: string[];
}) {
return request<any>({
url: '/system/config/pay',
method: 'put',
data
});
}
/** 获取支付配置 */
export function getPaymentConfig() {
return request<{
currency: string;
currencySymbol: string;
paymentMethods: string[];
}>({
url: '/system/config/pay',
method: 'get'
});
}
// 获取告警数据
export function fetchAlertList(siteId: string, params: {
pageNum: number;
pageSize: number;
filters: {
timeStart?: string;
timeEnd?: string;
module?: string;
resolved?: boolean;
}
}) {
return request<any>({
url: `/system/log/alerts/${siteId}`,
method: 'get',
params: {
pageNum: params.pageNum,
pageSize: params.pageSize,
'filters.timeStart': params.filters.timeStart,
'filters.timeEnd': params.filters.timeEnd,
'filters.module': params.filters.module,
'filters.resolved': params.filters.resolved,
}
})
}
// 删除告警
export function deleteAlerts(siteId: string, data: {
selectType: 'include' | 'exclude' | 'all';
startTime: number;
endTime: number;
logs: number[];
}) {
return request<any>({
url: `/system/log/alerts/${siteId}`,
method: 'delete',
data
});
}
// 标记告警为已解决
export function markAlertsResolved(siteId: string, data: {
selectType: 'unresolved' | 'resolved' | 'all';
startTime: number;
endTime: number;
logs: string[];
}) {
return request<any>({
url: `/system/log/alerts/${siteId}`,
method: 'post',
data
})
}
// 获取白名单配置
export function fetchAccessControl(siteId: string) {
return request<any>({
url: `/system/access-control/${siteId}`,
method: 'get'
})
}
// 更新白名单配置
export function updateAccessControl(siteId: string, data: {
preAuthAccessEnable: boolean;
freeAuthClientEnable: boolean;
preAuthAccessPolicies: Array<{
type: number;
ip?: string;
url?: string;
}>;
freeAuthClientPolicies: Array<{
type: number;
clientIp?: string;
clientMac?: string;
}>;
}) {
return request<any>({
url: `/system/access-control/${siteId}`,
method: 'post',
data
});
}
// 获取 Mesh 配置
export function getMeshConfig(siteId: string) {
return request<any>({
url: `/system/site/${siteId}/mesh`,
method: 'get'
})
}
// 更新 Mesh 配置
export function updateMeshConfig(siteId: string, data: { meshEnable: boolean; autoFailoverEnable?: boolean }) {
return request<any>({
url: `/system/site/${siteId}/mesh`,
method: 'post',
data
})
}
// 获取 Roaming 配置
export function getRoamingConfig(siteId: string) {
return request<any>({
url: `/system/site/${siteId}/roaming`,
method: 'get'
})
}
// 更新 Roaming 配置
export function updateRoamingConfig(siteId: string, data: { fastRoamingEnable: boolean; noStickRoamingEnable?: boolean; aiRoamingEnable?: boolean }) {
return request<any>({
url: `/system/site/${siteId}/roaming`,
method: 'post',
data
})
}
+5 -5
View File
@@ -8,7 +8,7 @@ export type DeptFormType = Pick<
/** 获取部门列表 */
export function doGetDeptList(params: Api.SystemManage.DeptSearchParams) {
return request<Api.SystemManage.DeptList>({
url: '/u/dept/list',
url: '/system/dept/list',
params
});
}
@@ -20,7 +20,7 @@ export function doGetDeptList(params: Api.SystemManage.DeptSearchParams) {
*/
export function doGetDeptInfo(deptId: number) {
return request<Api.SystemManage.Dept>({
url: `/u/dept/${deptId}`
url: `/system/dept/${deptId}`
});
}
@@ -31,7 +31,7 @@ export function doGetDeptInfo(deptId: number) {
*/
export function doAddDept(body: DeptFormType) {
return request({
url: '/u/dept',
url: '/system/dept',
method: 'post',
data: body
});
@@ -44,7 +44,7 @@ export function doAddDept(body: DeptFormType) {
*/
export function doEditDept(body: DeptFormType & { deptId: number }) {
return request({
url: '/u/dept',
url: '/system/dept',
method: 'put',
data: body
});
@@ -57,7 +57,7 @@ export function doEditDept(body: DeptFormType & { deptId: number }) {
*/
export function doDeleteDept(deptId: string | number) {
return request({
url: `/u/dept/${deptId}`,
url: `/system/dept/${deptId}`,
method: 'delete'
});
}
+43 -5
View File
@@ -6,7 +6,7 @@ export type DictSubmitModel = Partial<
export const doGetDictList = (params: Api.SystemManage.DictSearchParams) => {
return request<Api.SystemManage.DictList>({
url: '/u/dict/type/list',
url: '/system/dict/type/list',
method: 'get',
params
});
@@ -14,7 +14,7 @@ export const doGetDictList = (params: Api.SystemManage.DictSearchParams) => {
export const doAddDict = (data: DictSubmitModel) => {
return request({
url: '/u/dict/type',
url: '/system/dict/type',
method: 'post',
data
});
@@ -22,7 +22,7 @@ export const doAddDict = (data: DictSubmitModel) => {
export const doEditDict = (data: DictSubmitModel) => {
return request({
url: '/u/dict/type',
url: '/system/dict/type',
method: 'put',
data
});
@@ -30,7 +30,45 @@ export const doEditDict = (data: DictSubmitModel) => {
export const doDeleteDict = (dictId: string | number) => {
return request({
url: `/u/dict/type/${dictId}`,
method: 'post'
url: `/system/dict/type/${dictId}`,
method: 'delete'
});
};
/**
* 查询字典类型详细
* @param dictId 字典编号
* @returns object
*/
export function doGetType(dictId: string | number) {
return request({
url: `/system/dict/type/${dictId}`,
method: 'get',
});
}
/**
* 字典数据列表(指定字典类型)
* @param dictType 字典类型
* @returns object
*/
export function getDictDataType(dictType: string) {
return request({
url: `/system/dict/data/type/${dictType}`,
method: 'get',
});
}
/**
* 获取字典选择框列表
* @param data 字典数据对象
* @returns object
*/
export function getDictOptionselect() {
return request({
url: '/system/dict/type/optionselect',
method: 'get',
});
}
+87
View File
@@ -0,0 +1,87 @@
import { request } from '../request';
export type DictSubmitModel = Partial<
Pick<Api.SystemManage.Dict, 'dictName' | 'dictType' | 'status' | 'remark' | 'dictId'>
>;
/**
* 字典数据列表导出
* @param query 查询参数
* @returns bolb
*/
export function exportData(query: Record<string, any>) {
return request({
url: '/system/dict/data/export',
method: 'post',
data: query,
responseType: 'blob',
});
}
/**
* 查询字典数据列表
* @param query 查询值
* @returns
*/
export function doGetdictDataList(query: Record<string, any>) {
return request({
url: '/system/dict/data/list',
method: 'get',
params: query,
});
}
/**
* 查询字典数据详细
* @param dictCode 字典代码值
* @returns object
*/
export function doGetDataInfo(dictCode: string | number) {
return request({
url: `/system/dict/data/${dictCode}`,
method: 'get',
});
}
/**
* 新增字典数据
* @param data 字典数据对象
* @returns object
*/
export function addData(data: Record<string, any>) {
return request({
url: '/system/dict/data',
method: 'post',
data: data,
});
}
/**
* 修改字典数据
* @param data 字典数据对象
* @returns object
*/
export function updateData(data: Record<string, any>) {
return request({
url: '/system/dict/data',
method: 'put',
data: data,
});
}
/**
* 删除字典数据
* @param dictCode 字典代码值
* @returns object
*/
export function delData(dictCode: string | number) {
return request({
url: `/system/dict/data/${dictCode}`,
method: 'delete',
});
}
+103
View File
@@ -0,0 +1,103 @@
import { request } from '../request';
export function doGetjobList(params?: Api.SystemManage.UserSearchParams) {
return request<Api.SystemManage.UserList>({
url: '/schedule/job/list',
method: 'get',
params
});
}
export function doGetjobInfo(paramsId: any) {
return request<any>({
url: `/schedule/job/${paramsId}`,
method: 'get',
});
}
/**
* 新增定时任务调度
* @param data 任务对象
* @returns object
*/
export function addJob(data: Record<string, any>) {
return request({
url: '/schedule/job',
method: 'post',
data: data,
});
}
/**
* 修改定时任务调度
* @param data 任务对象
* @returns object
*/
export function updateJob(data: Record<string, any>) {
return request({
url: '/schedule/job',
method: 'put',
data: data,
});
}
/**
* 删除调度日志
* @param jobIds 任务日志Id
* @returns object
*/
export function delJobLog(jobIds: string) {
return request({
url: `/schedule/job/${jobIds}`,
method: 'delete',
});
}
/**
* 定时任务立即执行一次
* @param jobId 任务ID
* @returns object
*/
export function runJob(jobId: string) {
return request({
url: `/schedule/job/run`,
method: 'put',
data: {jobId},
});
}
/**
* 任务状态修改
* @param jobId 任务ID
* @param status 变更状态值
* @returns
*/
export function changeJobStatus(
jobId: string | number,
status: string | number
) {
return request({
url: '/schedule/job/changeStatus',
method: 'put',
data: {
jobId,
status,
},
});
}
/**
* 定时任务调度列表导出
* @param query 查询参数
* @returns bolb
*/
export function exportJob() {
return request({
url: '/schedule/job/export',
method: 'post',
responseType: 'blob',
});
}
+50
View File
@@ -0,0 +1,50 @@
import { request } from '../request';
export function doGetjobLogList(params?: any) {
return request<Api.SystemManage.UserList>({
url: '/schedule/job/log/list',
method: 'get',
params
});
}
export function doGetjobLogInfo(paramsId: any) {
return request<any>({
url: `/schedule/job/log/${paramsId}`,
method: 'get',
});
}
export function doDeleteJobLog(jobLogId: any) {
return request({ url: `/schedule/job/log/${jobLogId.join(',')}`, method: 'delete' });
}
/**
* 清空调度日志
* @returns object
*/
export function doCleanJobLog() {
return request({
url: '/schedule/job/log/clean',
method: 'delete',
});
}
/**
* 定时任务调度日志列表导出
* @returns bolb
*/
export function exportJobLog(
) {
return request({
url: '/schedule/job/log/export',
method: 'post',
responseType: 'blob',
headers: {
"Authorization": "Bearer your_token_here",
"Accept": "application/json, text/plain, */*",
},
});
}
+19
View File
@@ -0,0 +1,19 @@
import { request } from '../request';
export function doGetlogList(params?: Api.SystemManage.UserSearchParams) {
return request<Api.SystemManage.UserList>({
url: '/system/operlog/list',
method: 'get',
params
});
}
export function doDeleteLog(userId: any) {
return request({ url: `/system/operlog/${userId.join(',')}`, method: 'delete' });
}
export function doExportLog() {
return request({ url: `/system/operlog/export`, method: 'post', responseType: 'blob'});
}
+8 -8
View File
@@ -7,13 +7,13 @@ export type MenuListQuery = Partial<
Api.SystemManage.CommonSearchParams;
export function doGetMenuList(params: MenuListQuery) {
return request<Api.SystemManage.MenuList>({ url: '/u/menu/list', method: 'get', params });
return request<Api.SystemManage.MenuList>({ url: '/system/menu/list', method: 'get', params });
}
/** get all pages */
export function fetchGetAllPages() {
return request<Pick<Api.SystemManage.Menu, 'menuId' | 'menuName' | 'path'>[]>({
url: '/u/menu/list',
url: '/system/menu/list',
method: 'get',
params: {
menuType: 'C'
@@ -24,7 +24,7 @@ export function fetchGetAllPages() {
/** get menu tree */
export function fetchGetMenuTree() {
return request<Api.SystemManage.MenuTree[]>({
url: '/u/menu/treeselect',
url: '/system/menu/treeselect',
method: 'get'
});
}
@@ -35,27 +35,27 @@ export function doGetRoleMenuList(roleId: number) {
checkedKeys: number[];
menus: Api.SystemManage.MenuTree[];
}>({
url: `/u/menu/roleMenuTreeselect/${roleId}`,
url: `/system/menu/roleMenuTreeselect/${roleId}`,
method: 'get'
});
}
/** add menu */
export function doAddMenu(data: MenuModelType) {
return request({ url: '/u/menu', method: 'post', data });
return request({ url: '/system/menu', method: 'post', data });
}
/** delete menu */
export function doDeleteMenu(menuId: number) {
return request({ url: `/u/menu/${menuId}`, method: 'delete' });
return request({ url: `/system/menu/${menuId}`, method: 'delete' });
}
/** get menu detail */
export function doGetMenuDetail(menuId: number) {
return request<MenuModelType>({ url: `/u/menu/${menuId}`, method: 'get' });
return request<MenuModelType>({ url: `/system/menu/${menuId}`, method: 'get' });
}
/** edit menu */
export function doEditMenu(data: MenuModelType) {
return request({ url: '/u/menu', method: 'put', data });
return request({ url: '/system/menu', method: 'put', data });
}
+5 -5
View File
@@ -6,7 +6,7 @@ export type PostSubmitModel = Partial<
export function doGetPostList(params: Api.SystemManage.PostSearchParams) {
return request<Api.SystemManage.PostList>({
url: '/u/post/list',
url: '/system/post/list',
method: 'get',
params
});
@@ -14,14 +14,14 @@ export function doGetPostList(params: Api.SystemManage.PostSearchParams) {
export function doGetPostDetail(postId: number) {
return request({
url: `/u/post/${postId}`,
url: `/system/post/${postId}`,
method: 'get'
});
}
export function doAddPost(data: PostSubmitModel) {
return request({
url: '/u/post',
url: '/system/post',
method: 'post',
data
});
@@ -29,7 +29,7 @@ export function doAddPost(data: PostSubmitModel) {
export function doEditPost(data: PostSubmitModel) {
return request({
url: '/u/post',
url: '/system/post',
method: 'put',
data
});
@@ -37,7 +37,7 @@ export function doEditPost(data: PostSubmitModel) {
export function doDeletePost(postId: string | number) {
return request({
url: `/u/post/${postId}`,
url: `/system/post/${postId}`,
method: 'delete'
});
}
+4 -4
View File
@@ -7,7 +7,7 @@ import { request } from '../request';
* @returns
*/
export function doPutRole(role: Api.SystemManage.Role) {
return request({ url: '/u/role', method: 'put', data: role });
return request({ url: '/system/role', method: 'put', data: role });
}
/**
@@ -17,7 +17,7 @@ export function doPutRole(role: Api.SystemManage.Role) {
* @returns
*/
export function doPostRole(role: Api.SystemManage.Role) {
return request({ url: '/u/role', method: 'post', data: role });
return request({ url: '/system/role', method: 'post', data: role });
}
/**
@@ -27,12 +27,12 @@ export function doPostRole(role: Api.SystemManage.Role) {
* @returns
*/
export function doDeleteRole(roleId: number | string) {
return request({ url: `/u/role/${roleId}`, method: 'delete' });
return request({ url: `/system/role/${roleId}`, method: 'delete' });
}
export function doGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
return request<Api.SystemManage.RoleList>({
url: '/u/role/list',
url: '/system/role/list',
method: 'get',
params
});
+1 -1
View File
@@ -7,7 +7,7 @@ export function fetchGetConstantRoutes() {
/** get user routes */
export function doGetUserRoutes() {
return request<Api.Route.MenuRoute[]>({ url: '/u/menu/getRouters' });
return request<Api.Route.MenuRoute[]>({ url: '/system/menu/getRouters' });
}
/**
+26 -11
View File
@@ -2,20 +2,28 @@ import { request } from '../request';
// user api
export function doPutUser(user: Api.Auth.User) {
return request({ url: '/u/user', method: 'put', data: user });
return request({ url: '/system/user', method: 'put', data: user });
}
export function doPostUser(user: Api.Auth.User) {
return request({ url: '/u/user', method: 'post', data: user });
return request({ url: '/system/user', method: 'post', data: user });
}
export function doDeleteUser(userId: number | string) {
return request({ url: `/u/user/${userId}`, method: 'delete' });
return request({ url: `/system/user/${userId}`, method: 'delete' });
}
export function doGetUserList(params?: Api.SystemManage.UserSearchParams) {
return request<Api.SystemManage.UserList>({
url: '/u/user/list',
//系统管理查看客户
export function doGetsysUserList(params?: { userName?: string; email?: string }) {
return request<Api.Common.PaginatingQueryRecord<Api.Auth.User>>({
url: '/system/user/list',
method: 'get',
params
});
}
//系统管理查看用户
export function doGetUserList(params?: { userName?: string; email?: string }) {
return request<Api.Common.PaginatingQueryRecord<Api.Auth.User>>({
url: '/system/user/listUser',
method: 'get',
params
});
@@ -29,26 +37,33 @@ export function doGetUserList(params?: Api.SystemManage.UserSearchParams) {
export function doGetUserPostsAndRoles(userId: number | string | undefined) {
if (!userId) {
return request<Api.SystemManage.UserPostsAndRoles>({
url: '/u/user/',
url: '/system/user/',
method: 'get'
});
}
return request<Api.SystemManage.UserPostsAndRoles>({
url: `/u/user/${userId}`,
url: `/system/user/${userId}`,
method: 'get'
});
}
export function doGetAdminUserPostsAndRoles() {
return request<Api.SystemManage.UserPostsAndRoles>({
url: `/u/user`,
url: `/system/user`,
method: 'get'
});
}
export function doGetUserDeptTree() {
return request<Api.Common.CommonTree>({
url: '/u/user/deptTree',
url: '/system/user/deptTree',
method: 'get'
});
}
export function doUpdatePwd(pwdJson: any) {
var url= `/system/user/resetPwd`;
return request({ url, method: 'put',data:pwdJson });
}
+2 -2
View File
@@ -25,11 +25,11 @@ export const request = createFlatRequest<App.Service.Response, InstanceState>(
const { headers } = config;
// set token
const token = localStg.get('token');
const token = localStg.get('tokenSys');
const Authorization = token ? `Bearer ${token}` : null;
// set language
const ContentLanguage = localStg.get('lang') || 'zh-CN';
const ContentLanguage = localStg.get('lang') || 'en-US';
Object.assign(headers, { Authorization, "Content-Language": ContentLanguage });
return config;
+3 -3
View File
@@ -11,11 +11,11 @@ import { fetchRefreshToken } from '../api/auth';
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
const { resetStore } = useAuthStore();
const refreshToken = localStg.get('refreshToken') || '';
const refreshToken = localStg.get('refreshTokenSys') || '';
const { error, data } = await fetchRefreshToken(refreshToken);
if (!error) {
localStg.set('token', data.access_token);
localStg.set('refreshToken', data.refreshToken);
localStg.set('tokenSys', data.access_token);
localStg.set('refreshTokenSys', data.refreshToken);
const config = { ...axiosConfig };
if (config.headers) {
+9 -1
View File
@@ -11,6 +11,7 @@ import { useRouteStore } from '../route';
import { useTabStore } from '../tab';
import { useThemeStore } from '../theme';
export const useAppStore = defineStore(SetupStoreId.App, () => {
const themeStore = useThemeStore();
const routeStore = useRouteStore();
@@ -44,7 +45,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
setReloadFlag(true);
}
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'en-US');
const localeOptions: App.I18n.LangOption[] = [
{
@@ -58,9 +59,16 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
];
function changeLocale(lang: App.I18n.LangType) {
if (locale.value === lang) return;
locale.value = lang;
setLocale(lang);
localStg.set('lang', lang);
//routeStore.updateGlobalMenusByLocale();
setTimeout(() => {
window.location.reload();
}, 100);
}
/** Update document title by locale */
+20 -21
View File
@@ -7,7 +7,7 @@ import { localStg } from '@/utils/storage';
import { $t } from '@/locales';
import { useRouteStore } from '../route';
import { clearAuthStorage, emptyInfo, getToken } from './shared';
import {sendCaptcha} from "@/service/api/auth";
import { doCheckUserRepeat, sendCaptcha } from '@/service/api/auth';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const routeStore = useRouteStore();
@@ -31,13 +31,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
}
);
/** is super role in static route */
const isStaticSuper = computed(() => {
const { VITE_AUTH_ROUTE_MODE, VITE_STATIC_SUPER_ROLE } = import.meta.env;
return VITE_AUTH_ROUTE_MODE === 'static' && userInfo.roles?.includes(VITE_STATIC_SUPER_ROLE);
});
/** Is login */
const isLogin = computed(() => Boolean(token.value));
@@ -48,7 +41,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
*/
async function resetStore(isMe = false) {
if (isMe) {
doDeleteLogout();
await doDeleteLogout();
}
const authStore = useAuthStore();
@@ -77,10 +70,10 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
startLoading();
const { data: loginToken, error } = await fetchLogin(loginForm);
if (!error) {
const pass = await loginByToken(loginToken);
onSuccess && onSuccess();
if (pass) {
await routeStore.initAuthRoute();
@@ -105,8 +98,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
async function loginByToken(loginToken: Api.Auth.LoginToken) {
// 1. stored in the localStorage, the later requests need it in headers
localStg.set('token', loginToken.access_token);
localStg.set('refreshToken', loginToken.refreshToken);
localStg.set('tokenSys', loginToken.access_token);
localStg.set('refreshTokenSys', loginToken.refreshToken);
token.value = loginToken.access_token;
const isSuccess = await refreshUserInfo();
@@ -122,6 +115,15 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
}
return false;
}
/**
* 检查用户信息是否已存在
*/
async function checkUserRepeat(checkForm: Api.Auth.CheckBody) {
const { data, error } = await doCheckUserRepeat(checkForm);
return { exists: data, error };
}
/**
* Register new user
*/
@@ -139,22 +141,18 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
endLoading();
return !error;
}
async function captcha(email:string){
async function captcha(email: string) {
if (!email) {
return;
}
try {
await sendCaptcha({ email }); // 这里调用后端接口发送验证码
} catch (error) {
return null;
}
const { data, error } = await sendCaptcha({ email }); // 这里调用后端接口发送验证码
return { data, error };
}
return {
token,
userInfo,
isStaticSuper,
isLogin,
loginLoading,
resetStore,
@@ -163,5 +161,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
refreshUserInfo,
register,
captcha,
checkUserRepeat
};
});
+3 -4
View File
@@ -2,18 +2,17 @@ import { localStg } from '@/utils/storage';
/** Get token */
export function getToken() {
return localStg.get('token') || '';
return localStg.get('tokenSys') || '';
}
export const emptyInfo: Api.Auth.UserInfo = {
roles: [],
buttons: [],
permissions: [],
user: null
};
/** Clear auth storage */
export function clearAuthStorage() {
localStg.remove('token');
localStg.remove('refreshToken');
localStg.remove('tokenSys');
localStg.remove('refreshTokenSys');
}
+62
View File
@@ -0,0 +1,62 @@
import { defineStore } from 'pinia';
/**字典参数类型 */
type DictStore = {
/**字典数据 */
dicts: Map<string, DictType[]>;
};
const useDictStore = defineStore('dict', {
state: (): DictStore => ({
dicts: new Map(),
}),
actions: {
/**清空字典 */
clearDict() {
this.dicts.clear();
},
/**删除字典 */
removeDict(key: string) {
if (!key) return;
return this.dicts.delete(key);
},
/**
* 处理字典数据对象用于回显标签
* @param data 字典数据项
* @returns
*/
parseDataDict(data: Record<string, any>) {
return [
{
label: data.dictLabel,
value: data.dictValue,
tagType: data.tagType,
tagClass: data.tagClass,
},
];
},
/**获取字典 */
async getDict(key: string) {
if (!key) return [];
let disct = this.dicts.get(key);
if (disct === undefined || disct.length === 0) {
const res = await getDictDataType(key);
if (!res.error && Array.isArray(res.data)) {
const dictData: DictType[] = res.data.map((d:any) => ({
label: d.dictLabel,
value: d.dictValue,
tagType: d.tagType,
tagClass: d.tagClass,
}));
this.dicts.set(key, dictData);
disct = dictData;
} else {
disct = [];
}
}
return disct;
},
},
});
export default useDictStore;
+28 -53
View File
@@ -1,16 +1,16 @@
import { computed, ref, shallowRef } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia';
import { useBoolean } from '@sa/hooks';
import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey } from '@elegant-router/types';
import { SetupStoreId } from '@/enum';
import { router } from '@/router';
import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
import { ROOT_ROUTE } from '@/router/routes/builtin';
import { getRoutePath } from '@/router/elegant/transform';
import { useAppStore } from '../app';
import { useAuthStore } from '../auth';
import { useTabStore } from '../tab';
import {computed, ref, shallowRef} from 'vue';
import type {RouteRecordRaw} from 'vue-router';
import {defineStore} from 'pinia';
import {useBoolean} from '@sa/hooks';
import type {CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey} from '@elegant-router/types';
import {SetupStoreId} from '@/enum';
import {router} from '@/router';
import {createStaticRoutes, getAuthVueRoutes} from '@/router/routes';
import {ROOT_ROUTE} from '@/router/routes/builtin';
import {getRoutePath} from '@/router/elegant/transform';
import {useAppStore} from '../app';
import {useAuthStore} from '../auth';
import {useTabStore} from '../tab';
import {
filterAuthRoutesByRoles,
getBreadcrumbsByRoute,
@@ -26,12 +26,12 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const appStore = useAppStore();
const authStore = useAuthStore();
const tabStore = useTabStore();
const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean();
const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean();
const {bool: isInitConstantRoute, setBool: setIsInitConstantRoute} = useBoolean();
const {bool: isInitAuthRoute, setBool: setIsInitAuthRoute} = useBoolean();
/** Home route key */
const routeHome = ref('home');
const routeHome = ref('dashboard');
/**
* Set route home
@@ -46,7 +46,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const authRoutes = shallowRef<ElegantConstRoute[]>([]);
function addAuthRoutes(routes: ElegantConstRoute[]) {
const authRoutesMap = new Map(authRoutes.value.map(route => [route.name, route]));
const {constantRoutes} = createStaticRoutes();
const authRoutesMap = new Map(constantRoutes.map(route => [route.name, route]));
routes.forEach(route => {
authRoutesMap.set(route.name, route);
@@ -146,17 +148,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
async function initConstantRoute() {
if (isInitConstantRoute.value) return;
// if (authRouteMode.value === 'static') {
const { constantRoutes } = createStaticRoutes();
const {constantRoutes} = createStaticRoutes();
addAuthRoutes(constantRoutes);
// } else {
// const { data, error } = await fetchGetConstantRoutes();
// if (!error) {
// addAuthRoutes(data);
// }
// }
handleAuthRoutes();
@@ -165,43 +159,24 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** Init auth route */
async function initAuthRoute() {
await initStaticAuthRoute();
await initDynamicAuthRoute();
tabStore.initHomeTab();
}
/** Init static auth route */
async function initStaticAuthRoute() {
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
if (authStore.isStaticSuper) {
addAuthRoutes(staticAuthRoutes);
} else {
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles ?? []);
addAuthRoutes(filteredAuthRoutes);
}
handleAuthRoutes();
setIsInitAuthRoute(true);
}
/** Init dynamic auth route */
async function initDynamicAuthRoute() {
const { data: routes, error } = await doGetUserRoutes();
const {data: routes, error} = await doGetUserRoutes();
if (!error) {
addAuthRoutes(routes);
const {authRoutes: staticAuthRoutes} = createStaticRoutes();
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles ?? []);
addAuthRoutes(filteredAuthRoutes.concat(routes));
handleAuthRoutes();
setRouteHome('home');
setRouteHome('dashboard');
handleUpdateRootRouteRedirect('manage_role');
handleUpdateRootRouteRedirect('dashboard');
setIsInitAuthRoute(true);
} else {
await authStore.resetStore();
}
tabStore.initHomeTab();
}
/** handle auth routes */
@@ -249,7 +224,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const redirect = getRoutePath(redirectKey);
if (redirect) {
const rootRoute: CustomRoute = { ...ROOT_ROUTE, redirect };
const rootRoute: CustomRoute = {...ROOT_ROUTE, redirect};
router.removeRoute(rootRoute.name);
+1 -1
View File
@@ -57,7 +57,7 @@ function sortRouteByOrder(route: ElegantConstRoute) {
* @param routes routes
*/
export function sortRoutesByOrder(routes: ElegantConstRoute[]) {
routes.sort((next, prev) => (Number(next.meta?.order) || 0) - (Number(prev.meta?.order) || 0));
routes.sort((next, prev) => (Number(next.meta?.order) || 1) - (Number(prev.meta?.order) || 1));
routes.forEach(sortRouteByOrder);
return routes;
+13 -13
View File
@@ -1,7 +1,7 @@
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
import type { Ref } from 'vue';
import { defineStore } from 'pinia';
import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
import { usePreferredColorScheme } from '@vueuse/core';
import { SetupStoreId } from '@/enum';
import { localStg } from '@/utils/storage';
import { addThemeVarsToHtml, createThemeToken, getAntdTheme, initThemeSettings, toggleCssDarkMode } from './shared';
@@ -102,18 +102,18 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
}
/** Cache theme settings */
function cacheThemeSettings() {
const isProd = import.meta.env.PROD;
if (!isProd) return;
localStg.set('themeSettings', settings.value);
}
// cache theme settings when page is closed or refreshed
useEventListener(window, 'beforeunload', () => {
cacheThemeSettings();
});
// function cacheThemeSettings() {
// const isProd = import.meta.env.PROD;
//
// if (!isProd) return;
//
// localStg.set('themeSettings', settings.value);
// }
//
// // cache theme settings when page is closed or refreshed
// useEventListener(window, 'beforeunload', () => {
// cacheThemeSettings();
// });
// watch store
scope.run(() => {
+2
View File
@@ -10,6 +10,8 @@ const DARK_CLASS = 'dark';
/** Init theme settings */
export function initThemeSettings() {
return themeSettings; // 直接使用默认设置 src/store/modules/theme/index.ts 注释104行Cache theme settings相关
const isProd = import.meta.env.PROD;
// if it is development mode, the theme settings will not be cached, by update `themeSettings` in `src/theme/settings.ts` to update theme settings
+3 -3
View File
@@ -1,8 +1,8 @@
/** Default theme settings */
export const themeSettings: App.Theme.ThemeSetting = {
themeScheme: 'light',
themeColor: '#646cff',
otherColor: { info: '#2080f0', success: '#52c41a', warning: '#faad14', error: '#f5222d' },
themeScheme: 'auto',
themeColor: '#3B82F6',
otherColor: { info: '#3B82F6', success: '#52c41a', warning: '#faad14', error: '#f5222d' },
isInfoFollowPrimary: true,
layout: { mode: 'horizontal-mix', scrollMode: 'content' },
page: { animate: true, animateMode: 'fade-slide' },
+590 -1
View File
@@ -81,7 +81,6 @@ declare namespace Api {
interface UserInfo {
user: User | null;
roles: string[];
buttons: string[];
/** permissions */
permissions: string[];
}
@@ -152,6 +151,126 @@ declare namespace Api {
interface EmailCaptcha{
email:string;
}
interface CheckBody{
username?: string;
email?: string;
phonenumber?:string;
authType: string;
}
interface RateLimit {
id: number;
rateLimitName: string;
upLimit: number;
downLimit: number;
upLimitEnable: boolean;
downLimitEnable: boolean;
createBy: number;
createTime: string;
updateBy: number;
updateTime: string;
delFlag: boolean;
}
interface RateLimitAdd {
rateLimitName: string;
upLimitEnable: boolean;
downLimitEnable: boolean;
upLimit: number;
downLimit: number;
}
interface Package {
id: string;
packageName: string;
periodNum: number;
periodType: number;
price: string;
trafficEnable: boolean;
traffic: number;
rateLimitEnable: boolean;
rateLimitId?: number;
durationEnable: boolean;
duration: number;
clientNumEnable: boolean;
clientNum: number;
packageEnable: boolean;
createBy: number;
createTime: string;
updateBy: number;
updateTime: string;
delFlag: boolean;
}
interface PackageAdd {
packageName: string;
periodNum: number;
periodType: number;
price: string;
trafficEnable: boolean;
traffic: number;
rateLimitEnable: boolean;
rateLimitId?: number;
durationEnable: boolean;
duration: number;
clientNumEnable: boolean;
clientNum: number;
packageEnable: boolean;
}
// CDR 话单查询参数
interface CdrHistoryParams {
pageNum: number;
pageSize: number;
userName?: string;
clientName?: string;
clientMac?: string;
startTime?: string;
endTime?: string;
}
// CDR 话单信息
interface CdrHistory {
id: string;
userId: number;
userName: string;
clientName: string;
clientMac: string;
trafficUp: number;
trafficDown: number;
startTime: string;
endTime: string;
duration: number;
}
// CDR 话单响应
interface CdrHistoryResponse {
rows: CdrHistory[];
total: number;
}
// 账单查询参数
interface BillParams {
pageNum: number;
pageSize: number;
userName?: string;
type?: number;
status?: number;
}
// 账单信息
interface BillInfo {
id: string;
userId: number;
userName: string;
type: number;
packageName: string;
orderAmount: number;
status: number;
createTime: string;
updateTime: string;
}
// 账单响应
interface BillResponse {
rows: BillInfo[];
total: number;
}
}
/**
@@ -418,4 +537,474 @@ declare namespace Api {
type DictList = Common.PaginatingQueryRecord<Dict>;
}
namespace Device {
interface ApDevice {
mac: string;
name: string;
type: string;
subtype: string | null;
deviceSeriesType: number;
model: string;
ip: string;
ipv6: string | null;
uptime: string;
status: number;
lastSeen: number;
cpuUtil: number;
memUtil: number;
sn: string;
licenseStatus: number;
tagName: string | null;
uplinkDeviceMac: string | null;
uplinkDeviceName: string | null;
uplinkDevicePort: string | null;
linkSpeed: string | null;
duplex: string | null;
switchConsistent: boolean;
publicIp: string;
firmwareVersion: string;
}
interface ApDeviceListData {
total: number;
rows: ApDevice[];
}
interface ApDeviceResponse {
code: number;
msg: string;
data: ApDeviceListData;
}
interface ApDeviceParams {
pageNum: number;
pageSize: number;
name?: string;
mac?: string;
}
interface TerminalDevice {
id: number;
name: string;
ip: string;
authStatus: number;
ssid: string;
apName: string;
activity: number;
trafficDown: number;
trafficUp: number;
uptime: number;
mac:string;
createBy: string | null;
createTime: string;
updateBy: string | null;
updateTime: string | null;
}
type TerminalDeviceResponse = App.Service.Response<TerminalDevice[]>;
interface TerminalDeviceParams {
pageNum: number;
pageSize: number;
clientName?: string;
clientDeviceType?: string;
clientMac?: string;
}
/** AP设备添加参数 */
interface AddApDeviceItem {
sn?: string;
name?: string;
username?: string;
password?: string;
}
interface AddApDeviceParams {
devices: AddApDeviceItem[];
}
interface ApDeviceLocation {
longitude: number;
latitude: number;
address: string;
}
interface ApDeviceConfigData {
name: string;
ledSetting: number;
tagIds: string[];
location: ApDeviceLocation;
}
interface ApDeviceConfigResponse {
code: number;
msg: string;
data: {
name: string;
ledSetting: number;
tagIds: string[];
location: {
longitude: number;
latitude: number;
address: string;
}
}|null;
}
interface ApDeviceConfigUpdate {
name?: string;
ledSetting?: number;
tagIds?: string[];
location?: {
longitude?: number;
latitude?: number;
address?: string;
}
}
interface ApDeviceConfigForm {
name: string;
ledSetting: number;
tagIds?: string[];
longitude?: number;
latitude?: number;
address?: string;
}
}
namespace Billing {
interface BillRule {
id: number;
traffic: number;
price: number;
unit: number;
enable: boolean;
createBy: string | null;
createTime: string;
updateBy: string | null;
updateTime: string | null;
}
type BillRuleResponse = App.Service.Response<BillRule[]>;
interface BillRuleUpdate {
traffic: number;
price: number;
unit: number;
enable: boolean;
}
}
interface DashboardOverview {
cloud: {
connected: boolean;
};
siteNum: number;
totalGatewayNum: number;
connectedGatewayNum: number;
disconnectedGatewayNum: number;
totalSwitchNum: number;
connectedSwitchNum: number;
disconnectedSwitchNum: number;
totalApNum: number;
connectedApNum: number;
disconnectedApNum: number;
isolatedApNum: number;
totalClientNum: number;
wiredClientNum: number;
wirelessClientNum: number;
guestNum: number;
registerUserNum: number;
onlineUserNum: number;
}
interface DashboardSite {
siteId: string;
name: string;
region: string;
tagIds: string[];
timeZone: string;
scenario: string;
totalGatewayNum: number;
connectedGatewayNum: number;
disconnectedGatewayNum: number;
totalSwitchNum: number;
connectedSwitchNum: number;
disconnectedSwitchNum: number;
totalApNum: number;
connectedApNum: number;
disconnectedApNum: number;
isolatedApNum: number;
totalClientNum: number;
wiredClientNum: number;
wirelessClientNum: number;
guestNum: number;
}
interface DashboardSiteResponse {
rows: DashboardSite[];
total: number;
}
namespace Kyc {
interface KycInfo {
id: string;
userId: number;
userName: string;
realName: string;
idType: string;
idFile: string;
identifyPicture: string;
status: string;
reason?: string;
createTime: string;
updateTime: string;
}
interface KycParams {
pageNum: number;
pageSize: number;
userName?: string;
status?: string;
createTimeStart?: string;
createTimeEnd?: string;
}
interface KycResponse {
rows: KycInfo[];
total: number;
}
}
namespace Site {
interface SiteInfo {
siteId: string;
name: string;
tagIds: string[];
region: string;
timeZone: string;
scenario: string;
longitude: number | null;
latitude: number | null;
address: string | null;
type: number;
supportES: boolean;
supportL2: boolean;
}
interface AddSiteParams {
name: string;
region: string;
timeZone: string;
scenario: string;
deviceAccountSetting: {
username: string;
password: string;
};
}
interface SiteConfig {
name: string;
region: string;
timeZone: string;
scenario: string;
}
interface UpdateSiteParams {
name: string;
region: string;
timeZone: string;
scenario: string;
}
interface SiteParams {
pageNum: number;
pageSize: number;
}
interface SiteListData {
total: number;
rows: SiteInfo[];
}
type SiteResponse = App.Service.ApiResponse<{
rows: SiteInfo[];
total: number;
}>;
}
namespace Wlan {
interface WlanGroup {
wlanId: string;
wlanName: string;
ssidList: SsidInfo[];
}
type WlanGroupResponse = App.Service.ApiResponse<WlanGroup[]>;
interface WlanSsid {
ssidId: string;
name: string;
deviceType: number;
band: number;
guestNetEnable: boolean;
security: number;
broadcast: boolean;
vlanEnable: boolean;
vlanId: number | null;
mloEnable?: boolean;
pmfMode?: number;
enable11r?: boolean;
hidePwd?: boolean;
}
interface WlanSsidConfigResponse extends App.Service.ApiResponse<WlanSsid> {}
interface WlanSsidResponse extends App.Service.ApiResponse<{
rows: WlanSsid[];
total: number;
}>{}
interface WlanSsidParams {
pageNum: number;
pageSize: number;
}
interface AddWlanSsidParams {
name: string;
deviceType: number;
band: number;
guestNetEnable: boolean;
security: number;
broadcast: boolean;
vlanEnable: boolean;
vlanId?: number;
pskSetting:{
securityKey:string;
versionPsk:number;
encryptionPsk:number;
gikRekeyPskEnable:boolean;
};
// 固定值字段
mloEnable: false;
pmfMode: 1;
enable11r: false;
hidePwd: false;
greEnable: false;
}
// 表单数据类型
interface WlanSsidForm {
name: string;
deviceType: {
eap: boolean;
gateway: boolean;
};
band: {
band24G: boolean;
band5G: boolean;
band6G: boolean;
};
password:string;
guestNetEnable: boolean;
security: number;
pskSetting:{
securityKey:string;
versionPsk:number;
encryptionPsk:number;
gikRekeyPskEnable:boolean;
};
broadcast: boolean;
vlanEnable: boolean;
vlanId?: number;
}
}
namespace Portal {
interface Portal {
id: string;
name: string;
enable: boolean;
ssidNames: string[];
ssidList: string[];
networkList: string[];
authType: number;
hotspotTypes: any;
}
interface PortalResponse extends App.Service.ApiResponse<{
rows: Portal[];
total: number;
}> {}
}
namespace Portal {
// 先定义基础类型
interface SsidInfo {
ssidId: string;
ssidName: string;
}
interface WlanGroup {
wlanId: string;
wlanName: string;
ssidList: SsidInfo[];
}
interface Portal {
id: string;
name: string;
enable: boolean;
ssidNames: string[];
ssidList: string[];
networkList: string[];
authType: number;
hotspotTypes: any;
}
interface PortalResponse extends App.Service.ApiResponse<{
rows: Portal[];
total: number;
}> {}
interface SsidListResponse extends App.Service.ApiResponse<WlanGroup[]> {}
interface AuthTimeout {
customTimeout: number;
customTimeoutUnit: 1 | 2 | 3; // 1:分钟 2:小时 3:天
}
interface NoAuth {
dailyLimitEnable: boolean;
}
interface ExternalPortal {
hostType: 1 | 2; // 1: IP地址 2: URL
serverUrl: string; // IP地址或URL值
serverUrlScheme?: string; // URL时的协议类型
}
interface ImportedPortalPage {
id: string;
}
interface AddPortalParams {
name: string;
enable: boolean;
ssidList: string[];
authType: number;
authTimeout: AuthTimeout;
noAuth: NoAuth;
httpsRedirectEnable: boolean;
landingPage: 1 | 2 | 3;
landingUrlScheme?: string;
landingUrl?: string;
externalPortal?: ExternalPortal; // 添加外部Portal配置
pageType: number;
importedPortalPage: ImportedPortalPage;
}
}
}
declare namespace App {
namespace Service {
interface ApiResponse<T = any> {
data: T;
error: any;
}
}
}
+1 -402
View File
@@ -243,408 +243,7 @@ declare namespace App {
};
type Schema = {
system: {
title: string;
};
common: {
action: string;
add: string;
addSuccess: string;
backToHome: string;
batchDelete: string;
cancel: string;
close: string;
check: string;
columnSetting: string;
config: string;
confirm: string;
delete: string;
deleteSuccess: string;
confirmDelete: string;
edit: string;
index: string;
keywordSearch: string;
logout: string;
logoutConfirm: string;
lookForward: string;
modify: string;
modifySuccess: string;
noData: string;
operate: string;
pleaseCheckValue: string;
refresh: string;
reset: string;
search: string;
switch: string;
tip: string;
trigger: string;
update: string;
updateSuccess: string;
userCenter: string;
yesOrNo: {
yes: string;
no: string;
};
};
request: {
logout: string;
logoutMsg: string;
logoutWithModal: string;
logoutWithModalMsg: string;
refreshToken: string;
tokenExpired: string;
};
theme: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
themeColor: {
title: string;
followPrimary: string;
} & Theme.ThemeColor;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
page: {
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
header: {
height: string;
breadcrumb: {
visible: string;
showIcon: string;
};
};
tab: {
visible: string;
cache: string;
height: string;
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
};
sider: {
inverted: string;
width: string;
collapsedWidth: string;
mixWidth: string;
mixCollapsedWidth: string;
mixChildMenuWidth: string;
};
footer: {
visible: string;
fixed: string;
height: string;
right: string;
};
themeDrawerTitle: string;
pageFunTitle: string;
configOperation: {
copyConfig: string;
copySuccessMsg: string;
resetConfig: string;
resetSuccessMsg: string;
};
};
route: Record<I18nRouteKey, string>;
page: {
login: {
common: {
loginOrRegister: string;
userNamePlaceholder: string;
phonePlaceholder: string;
codePlaceholder: string;
passwordPlaceholder: string;
confirmPasswordPlaceholder: string;
codeLogin: string;
confirm: string;
back: string;
validateSuccess: string;
loginSuccess: string;
registerSuccess: string;
welcomeBack: string;
checkCode: string;
};
pwdLogin: {
title: string;
rememberMe: string;
forgetPassword: string;
register: string;
otherAccountLogin: string;
otherLoginMode: string;
superAdmin: string;
admin: string;
user: string;
};
codeLogin: {
title: string;
getCode: string;
reGetCode: string;
sendCodeSuccess: string;
imageCodePlaceholder: string;
};
register: {
title: string;
agreement: string;
protocol: string;
policy: string;
};
resetPwd: {
title: string;
};
bindWeChat: {
title: string;
};
};
about: {
title: string;
introduction: string;
projectInfo: {
title: string;
version: string;
latestBuildTime: string;
githubLink: string;
previewLink: string;
};
prdDep: string;
devDep: string;
};
home: {
greeting: string;
weatherDesc: string;
projectCount: string;
todo: string;
message: string;
downloadCount: string;
registerCount: string;
schedule: string;
study: string;
work: string;
rest: string;
entertainment: string;
visitCount: string;
turnover: string;
dealCount: string;
projectNews: {
title: string;
moreNews: string;
desc1: string;
desc2: string;
desc3: string;
desc4: string;
desc5: string;
};
creativity: string;
};
function: {
tab: {
tabOperate: {
title: string;
addTab: string;
addTabDesc: string;
closeTab: string;
closeCurrentTab: string;
closeAboutTab: string;
addMultiTab: string;
addMultiTabDesc1: string;
addMultiTabDesc2: string;
};
tabTitle: {
title: string;
changeTitle: string;
change: string;
resetTitle: string;
reset: string;
};
};
multiTab: {
routeParam: string;
backTab: string;
};
toggleAuth: {
toggleAccount: string;
authHook: string;
superAdminVisible: string;
adminVisible: string;
adminOrUserVisible: string;
};
};
manage: {
common: {
status: {
enable: string;
disable: string;
};
};
role: {
title: string;
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
form: {
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
};
addRole: string;
editRole: string;
menuAuth: string;
buttonAuth: string;
};
user: {
userName: string;
nickName: string;
email: string;
phonenumber: string;
status: string;
dept: string;
title: string;
addUser: string;
remark: string;
editUser: string;
post: string;
role: string;
password: string;
form: {
userName: string;
email: string;
status: string;
nickName: string;
phonenumber: string;
remark: string;
password: string;
dept: string;
};
};
menu: {
home: string;
title: string;
id: string;
parentId: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
routeParams: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
iconTypeTitle: string;
order: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedIndexInTab: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
form: {
home: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
order: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedInTab: string;
fixedIndexInTab: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
};
addMenu: string;
editMenu: string;
addChildMenu: string;
type: {
directory: string;
menu: string;
};
iconType: {
iconify: string;
local: string;
};
};
dept: {
deptName: string;
leader: string;
status: string;
form: {
deptName: string;
leader: string;
status: string;
};
};
post: {
addPost: string;
editPost: string;
postName: string;
postCode: string;
postSort: string;
status: string;
remark: string;
title: string;
form: {
postName: string;
postCode: string;
postSort: string;
remark: string;
status: string;
};
};
dict: {
title: string;
dictName: string;
dictType: string;
status: string;
remark: string;
form: {
dictName: string;
dictType: string;
status: string;
remark: string;
};
addDict: string;
editDict: string;
};
};
};
form: {
required: string;
username: FormMsg;
phone: FormMsg;
pwd: FormMsg;
confirmPwd: FormMsg;
code: FormMsg;
email: FormMsg;
};
dropdown: Record<Global.DropdownKey, string>;
icon: {
themeConfig: string;
themeSchema: string;
lang: string;
fullscreen: string;
fullscreenExit: string;
reload: string;
collapse: string;
expand: string;
pin: string;
unpin: string;
};
[key: string]: string;
};
type GetI18nKey<T extends Record<string, unknown>, K extends keyof T = keyof T> = K extends string
+112 -1
View File
@@ -10,17 +10,30 @@ declare global {
const $notification: typeof import('ant-design-vue')['notification']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const addApDevice: typeof import('../service/api/auth')['addApDevice']
const addData: typeof import('../service/api/dictData')['addData']
const addJob: typeof import('../service/api/job')['addJob']
const addPackage: typeof import('../service/api/auth')['addPackage']
const addPortal: typeof import('../service/api/auth')['addPortal']
const addRateLimit: typeof import('../service/api/auth')['addRateLimit']
const addSite: typeof import('../service/api/auth')['addSite']
const addThemeVarsToHtml: typeof import('../store/modules/theme/shared')['addThemeVarsToHtml']
const addWlanSsid: typeof import('../service/api/auth')['addWlanSsid']
const adoptApDevice: typeof import('../service/api/auth')['adoptApDevice']
const afterAll: typeof import('vitest')['afterAll']
const afterEach: typeof import('vitest')['afterEach']
const approveKyc: typeof import('../service/api/auth')['approveKyc']
const assert: typeof import('vitest')['assert']
const assign: typeof import('lodash-es')['assign']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const bandwidthFactors: typeof import('../utils/units')['bandwidthFactors']
const bandwidthUnits: typeof import('../utils/units')['bandwidthUnits']
const beforeAll: typeof import('vitest')['beforeAll']
const beforeEach: typeof import('vitest')['beforeEach']
const chai: typeof import('vitest')['chai']
const checkReport: typeof import('../service/api/auth')['checkReport']
const changeJobStatus: typeof import('../service/api/job')['changeJobStatus']
const cleanJobLog: typeof import('../service/api/jobLog')['cleanJobLog']
const clearAuthStorage: typeof import('../store/modules/auth/shared')['clearAuthStorage']
const cloneDeep: typeof import('lodash-es')['cloneDeep']
const computed: typeof import('vue')['computed']
@@ -30,6 +43,9 @@ declare global {
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const convertBandwidth: typeof import('../utils/units')['convertBandwidth']
const convertStorage: typeof import('../utils/units')['convertStorage']
const convertTime: typeof import('../utils/units')['convertTime']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
@@ -48,13 +64,26 @@ declare global {
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const delData: typeof import('../service/api/dictData')['delData']
const delJobLog: typeof import('../service/api/job')['delJobLog']
const deleteAlerts: typeof import('../service/api/auth')['deleteAlerts']
const deleteApDevices: typeof import('../service/api/auth')['deleteApDevices']
const deletePackage: typeof import('../service/api/auth')['deletePackage']
const deletePortal: typeof import('../service/api/auth')['deletePortal']
const deleteSite: typeof import('../service/api/auth')['deleteSite']
const deleteWlanSsid: typeof import('../service/api/auth')['deleteWlanSsid']
const describe: typeof import('vitest')['describe']
const dict: typeof import('../store/modules/dict/index')['default']
const doAddDept: typeof import('../service/api/dept')['doAddDept']
const doAddDict: typeof import('../service/api/dict')['doAddDict']
const doAddMenu: typeof import('../service/api/menu')['doAddMenu']
const doAddPost: typeof import('../service/api/post')['doAddPost']
const doCheckUserRepeat: typeof import('../service/api/auth')['doCheckUserRepeat']
const doCleanJobLog: typeof import('../service/api/jobLog')['doCleanJobLog']
const doDeleteDept: typeof import('../service/api/dept')['doDeleteDept']
const doDeleteDict: typeof import('../service/api/dict')['doDeleteDict']
const doDeleteJobLog: typeof import('../service/api/jobLog')['doDeleteJobLog']
const doDeleteLog: typeof import('../service/api/log')['doDeleteLog']
const doDeleteLogout: typeof import('../service/api/auth')['doDeleteLogout']
const doDeleteMenu: typeof import('../service/api/menu')['doDeleteMenu']
const doDeletePost: typeof import('../service/api/post')['doDeletePost']
@@ -64,8 +93,10 @@ declare global {
const doEditDict: typeof import('../service/api/dict')['doEditDict']
const doEditMenu: typeof import('../service/api/menu')['doEditMenu']
const doEditPost: typeof import('../service/api/post')['doEditPost']
const doExportLog: typeof import('../service/api/log')['doExportLog']
const doGetAdminUserPostsAndRoles: typeof import('../service/api/user')['doGetAdminUserPostsAndRoles']
const doGetCheckCode: typeof import('../service/api/auth')['doGetCheckCode']
const doGetDataInfo: typeof import('../service/api/dictData')['doGetDataInfo']
const doGetDeptInfo: typeof import('../service/api/dept')['doGetDeptInfo']
const doGetDeptList: typeof import('../service/api/dept')['doGetDeptList']
const doGetDictList: typeof import('../service/api/dict')['doGetDictList']
@@ -75,50 +106,99 @@ declare global {
const doGetPostList: typeof import('../service/api/post')['doGetPostList']
const doGetRoleList: typeof import('../service/api/role')['doGetRoleList']
const doGetRoleMenuList: typeof import('../service/api/menu')['doGetRoleMenuList']
const doGetType: typeof import('../service/api/dict')['doGetType']
const doGetUserDeptTree: typeof import('../service/api/user')['doGetUserDeptTree']
const doGetUserInfo: typeof import('../service/api/auth')['doGetUserInfo']
const doGetUserList: typeof import('../service/api/user')['doGetUserList']
const doGetUserPostsAndRoles: typeof import('../service/api/user')['doGetUserPostsAndRoles']
const doGetUserRoutes: typeof import('../service/api/route')['doGetUserRoutes']
const doGetdictDataList: typeof import('../service/api/dictData')['doGetdictDataList']
const doGetjobInfo: typeof import('../service/api/job')['doGetjobInfo']
const doGetjobList: typeof import('../service/api/job')['doGetjobList']
const doGetjobLogInfo: typeof import('../service/api/jobLog')['doGetjobLogInfo']
const doGetjobLogList: typeof import('../service/api/jobLog')['doGetjobLogList']
const doGetlogList: typeof import('../service/api/log')['doGetlogList']
const doGetsysUserList: typeof import('../service/api/user')['doGetsysUserList']
const doPostRole: typeof import('../service/api/role')['doPostRole']
const doPostUser: typeof import('../service/api/user')['doPostUser']
const doPutRole: typeof import('../service/api/role')['doPutRole']
const doPutUser: typeof import('../service/api/user')['doPutUser']
const doUpdatePwd: typeof import('../service/api/user')['doUpdatePwd']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const editRateLimit: typeof import('../service/api/auth')['editRateLimit']
const effectScope: typeof import('vue')['effectScope']
const emptyInfo: typeof import('../store/modules/auth/shared')['emptyInfo']
const expect: typeof import('vitest')['expect']
const exportData: typeof import('../service/api/dictData')['exportData']
const exportJob: typeof import('../service/api/job')['exportJob']
const exportJobLog: typeof import('../service/api/jobLog')['exportJobLog']
const extendRef: typeof import('@vueuse/core')['extendRef']
const extractTabsByAllRoutes: typeof import('../store/modules/tab/shared')['extractTabsByAllRoutes']
const fetchAccessControl: typeof import('../service/api/auth')['fetchAccessControl']
const fetchAlertList: typeof import('../service/api/auth')['fetchAlertList']
const fetchApDeviceList: typeof import('../service/api/auth')['fetchApDeviceList']
const fetchBillList: typeof import('../service/api/auth')['fetchBillList']
const fetchBillRuleList: typeof import('../service/api/auth')['fetchBillRuleList']
const fetchCdrHistory: typeof import('../service/api/auth')['fetchCdrHistory']
const fetchCustomBackendError: typeof import('../service/api/auth')['fetchCustomBackendError']
const fetchGetAllPages: typeof import('../service/api/menu')['fetchGetAllPages']
const fetchGetConstantRoutes: typeof import('../service/api/route')['fetchGetConstantRoutes']
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
const fetchKycImg: typeof import('../service/api/auth')['fetchKycImg']
const fetchKycList: typeof import('../service/api/auth')['fetchKycList']
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
const fetchPackageList: typeof import('../service/api/auth')['fetchPackageList']
const fetchPortalList: typeof import('../service/api/auth')['fetchPortalList']
const fetchRateLimitList: typeof import('../service/api/auth')['fetchRateLimitList']
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']
const fetchRegister: typeof import('../service/api/auth')['fetchRegister']
const fetchSiteList: typeof import('../service/api/auth')['fetchSiteList']
const fetchSsidList: typeof import('../service/api/auth')['fetchSsidList']
const fetchTerminalList: typeof import('../service/api/auth')['fetchTerminalList']
const fetchWlanGroups: typeof import('../service/api/auth')['fetchWlanGroups']
const fetchWlanSsidList: typeof import('../service/api/auth')['fetchWlanSsidList']
const fetcodeReset: typeof import('../service/api/auth')['fetcodeReset']
const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles']
const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById']
const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds']
const findTabByRouteName: typeof import('../store/modules/tab/shared')['findTabByRouteName']
const forgetApDevice: typeof import('../service/api/auth')['forgetApDevice']
const formatBandwidth: typeof import('../utils/units')['formatBandwidth']
const formatStorage: typeof import('../utils/units')['formatStorage']
const formatTime: typeof import('../utils/units')['formatTime']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getAllTabs: typeof import('../store/modules/tab/shared')['getAllTabs']
const getAntdTheme: typeof import('../store/modules/theme/shared')['getAntdTheme']
const getApDeviceConfig: typeof import('../service/api/auth')['getApDeviceConfig']
const getBreadcrumbsByRoute: typeof import('../store/modules/route/shared')['getBreadcrumbsByRoute']
const getCacheRouteNames: typeof import('../store/modules/route/shared')['getCacheRouteNames']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDashboardOverview: typeof import('../service/api/auth')['getDashboardOverview']
const getDashboardSiteList: typeof import('../service/api/auth')['getDashboardSiteList']
const getData: typeof import('../service/api/dictData')['getData']
const getDefaultHomeTab: typeof import('../store/modules/tab/shared')['getDefaultHomeTab']
const getDictDataType: typeof import('../service/api/dict')['getDictDataType']
const getDictOptionselect: typeof import('../service/api/dict')['getDictOptionselect']
const getEmailCode: typeof import('../service/api/auth')['getEmailCode']
const getFixedTabIds: typeof import('../store/modules/tab/shared')['getFixedTabIds']
const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs']
const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes']
const getLocalizedTimeUnit: typeof import('../utils/units')['getLocalizedTimeUnit']
const getMeshConfig: typeof import('../service/api/auth')['getMeshConfig']
const getPaymentConfig: typeof import('../service/api/auth')['getPaymentConfig']
const getPortalConfig: typeof import('../service/api/auth')['getPortalConfig']
const getRoamingConfig: typeof import('../service/api/auth')['getRoamingConfig']
const getRouteIcons: typeof import('../store/modules/tab/shared')['getRouteIcons']
const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey']
const getServiceBaseURL: typeof import('../utils/service')['getServiceBaseURL']
const getSiteConfig: typeof import('../service/api/auth')['getSiteConfig']
const getTabByRoute: typeof import('../store/modules/tab/shared')['getTabByRoute']
const getTabIdByRoute: typeof import('../store/modules/tab/shared')['getTabIdByRoute']
const getToken: typeof import('../store/modules/auth/shared')['getToken']
const getUserProfile: typeof import('../service/api/auth')['getUserProfile']
const getWlanSsidConfig: typeof import('../service/api/auth')['getWlanSsidConfig']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const initThemeSettings: typeof import('../store/modules/theme/shared')['initThemeSettings']
@@ -141,6 +221,7 @@ declare global {
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markAlertsResolved: typeof import('../service/api/auth')['markAlertsResolved']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const omit: typeof import('lodash-es')['omit']
@@ -173,17 +254,24 @@ declare global {
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readLoalXlsx: typeof import('../utils/execl-utils')['readLoalXlsx']
const readSheet: typeof import('../utils/execl-utils')['readSheet']
const readonly: typeof import('vue')['readonly']
const rebootApDevice: typeof import('../service/api/auth')['rebootApDevice']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const rejectKyc: typeof import('../service/api/auth')['rejectKyc']
const removeEmptyChildren: typeof import('../utils/menu')['removeEmptyChildren']
const removeRateLimit: typeof import('../service/api/auth')['removeRateLimit']
const resetPassword: typeof import('../service/api/auth')['resetPassword']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const runJob: typeof import('../service/api/job')['runJob']
const sendCaptcha: typeof import('../service/api/auth')['sendCaptcha']
const sessionStg: typeof import('../utils/storage')['sessionStg']
const setActivePinia: typeof import('pinia')['setActivePinia']
@@ -192,6 +280,8 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
const storageFactors: typeof import('../utils/units')['storageFactors']
const storageUnits: typeof import('../utils/units')['storageUnits']
const storeToRefs: typeof import('pinia')['storeToRefs']
const suite: typeof import('vitest')['suite']
const syncRef: typeof import('@vueuse/core')['syncRef']
@@ -200,6 +290,8 @@ declare global {
const test: typeof import('vitest')['test']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const timeFactors: typeof import('../utils/units')['timeFactors']
const timeUnits: typeof import('../utils/units')['timeUnits']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
@@ -217,12 +309,27 @@ declare global {
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unitFactors: typeof import('../utils/bandwidth')['unitFactors']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const updateAccessControl: typeof import('../service/api/auth')['updateAccessControl']
const updateApDeviceConfig: typeof import('../service/api/auth')['updateApDeviceConfig']
const updateBillRule: typeof import('../service/api/auth')['updateBillRule']
const updateData: typeof import('../service/api/dictData')['updateData']
const updateJob: typeof import('../service/api/job')['updateJob']
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
const updateMeshConfig: typeof import('../service/api/auth')['updateMeshConfig']
const updatePackage: typeof import('../service/api/auth')['updatePackage']
const updatePasswordByOld: typeof import('../service/api/auth')['updatePasswordByOld']
const updatePaymentConfig: typeof import('../service/api/auth')['updatePaymentConfig']
const updatePortalConfig: typeof import('../service/api/auth')['updatePortalConfig']
const updateRoamingConfig: typeof import('../service/api/auth')['updateRoamingConfig']
const updateSite: typeof import('../service/api/auth')['updateSite']
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
const updateUserProfile: typeof import('../service/api/auth')['updateUserProfile']
const updateWlanSsid: typeof import('../service/api/auth')['updateWlanSsid']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAntdForm: typeof import('../hooks/common/form')['useAntdForm']
@@ -291,6 +398,7 @@ declare global {
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFormRules: typeof import('../hooks/common/form')['useFormRules']
const useFormatTime: typeof import('../utils/units')['useFormatTime']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
@@ -373,6 +481,7 @@ declare global {
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeUnits: typeof import('../utils/units')['useTimeUnits']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
@@ -414,6 +523,8 @@ declare global {
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
const writeSheet: typeof import('../utils/execl-utils')['writeSheet']
const xlsxUrl: typeof import('../utils/execl-utils')['xlsxUrl']
}
// for type re-export
declare global {
-91
View File
@@ -1,91 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover']
AppLoading: typeof import('./../components/common/app-loading.vue')['default']
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
ASegmented: typeof import('ant-design-vue/es')['Segmented']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATree: typeof import('ant-design-vue/es')['Tree']
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default']
SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}
}
+7
View File
@@ -0,0 +1,7 @@
/**字段类型 */
type DictType = {
label: string;
value: string;
tagType?: string;
tagClass?: string;
};
+1 -2
View File
@@ -55,8 +55,7 @@ declare namespace Env {
* use "," to separate multiple codes
*/
readonly VITE_SERVICE_EXPIRED_TOKEN_CODES: string;
/** when the route mode is static, the defined super role */
readonly VITE_STATIC_SUPER_ROLE: string;
/**
* other backend service base url
*
+2 -2
View File
@@ -13,9 +13,9 @@ declare namespace StorageType {
/** The i18n language */
lang: App.I18n.LangType;
/** The token */
token: string;
tokenSys: string;
/** The refresh token */
refreshToken: string;
refreshTokenSys: string;
/** The user info */
userInfo: Api.Auth.UserInfo;
/** The theme color */
+87
View File
@@ -0,0 +1,87 @@
import { JSON2SheetOpts, read, utils, write } from 'xlsx';
// 静态资源路径
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
export const xlsxUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/alarmHelp`;
/**
* 读取本地文件
* @param id 表格ID
* @returns 数据数组
* @example
* readLoalXlsx('20001').then(res=>{
* console.log(res)
* });
*/
export async function readLoalXlsx(
lang: string,
id: string
): Promise<Record<string, any>[]> {
let result = await fetch(`${xlsxUrl}/${lang}/${id}.xlsx`);
let fileBuffer = await result.arrayBuffer();
// 判断是否xlsx文件
const data = new Uint8Array(fileBuffer);
const isXlsxFile =
data[0] === 0x50 &&
data[1] === 0x4b &&
data[2] === 0x03 &&
data[3] === 0x04;
if (!isXlsxFile) {
result = await fetch(`${xlsxUrl}/${lang}/all.xlsx`);
fileBuffer = await result.arrayBuffer();
}
return readSheet(fileBuffer, 0);
}
/**
* 读取表格数据 工作表
* @param fileBolb 文件对象
* @param index 文件保存路径
* @return 表格对象列表
*/
export async function readSheet(
fileBolb: Blob | ArrayBuffer,
index: number = 0
): Promise<Record<string, string>[]> {
const workBook = read(fileBolb);
let workSheet = workBook.Sheets[workBook.SheetNames[index]];
return utils.sheet_to_json<Record<string, string>>(workSheet);
}
/**
* 写入表格数据,一般用于导出
* @param filePath 文件路径
* @param sheetName 工作表名称
* @return xlsx文件流, 使用saveAs函数保存
* @example
* writeSheet(res.data, from.logType).then(fileBlob =>
* saveAs(fileBlob, `${from.logType}_${Date.now()}.xlsx`)
* );
*
*/
export async function writeSheet(
data: any[],
sheetName: string,
opts?: JSON2SheetOpts
) {
if (data.length === 0) {
return new Blob([], { type: 'application/octet-stream' });
}
const workSheet = utils.json_to_sheet(data, opts);
// 设置列宽度,单位厘米
workSheet['!cols'] = Object.keys(data[0]).map(() => {
return { wch: 20 };
});
const workBook = utils.book_new();
utils.book_append_sheet(workBook, workSheet, sheetName);
const excelBuffer = write(workBook, { type: 'array', bookType: 'xlsx' });
return new Blob([excelBuffer], { type: 'application/octet-stream' });
}
+17 -8
View File
@@ -10,17 +10,26 @@ import type { DataNode } from 'ant-design-vue/es/tree';
*/
export function transformMenuChildWithRootIds(menuList: DataNode[], checkedKeys: number[]): number[] {
const checkedKeysSet = new Set(checkedKeys);
function changeCheckedKeys(menus: DataNode[]) {
menus.forEach(menu => {
if (menu.children) {
changeCheckedKeys(menu.children);
function changeCheckedKeys(menus: DataNode[], checkedKey: number): boolean {
for (let menu of menus) {
if (menu.key === checkedKey) {
checkedKeysSet.add(menu.key as number);
return true;
}
});
if (menu.children && menu.children.length > 0) {
let isFind = changeCheckedKeys(menu.children, checkedKey);
if (isFind) {
checkedKeysSet.add(menu.key as number);
return true;
}
}
};
return false;
}
changeCheckedKeys(menuList);
checkedKeys.forEach(checkedKey => {
changeCheckedKeys(menuList, checkedKey);
})
return Array.from(checkedKeysSet);
}
+1 -1
View File
@@ -3,5 +3,5 @@ import { useAuthStore } from '@/store/modules/auth';
export function isShowBtn(permission: string) {
const { permissions, userInfo } = storeToRefs(useAuthStore());
return userInfo.value.roles.includes('admin') || permissions.value.includes(permission);
return userInfo.value.roles.some(role => ['admin', 'super'].includes(role)) || permissions.value.includes(permission);
}
+108
View File
@@ -0,0 +1,108 @@
import {useI18n} from "vue-i18n";
// 带宽单位转换
export type BandwidthUnit = 'Kbps' | 'Mbps' | 'Gbps';
export const bandwidthUnits: BandwidthUnit[] = ['Kbps', 'Mbps', 'Gbps'];
export const bandwidthFactors: Record<BandwidthUnit, number> = {
Kbps: 1,
Mbps: 1024,
Gbps: 1048576
};
export function convertBandwidth(value: number, fromUnit: BandwidthUnit, toUnit: BandwidthUnit): number {
const fromFactor = bandwidthFactors[fromUnit];
const toFactor = bandwidthFactors[toUnit];
return (value * fromFactor) / toFactor;
}
export function formatBandwidth(kbpsValue: number): { value: number; unit: BandwidthUnit } {
if (kbpsValue >= 1048576) {
return { value: kbpsValue / 1048576, unit: 'Gbps' };
} else if (kbpsValue >= 1024) {
return { value: kbpsValue / 1024, unit: 'Mbps' };
}
return { value: kbpsValue, unit: 'Kbps' };
}
// 流量单位转换
export type StorageUnit = 'B' | 'KB' | 'MB' | 'GB' | 'TB';
export const storageUnits: StorageUnit[] = ['B', 'KB', 'MB', 'GB', 'TB'];
export const storageFactors: Record<StorageUnit, number> = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024
};
export function convertStorage(value: number, fromUnit: StorageUnit, toUnit: StorageUnit): number {
const fromFactor = storageFactors[fromUnit];
const toFactor = storageFactors[toUnit];
return (value * fromFactor) / toFactor;
}
export function formatStorage(byteValue: number): { value: number; unit: StorageUnit } {
if (byteValue >= 1024 * 1024 * 1024 * 1024) {
return { value: byteValue / (1024 * 1024 * 1024 * 1024), unit: 'TB' };
} else if (byteValue >= 1024 * 1024 * 1024) {
return { value: byteValue / (1024 * 1024 * 1024), unit: 'GB' };
} else if (byteValue >= 1024 * 1024) {
return { value: byteValue / (1024 * 1024), unit: 'MB' };
} else if (byteValue >= 1024) {
return { value: byteValue / 1024, unit: 'KB' };
}
return { value: byteValue, unit: 'B' };
}
// 时间单位转换
export type TimeUnit = 'second' | 'minute' | 'hour' | 'day';
export const useTimeUnits = () => {
const { t } = useI18n();
return computed(() => ([
{ key: 'second', label: t('common.time.second') },
{ key: 'minute', label: t('common.time.minute') },
{ key: 'hour', label: t('common.time.hour') },
{ key: 'day', label: t('common.time.day') }
]));
};
export const timeFactors: Record<TimeUnit, number> = {
'second': 1,
'minute': 60,
'hour': 3600,
'day': 86400
};
export function convertTime(value: number, fromUnit: TimeUnit, toUnit: TimeUnit): number {
const fromFactor = timeFactors[fromUnit];
const toFactor = timeFactors[toUnit];
return (value * fromFactor) / toFactor;
}
export const useFormatTime = () => {
const { t } = useI18n();
return (secondsValue: number): { value: number; unit: TimeUnit; display: string } => {
let result: { value: number; unit: TimeUnit };
if (secondsValue >= 86400) {
result = { value: secondsValue / 86400, unit: 'day' };
} else if (secondsValue >= 3600) {
result = { value: secondsValue / 3600, unit: 'hour' };
} else if (secondsValue >= 60) {
result = { value: secondsValue / 60, unit: 'minute' };
} else {
result = { value: secondsValue, unit: 'second' };
}
return {
...result,
display: `${result.value} ${t(`common.time.${result.unit}`)}`
};
};
};
+8 -3
View File
@@ -11,6 +11,8 @@ import ResetPwd from './modules/reset-pwd.vue';
import BindWechat from './modules/bind-wechat.vue';
import { WifiOutlined } from '@ant-design/icons-vue';
import {useI18n} from "vue-i18n";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
//import { $t } from '@/locales';
interface Props {
/** The login module */
@@ -58,9 +60,12 @@ const bgColor = computed(() => {
<WifiOutlined class="text-64px text-primary lt-sm:text-48px" style="margin-left: 9px"/>
<h3 class="text-18px text-primary font-medium">{{ t(activeModule.label) }}</h3>
</header>
<main >
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ "WANFi" }}</h3>
<div class="animation-slide-in-left ">
<main>
<div class="flex justify-between items-center">
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
</div>
<div class="animation-slide-in-left">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
+10 -11
View File
@@ -20,16 +20,15 @@ const codeImg = ref('');
getCheckCode();
const model = reactive({
username: '123456',
password: '123456',
username: '',
password: '',
code: '',
uuid: '',
authType: 'u'
authType: 'sys'
});
const rules = {
username: patternRules.username,
password: patternRules.pwd
};
async function handleSubmit() {
@@ -77,21 +76,21 @@ async function getCheckCode() {
<ASpace direction="vertical" size="large" class="w-full">
<div class="flex-y-center justify-between">
<ACheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</ACheckbox>
<span></span>
<AButton type="text" @click="toggleLoginModule('reset-pwd')">{{ t('page.login.pwdLogin.forgetPassword') }}</AButton>
</div>
<AButton type="primary" block size="large" shape="round" :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.confirm') }}
</AButton>
<div class="flex-y-center justify-between">
<!-- <div class="flex-y-center justify-between">-->
<!-- <AButton class="h-34px flex-1" block @click="toggleLoginModule('code-login')">-->
<!-- {{ t(loginModuleRecord['code-login']) }}-->
<!-- </AButton>-->
<div class="w-12px"></div>
<AButton class="h-34px flex-1" block @click="toggleLoginModule('register')">
{{ t(loginModuleRecord.register) }}
</AButton>
</div>
<!-- <div class="w-12px"></div>-->
<!-- <AButton class="h-34px flex-1" block @click="toggleLoginModule('register')">-->
<!-- {{ t(loginModuleRecord.register) }}-->
<!-- </AButton>-->
<!-- </div>-->
</ASpace>
</AForm>
</template>
+296 -206
View File
@@ -1,24 +1,26 @@
<script setup lang="ts">
import {computed, reactive, ref} from 'vue';
import {useI18n} from 'vue-i18n'; // 添加这行
import {useAuthStore} from '@/store/modules/auth';
import {useRouterPush} from '@/hooks/common/router';
import {useAntdForm, useFormRules} from '@/hooks/common/form';
import {useCaptcha} from '@/hooks/business/captcha';
import {useWindowSize} from '@vueuse/core';
import {registerTerms} from '@/views/_builtin/login/modules/terms';
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules } from '@/hooks/common/form';
import { useCaptcha } from '@/hooks/business/captcha';
import { useWindowSize } from '@vueuse/core';
import { registerTerms } from '@/views/_builtin/login/modules/terms';
import dayjs from 'dayjs';
import type {Rule} from 'ant-design-vue/es/form';
import type { Rule } from 'ant-design-vue/es/form';
const { t } = useI18n();
const authStore = useAuthStore();
// 添加两个表单引用
const basicFormRef = ref();
const securityFormRef = ref();
defineOptions({
name: 'Register'
});
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useAntdForm();
const { label, isCounting, loading, getCaptcha } = useCaptcha();
const { width } = useWindowSize();
@@ -29,17 +31,19 @@ const currentStep = ref(0);
// 是否同意协议
const agreeTerms = ref(false);
// 定义一个统一的数据模型
interface RegisterModel {
username: string;
password: string;
email: string;
fullName: string;
age:0,
age: number;
gender: string;
phone: string;
address: string;
code: string;
uuid: string;
authType: string;
}
@@ -49,11 +53,12 @@ const model = reactive<RegisterModel>({
password: '',
email: '',
fullName: '',
age:0,
age: 0,
gender: '',
phone: '',
address: '',
code: '',
uuid: '',
authType: 'u'
});
@@ -64,7 +69,6 @@ interface BasicFormModel {
birthDate: string;
gender: string;
phone: string;
email: string;
address: string;
}
@@ -74,7 +78,6 @@ const basicModel = reactive<BasicFormModel>({
birthDate: '',
gender: '',
phone: '',
email: '',
address: ''
});
@@ -82,6 +85,7 @@ const basicModel = reactive<BasicFormModel>({
interface SecurityFormModel {
email: string;
code: string;
uuid: string;
password: string;
confirmPassword: string;
}
@@ -89,27 +93,85 @@ interface SecurityFormModel {
const securityModel = reactive<SecurityFormModel>({
email: '',
code: '',
uuid: '',
password: '',
confirmPassword: ''
});
// 第一步表单验证规则
const basicRules = computed<Record<string, Rule | Rule[]>>(() => {
const { formRules } = useFormRules();
const validateUsername = async (_rule: Rule, value: string) => {
if (value && (value.length < 4 || value.length > 20)) {
return Promise.reject(t('page.login.register.usernameLengthLimit'));
}
if (value) {
const { exists } = await authStore.checkUserRepeat({ username: value, authType: 'u' });
if (exists) {
return Promise.reject(t('page.login.register.usernameExists'));
}
}
return Promise.resolve();
};
const validatePhone = async (_rule: Rule, value: string) => {
if (!value) return Promise.resolve();
const phonePattern = /^1[3-9]\d{9}$/;
if (!phonePattern.test(value)) {
return Promise.reject(t('page.login.register.phoneInvalid'));
}
const { exists } = await authStore.checkUserRepeat({ phonenumber: value, authType: 'u' });
if (exists) {
return Promise.reject(t('page.login.register.phoneExists'));
}
return Promise.resolve();
};
return {
username: formRules.username,
email: formRules.email,
username: [
{ required: true, message: t('page.login.register.usernameRequired'), trigger: 'change' },
{ validator: validateUsername, trigger: 'blur' }
],
phone: [{ validator: validatePhone, trigger: 'blur' }],
birthDate: [{ required: true, message: t('page.login.register.birthDateRequired') }],
phone: [{ pattern: /^1[3-9]\d{9}$/, message: t('form.phone.invalid'), trigger: 'blur' }]
fullName: []
};
});
// 第三步表单验证规则
const securityRules = computed<Record<string, Rule | Rule[]>>(() => {
const { formRules, createConfirmPwdRule } = useFormRules();
const { createConfirmPwdRule } = useFormRules();
const validateEmail = async (_rule: Rule, value: string) => {
if (!value) {
return Promise.reject(t('page.login.register.emailRequired'));
}
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailPattern.test(value)) {
return Promise.reject(t('page.login.register.emailInvalid'));
}
const { exists } = await authStore.checkUserRepeat({ email: value, authType: 'u' });
if (exists) {
return Promise.reject(t('page.login.register.emailExists'));
}
return Promise.resolve();
};
return {
code: formRules.code,
password: formRules.pwd,
email: [
{ required: true, message: t('page.login.register.emailRequired'), trigger: 'change' },
{ validator: validateEmail, trigger: 'blur' }
],
code: [{ required: true, message: t('page.login.register.codeRequired') }],
password: [
{ required: true, message: t('page.login.register.passwordRequired') },
{ min: 6, message: t('page.login.register.passwordLength') }
],
confirmPassword: createConfirmPwdRule(securityModel.password)
};
});
@@ -122,43 +184,61 @@ const terms = computed(() => {
// 步骤控制函数
async function nextStep() {
if (currentStep.value === 0) {
await validate();
// 复制电话号码到第三步
securityModel.email = basicModel.email;
}
if (currentStep.value === 1) {
if (!agreeTerms.value) {
window.$message?.error(t('page.login.register.agreeTermsFirst'));
return;
try {
if (currentStep.value === 0) {
// 验证第一步表单
await basicFormRef.value?.validate();
}
if (currentStep.value === 1) {
if (!agreeTerms.value) {
window.$message?.error(t('page.login.register.agreeTermsFirst'));
return;
}
}
// 验证通过后增加步骤
currentStep.value += 1;
} catch (error) {
// 验证失败时不增加步骤
console.error('Validation failed:', error);
}
currentStep.value += 1;
}
// 返回
function prevStep() {
currentStep.value -= 1;
}
//注册按钮
async function handleCaptcha() {
const res = await getCaptcha(securityModel.email);
if (res) {
securityModel.uuid = res.data.uuid;
if (res.data?.text) {
securityModel.code = res.data.text;
}
}
}
// 注册按钮
async function handleSubmit() {
try {
await validate();
await securityFormRef.value?.validate();
// 整合表单数据
model.username = basicModel.username;
model.password = securityModel.password;
model.email = securityModel.email;
model.email = securityModel.email; // 使用第三步的 email
model.fullName = basicModel.fullName;
model.gender = basicModel.gender; // 直接使用 gender 值
model.gender = basicModel.gender;
model.phone = basicModel.phone;
model.address = basicModel.address;
model.code = securityModel.code;
model.uuid = securityModel.uuid;
const success = await authStore.register({
...model,
age: dayjs().diff(dayjs(basicModel.birthDate), 'year'),
sex: model.gender, // 直接使用 gender 值,不需要转换
phonenumber: model.phone,
sex: model.gender,
phonenumber: model.phone
});
if (success) {
@@ -175,187 +255,196 @@ const showSteps = computed(() => !isMobile.value);
</script>
<template>
<ASteps
v-if="showSteps"
:current="currentStep"
size="small"
class="max-w-full mb-16px"
direction="horizontal"
:responsive="false"
>
<AStep :title="t('page.login.register.basicInfo')" />
<AStep :title="t('page.login.register.terms')" />
<AStep :title="t('page.login.register.security')" />
</ASteps>
<div class="register-container">
<ASteps
v-if="showSteps"
:current="currentStep"
size="small"
class="max-w-full mb-16px"
direction="horizontal"
:responsive="false"
>
<AStep :title="t('page.login.register.basicInfo')" />
<AStep :title="t('page.login.register.terms')" />
<AStep :title="t('page.login.register.security')" />
</ASteps>
<div v-else class="mobile-step-indicator mb-16px text-center">
{{ currentStep + 1 }}/3: {{
currentStep === 0
? t('page.login.register.basicInfo')
: currentStep === 1
? t('page.login.register.terms')
: t('page.login.register.security')
}}
</div>
<div v-else class="mobile-step-indicator mb-16px text-center">
{{ currentStep + 1 }}/3: {{
currentStep === 0
? t('page.login.register.basicInfo')
: currentStep === 1
? t('page.login.register.terms')
: t('page.login.register.security')
}}
</div>
<div class="step-content">
<!-- 第一步:基本信息 -->
<div v-if="currentStep === 0">
<AForm
ref="formRef"
:model="basicModel"
:rules="basicRules"
:label-wrap="true"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<ARow :gutter="[8,2]">
<ACol :span="24" :lg="24">
<AFormItem name="username" :label="t('page.login.register.username')">
<AInput v-model:value="basicModel.username" />
</AFormItem>
</ACol>
<ACol :span="24" :lg="24">
<AFormItem name="fullName" :label="t('page.login.register.fullName')">
<AInput v-model:value="basicModel.fullName" />
</AFormItem>
</ACol>
<ACol :xs="12" :sm="12" :lg="24">
<AFormItem name="birthDate" :label="t('page.login.register.birthDate')">
<ADatePicker
v-model:value="basicModel.birthDate"
class="!w-full birth-date-picker"
:placeholder="t('page.login.register.birthDatePlaceholder')"
:disabled-date="(current: dayjs.Dayjs) => current && current.isAfter(dayjs())"
<div class="step-content">
<!-- 第一步:基本信息 -->
<div v-show="currentStep === 0">
<AForm
ref="basicFormRef"
:model="basicModel"
:rules="basicRules"
:label-wrap="true"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<ARow :gutter="[8,2]">
<ACol :span="24" :lg="24">
<AFormItem name="username" :label="t('page.login.register.username')">
<AInput v-model:value="basicModel.username" />
</AFormItem>
</ACol>
<ACol :span="24" :lg="24">
<AFormItem name="fullName" :label="t('page.login.register.fullName')">
<AInput v-model:value="basicModel.fullName" />
</AFormItem>
</ACol>
<ACol :xs="12" :sm="12" :lg="24">
<AFormItem name="birthDate" :label="t('page.login.register.birthDate')">
<ADatePicker
v-model:value="basicModel.birthDate"
class="!w-full birth-date-picker"
:placeholder="t('page.login.register.birthDatePlaceholder')"
:disabled-date="(current: dayjs.Dayjs) => current && current.isAfter(dayjs())"
/>
</AFormItem>
</ACol>
<ACol :xs="12" :sm="12" :lg="24">
<AFormItem name="gender" :label="t('page.login.register.gender')">
<ASelect v-model:value="basicModel.gender">
<ASelectOption value="0">{{ t('page.login.register.male') }}</ASelectOption>
<ASelectOption value="1">{{ t('page.login.register.female') }}</ASelectOption>
</ASelect>
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<AFormItem name="phone" :label="t('page.login.register.phone')">
<AInput v-model:value="basicModel.phone" />
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<AFormItem name="address" :label="t('page.login.register.address')">
<ATextarea v-model:value="basicModel.address" :rows="2" />
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" @click="nextStep">
{{ t('page.login.register.next') }}
</AButton>
<AButton block size="small" @click="toggleLoginModule('pwd-login')">
{{ t('page.login.common.back') }}
</AButton>
</ASpace>
</ACol>
</ARow>
</AForm>
</div>
<!-- 第二步:协议 -->
<div v-show="currentStep === 1">
<ATextarea
:value="terms"
:rows="12"
readonly
class="mb-16px"
size="small"
:style="{ fontSize: '14px', lineHeight: '1.6' }"
/>
<div class="mb-16px">
<ACheckbox
v-model:checked="agreeTerms"
class="terms-checkbox"
>
{{ t('page.login.register.agreeTerms') }}
</ACheckbox>
</div>
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" :disabled="!agreeTerms" @click="nextStep">
{{ t('page.login.register.next') }}
</AButton>
<AButton block size="small" @click="prevStep">
{{ t('page.login.register.prev') }}
</AButton>
</ASpace>
</div>
<!-- 第三步:安全信息 -->
<div v-show="currentStep === 2">
<AForm
ref="securityFormRef"
:model="securityModel"
:rules="securityRules"
:label-wrap="true"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
class="compact-form"
>
<AFormItem name="email" :label="t('page.login.register.email')">
<AInput
v-model:value="securityModel.email"
:placeholder="t('page.login.common.emailPlaceholder')"
/>
</AFormItem>
<AFormItem name="code" :label="t('page.login.register.code')">
<div class="w-full flex-y-center gap-8px">
<AInput
v-model:value="securityModel.code"
:placeholder="t('page.login.common.codePlaceholder')"
/>
</AFormItem>
</ACol>
<ACol :xs="12" :sm="12" :lg="24">
<AFormItem name="gender" :label="t('page.login.register.gender')">
<ASelect v-model:value="basicModel.gender">
<ASelectOption value="male">{{ t('page.login.register.male') }}</ASelectOption>
<ASelectOption value="female">{{ t('page.login.register.female') }}</ASelectOption>
</ASelect>
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<AFormItem name="phone" :label="t('page.login.register.phone')">
<AInput v-model:value="basicModel.phone" />
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<AFormItem name="email" :label="t('page.login.register.email')">
<AInput v-model:value="basicModel.email" />
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<AFormItem name="address" :label="t('page.login.register.address')">
<ATextarea v-model:value="basicModel.address" :rows="2" />
</AFormItem>
</ACol>
<ACol :lg="24" :span="24">
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" @click="nextStep">
{{ t('page.login.register.next') }}
<AButton
size="small"
:disabled="isCounting"
:loading="loading"
@click="handleCaptcha()"
>
{{ label }}
</AButton>
<AButton block size="small" @click="toggleLoginModule('pwd-login')">
{{ t('page.login.common.back') }}
</div>
</AFormItem>
<AFormItem name="password" :label="t('page.login.register.password')">
<AInputPassword
v-model:value="securityModel.password"
:placeholder="t('page.login.common.passwordPlaceholder')"
/>
</AFormItem>
<AFormItem name="confirmPassword" :label="t('page.login.register.confirmPassword')">
<AInputPassword
v-model:value="securityModel.confirmPassword"
:placeholder="t('page.login.common.confirmPasswordPlaceholder')"
/>
</AFormItem>
<AFormItem :wrapper-col="{ xs: { span: 24 }, sm: { span: 24 } }">
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" @click="handleSubmit">
{{ t('common.confirm') }}
</AButton>
<AButton block size="small" @click="prevStep">
{{ t('page.login.register.prev') }}
</AButton>
</ASpace>
</ACol>
</ARow>
</AForm>
</div>
<!-- 第二步:协议 -->
<div v-if="currentStep === 1">
<ATextarea
:value="terms"
:rows="12"
readonly
class="mb-16px"
size="small"
:style="{ fontSize: '14px', lineHeight: '1.6' }"
/>
<div class="mb-16px">
<ACheckbox
v-model:checked="agreeTerms"
class="terms-checkbox"
>
{{ t('page.login.register.agreeTerms') }}
</ACheckbox>
</AFormItem>
</AForm>
</div>
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" :disabled="!agreeTerms" @click="nextStep">
{{ t('page.login.register.next') }}
</AButton>
<AButton block size="small" @click="prevStep">
{{ t('page.login.register.prev') }}
</AButton>
</ASpace>
</div>
<!-- 第三步:安全信息 -->
<div v-if="currentStep === 2">
<AForm
ref="formRef"
:model="securityModel"
:rules="securityRules"
:label-wrap="true"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
class="compact-form"
>
<AFormItem name="email" :label="t('page.login.register.email')">
<AInput
v-model:value="securityModel.email"
:placeholder="t('page.login.common.emailPlaceholder')"
/>
</AFormItem>
<AFormItem name="code" :label="t('page.login.register.code')">
<div class="w-full flex-y-center gap-8px">
<AInput
v-model:value="securityModel.code"
:placeholder="t('page.login.common.codePlaceholder')"
/>
<AButton
size="small"
:disabled="isCounting"
:loading="loading"
@click="getCaptcha(securityModel.email)"
>
{{ label }}
</AButton>
</div>
</AFormItem>
<AFormItem name="password" :label="t('page.login.register.password')">
<AInputPassword
v-model:value="securityModel.password"
:placeholder="t('page.login.common.passwordPlaceholder')"
/>
</AFormItem>
<AFormItem name="confirmPassword" :label="t('page.login.register.confirmPassword')">
<AInputPassword
v-model:value="securityModel.confirmPassword"
:placeholder="t('page.login.common.confirmPasswordPlaceholder')"
/>
</AFormItem>
<AFormItem :wrapper-col="{ xs: { span: 24 }, sm: { span: 24 } }">
<ASpace direction="vertical" size="small" class="w-full">
<AButton type="primary" block size="small" @click="handleSubmit">
{{ t('common.confirm') }}
</AButton>
<AButton block size="small" @click="prevStep">
{{ t('page.login.register.prev') }}
</AButton>
</ASpace>
</AFormItem>
</AForm>
</div>
</div>
</template>
<style scoped>
.register-container {
width: 100%;
}
.step-content {
position: relative;
}
.step-form {
width: 100%;
}
@media (max-width: 640px) {
:deep(.ant-form) {
.ant-form-item {
@@ -433,6 +522,7 @@ const showSteps = computed(() => !isMobile.value);
:deep(.ant-form-item-label) {
white-space: normal;
text-align: left;
> label {
height: auto !important;
padding-bottom: 4px;
+14 -3
View File
@@ -36,15 +36,26 @@ const rules = computed<RuleRecord>(() => {
//phone: formRules.phone,
email:formRules.email,
code:formRules.code,
password: formRules.pwd,
password: formRules.userPwd,
confirmPassword: createConfirmPwdRule(model.password)
};
});
async function handleSubmit() {
await validate();
// request to reset password
// await validate();
// // request to reset password
// $message?.success(t('page.login.common.validateSuccess'));
await validate();//验证表单内容
const { error } = await fetcodeReset({
email: model.email,
code: model.code,
password: model.password
});
if(!error){
$message?.success(t('page.login.common.validateSuccess'));
toggleLoginModule('pwd-login');
}
}
</script>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>历史账单</div>
</template>
<style scoped></style>
+594
View File
@@ -0,0 +1,594 @@
<template>
<div class="h-full">
<SimpleScrollbar>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ACard
:title="t('page.package.title')"
:bordered="false"
:body-style="{ flex: 1, overflow: 'hidden' }"
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
>
<template #extra>
<div class="flex items-center gap-2">
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-delete="false"
@add="handleAdd"
@refresh="getData"
/>
</div>
</template>
<ATable
ref="wrapperEl"
:columns="columns"
:data-source="data"
:loading="loading"
row-key="id"
size="small"
:pagination="mobilePagination"
:scroll="scrollConfig"
class="h-full"
/>
</ACard>
</div>
</SimpleScrollbar>
<!-- 新增套餐弹窗 -->
<AModal
v-model:open="showModal"
:title="t('page.package.title')"
@ok="handleOk"
@cancel="handleCancel"
width="600px"
>
<AForm
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<AFormItem :label="t('page.package.packagename')" name="packageName">
<AInput v-model:value="formState.packageName" :placeholder="t('page.package.plepackagename')" />
</AFormItem>
<AFormItem :label="t('page.package.period')" required>
<div class="flex gap-2">
<AFormItem name="periodNum" class="mb-0 flex-1">
<AInputNumber
v-model:value="formState.periodNum"
:placeholder="t('page.package.pleperiod')"
:min="1"
style="width: 100%"
/>
</AFormItem>
<AFormItem name="periodType" class="mb-0 flex-1">
<ASelect v-model:value="formState.periodType" :options="periodOptions" />
</AFormItem>
</div>
</AFormItem>
<AFormItem :label="t('page.package.price')" name="price" :rules="[{ required: true, message: t('page.package.pleprice') }]">
<AInputNumber
v-model:value="formState.price"
:placeholder="t('page.package.pleprice')"
:min="0.01"
:precision="2"
style="width: 100%"
:addon-before="currencyStore.symbol"
/>
</AFormItem>
<AFormItem :label="t('page.package.traffic')" name="traffic">
<div class="flex items-center gap-4">
<ASwitch v-model:checked="formState.trafficEnable" />
<div v-if="formState.trafficEnable" class="flex gap-2 flex-1">
<AInputNumber
v-model:value="formState.traffic"
:placeholder="t('page.package.pletraffic')"
:min="1"
:max="1024"
:precision="0"
style="width: 100%"
/>
<ASelect
v-model:value="formState.trafficUnit"
:options="storageUnits.map(unit => ({ label: unit, value: unit }))"
style="width: 100px"
/>
</div>
</div>
</AFormItem>
<AFormItem :label="t('page.package.limit')" name="rateLimitId">
<div class="flex items-center gap-4">
<ASwitch v-model:checked="formState.rateLimitEnable" />
<ASelect
v-if="formState.rateLimitEnable"
v-model:value="formState.rateLimitId"
:placeholder="t('page.package.plelimit')"
:options="rateLimitOptions"
:loading="!rateLimitOptions.length"
style="width: 100%"
/>
</div>
</AFormItem>
<AFormItem :label="t('page.package.duration')" name="duration">
<div class="flex items-center gap-4">
<ASwitch v-model:checked="formState.durationEnable" />
<div v-if="formState.durationEnable" class="flex gap-2 flex-1">
<AInputNumber
v-model:value="formState.duration"
:placeholder="t('page.package.pleduration')"
:min="1"
style="width: 100%"
/>
<ASelect
v-model:value="formState.durationUnit"
:options="timeUnits.map(item => ({
label: item.label,
value: item.key }))"
style="width: 100px"
/>
</div>
</div>
</AFormItem>
<AFormItem :label="t('page.package.client')" name="clientNum">
<div class="flex items-center gap-4">
<ASwitch v-model:checked="formState.clientNumEnable" />
<AInputNumber
v-if="formState.clientNumEnable"
v-model:value="formState.clientNum"
:placeholder="t('page.package.pleclient')"
:min="1"
:addon-after="t('page.package.unit')"
style="width: 100%"
/>
</div>
</AFormItem>
<AFormItem :label="t('page.package.usepackage')">
<ASwitch v-model:checked="formState.packageEnable" />
</AFormItem>
</AForm>
</AModal>
</div>
</template>
<script setup lang="tsx">
import { useTable } from '@/hooks/common/table';
import { SimpleScrollbar } from '~/packages/materials/src';
import { computed, shallowRef, ref, onMounted } from 'vue';
import { useElementSize } from '@vueuse/core';
import { fetchPackageList, addPackage, fetchRateLimitList, updatePackage, deletePackage } from '@/service/api/auth';
import { Button as AButton, message, Modal, Form as AForm, Input as AInput, InputNumber as AInputNumber, Select as ASelect, Switch as ASwitch, Tag as ATag } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { useCurrencyStore } from '@/views/billing/rule/modules/currency';
import {
formatBandwidth,
formatStorage,
useFormatTime,
convertStorage,
convertTime,
storageUnits,
useTimeUnits,
type StorageUnit,
type TimeUnit
} from '@/utils/units';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const timeUnits = useTimeUnits();
const formatTime = useFormatTime();
const wrapperEl = shallowRef<HTMLElement | null>(null);
const { height: wrapperElHeight } = useElementSize(wrapperEl);
const currencyStore = useCurrencyStore();
const scrollConfig = computed(() => {
return {
y: wrapperElHeight.value - 72,
x: 1000
};
});
const periodMap: Record<number, string> = {
1: t('page.package.day'),
2: t('page.package.month'),
3: t('page.package.year')
};
const { columns, columnChecks, data, loading, getData, mobilePagination } = useTable({
apiFn: async () => {
try {
const response = await fetchPackageList();
return {
data: {
rows: response.data || [],
total: response.data?.length || 0
}
};
} catch (err) {
return {
data: {
rows: [],
total: 0
}
};
}
},
immediate: true,
rowKey: 'id',
columns: (): AntDesign.TableColumn<Api.Auth.Package>[] => [
{
key: 'packageName',
dataIndex: 'packageName',
title: t('page.package.packagename'),
align: 'center'
},
{
key: 'periodNum',
dataIndex: 'periodNum',
title: t('page.package.period'),
align: 'center',
customRender: ({ record }) => {
const { periodNum, periodType } = record;
const unit = periodMap[periodType as number] || '';
return `${periodNum}${unit}`;
}
},
{
key: 'price',
dataIndex: 'price',
title: t('page.package.price')+currencyStore.symbol,
align: 'center',
customRender: ({ text }) => `${Number(text).toFixed(2)} ${currencyStore.symbol}`
},
{
key: 'traffic',
dataIndex: 'traffic',
title: t('page.package.traffic'),
align: 'center',
customRender: ({ text, record }) => {
if (!record.trafficEnable) return t('page.package.unlimit');
const { value, unit } = formatStorage(text);
return `${value} ${unit}`;
}
},
{
key: 'rateLimitId',
dataIndex: 'rateLimitId',
title: t('page.package.limit'),
align: 'center',
customRender: ({ record }) => {
if (!record.rateLimitEnable) return t('page.package.unlimit');
const rateLimit = rateLimitData.value.find(item => item.id === record.rateLimitId);
if (!rateLimit) return '-';
const upLimitText = rateLimit.upLimitEnable
? `${t('page.package.up')}:${formatBandwidth(rateLimit.upLimit).value} ${formatBandwidth(rateLimit.upLimit).unit}`
: `${t('page.package.up')}:${t('page.package.unlimit')}`;
const downLimitText = rateLimit.downLimitEnable
? `${t('page.package.down')}:${formatBandwidth(rateLimit.downLimit).value} ${formatBandwidth(rateLimit.downLimit).unit}`
: `${t('page.package.down')}:${t('page.package.unlimit')}`;
return `${rateLimit.rateLimitName} (${upLimitText}/${downLimitText})`;
}
},
{
key: 'duration',
dataIndex: 'duration',
title: t('page.package.duration'),
align: 'center',
customRender: ({ text, record }) => {
if (!record.durationEnable) return t('page.package.unlimit');
const { value, unit } = formatTime(text);
return `${value} ${unit}`;
}
},
{
key: 'clientNum',
dataIndex: 'clientNum',
title: t('page.package.client'),
align: 'center',
customRender: ({ text, record }) => {
if (!record.clientNumEnable) return t('page.package.unlimit');
return text;
}
},
{
key: 'packageEnable',
dataIndex: 'packageEnable',
title: t('page.package.status'),
align: 'center',
customRender: ({ text }) => (
<ATag color={text ? 'success' : 'error'}>
{text ? t('page.package.use') : t('page.package.unuse')}
</ATag>
)
},
{
key: 'operate',
title: t('page.package.operate'),
align: 'center',
width: 200,
customRender: ({ record }) => (
<div class="flex justify-center gap-2">
<AButton type="link" onClick={() => handleEdit(record)}>
{t('page.package.edit')}
</AButton>
<AButton type="link" danger onClick={() => handleDelete(record)}>
{t('page.package.delete')}
</AButton>
</div>
)
}
]
});
const showModal = ref(false);
const formRef = ref();
interface PackageForm {
id?: string;
packageName: string;
periodNum: number;
periodType: number;
price: string;
trafficEnable: boolean;
traffic: number;
trafficUnit: StorageUnit;
rateLimitEnable: boolean;
rateLimitId?: number;
durationEnable: boolean;
duration: number;
durationUnit: TimeUnit;
clientNumEnable: boolean;
clientNum: number;
packageEnable: boolean;
}
const formState = ref<PackageForm>({
packageName: '',
periodNum: 1,
periodType: 2,
price: '',
trafficEnable: false,
traffic: 0,
trafficUnit: 'GB',
rateLimitEnable: false,
rateLimitId: undefined,
durationEnable: false,
duration: 0,
durationUnit: 'hour',
clientNumEnable: false,
clientNum: 0,
packageEnable: true
});
const rules: Record<string, Rule[]> = {
packageName: [{ required: true, message: t('page.package.plepackagename'), trigger: 'blur' }],
periodNum: [{ required: true, message: t('page.package.pleperiod'), trigger: 'blur' }],
periodType: [{ required: true, message: t('page.package.pleunit'), trigger: 'change' }],
price: [{ required: true, message: t('page.package.pleprice'), trigger: 'blur' }],
traffic: [
{
validator: (_rule: Rule, value: number) => {
if (!formState.value.trafficEnable) return Promise.resolve();
if (!value || value <= 0) return Promise.reject(t('page.package.rejtraffic'));
return Promise.resolve();
},
trigger: 'blur'
}
],
rateLimitId: [
{
validator: (_rule: Rule, value: number) => {
if (!formState.value.rateLimitEnable) return Promise.resolve();
if (!value) return Promise.reject(t('page.package.plelimit'));
return Promise.resolve();
},
trigger: 'change'
}
],
duration: [
{
validator: (_rule: Rule, value: number) => {
if (!formState.value.durationEnable) return Promise.resolve();
if (!value || value <= 0) return Promise.reject(t('page.package.rejduration'));
return Promise.resolve();
},
trigger: 'blur'
}
],
clientNum: [
{
validator: (_rule: Rule, value: number) => {
if (!formState.value.clientNumEnable) return Promise.resolve();
if (!value || value <= 0) return Promise.reject(t('page.package.rejclient'));
return Promise.resolve();
},
trigger: 'blur'
}
]
};
const periodOptions =computed(()=> [
{ label: t('page.package.hour'), value: 0 },
{ label: t('page.package.day'), value: 1 },
{ label: t('page.package.month'), value: 2 },
{ label: t('page.package.year'), value: 3 }
]);
const rateLimitOptions = ref<{ label: string; value: number }[]>([]);
const rateLimitData = ref<Api.Auth.RateLimit[]>([]);
const getRateLimitData = async () => {
try {
const response = await fetchRateLimitList();
rateLimitData.value = response.data || [];
rateLimitOptions.value = rateLimitData.value.map(item => {
const upLimitText = item.upLimitEnable
? `${t('page.package.up')}:${formatBandwidth(item.upLimit).value} ${formatBandwidth(item.upLimit).unit}`
: `${t('page.package.up')}:${t('page.package.unlimit')}`;
const downLimitText = item.downLimitEnable
? `${t('page.package.down')}:${formatBandwidth(item.downLimit).value} ${formatBandwidth(item.downLimit).unit}`
: `${t('page.package.down')}:${t('page.package.unlimit')}`;
return {
label: `${item.rateLimitName} (${upLimitText}/${downLimitText})`,
value: item.id
};
});
} catch (error) {
// console.error('error:', error);
}
};
// 编辑状态
const isEdit = ref(false);
const editId = ref<string>();
const handleAdd = () => {
isEdit.value = false;
editId.value = undefined;
showModal.value = true;
getRateLimitData();
};
const handleCancel = () => {
showModal.value = false;
isEdit.value = false;
editId.value = undefined;
formState.value = {
packageName: '',
periodNum: 1,
periodType: 2,
price: '',
trafficEnable: false,
traffic: 0,
trafficUnit: 'GB',
rateLimitEnable: false,
rateLimitId: undefined,
durationEnable: false,
duration: 0,
durationUnit:'hour',
clientNumEnable: false,
clientNum: 0,
packageEnable: true
};
};
const handleOk = async () => {
try {
await formRef.value?.validate();
// 准备基础数据
const baseData = {
packageName: formState.value.packageName,
periodNum: formState.value.periodNum,
periodType: formState.value.periodType,
price: formState.value.price,
trafficEnable: formState.value.trafficEnable,
rateLimitEnable: formState.value.rateLimitEnable,
rateLimitId: formState.value.rateLimitId,
durationEnable: formState.value.durationEnable,
clientNumEnable: formState.value.clientNumEnable,
clientNum: formState.value.clientNum,
packageEnable: formState.value.packageEnable
};
// 计算流量和时长
const traffic = formState.value.trafficEnable
? Math.round(convertStorage(formState.value.traffic, formState.value.trafficUnit, 'B'))
: 0;
const duration = formState.value.durationEnable
? Math.round(convertTime(formState.value.duration, formState.value.durationUnit, 'second'))
: 0;
if (isEdit.value) {
if (!editId.value) {
message.error(t('page.package.isnull'));
return;
}
// 编辑模式
await updatePackage({
...baseData,
id: editId.value,
traffic,
duration
});
message.success(t('page.package.editsuc'));
} else {
// 新增模式
await addPackage({
...baseData,
traffic,
duration
});
message.success(t('page.package.addsuc'));
}
handleCancel();
getData();
} catch (error) {
// message.error(isEdit.value ? t('page.package.editerr') : t('page.package.adderr'));
}
};
const handleEdit = async (record: Api.Auth.Package) => {
isEdit.value = true;
editId.value = record.id;
showModal.value = true;
await getRateLimitData();
const traffic = formatStorage(record.traffic);
const duration = formatTime(record.duration);
formState.value = {
packageName: record.packageName,
periodNum: record.periodNum,
periodType: record.periodType,
price: record.price,
trafficEnable: record.trafficEnable,
traffic: traffic.value,
trafficUnit: traffic.unit,
rateLimitEnable: record.rateLimitEnable,
rateLimitId: record.rateLimitId,
durationEnable: record.durationEnable,
duration: duration.value,
durationUnit: duration.unit,
clientNumEnable: record.clientNumEnable,
clientNum: record.clientNum,
packageEnable: record.packageEnable
};
};
const handleDelete = (record: Api.Auth.Package) => {
Modal.confirm({
title: t('page.package.confirmdelete'),
content: t('page.package.deletecontent'),
okText: t('page.package.confirm'),
cancelText: t('page.package.close'),
okType: 'danger',
async onOk() {
try {
await deletePackage(record.id);
message.success(t('page.package.deletesuc'));
getData();
} catch (error) {
// message.error(t('page.package.deleteerr'));
}
}
});
};
onMounted(() => {
getRateLimitData();
});
</script>
<style scoped>
.h-full {
height: 100%;
}
</style>
+372
View File
@@ -0,0 +1,372 @@
<template>
<SimpleScrollbar>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ACard
:title="t('page.ratelimit.title')"
:bordered="false"
:body-style="{ flex: 1, overflow: 'hidden' }"
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
>
<template #extra>
<div class="flex items-center gap-2">
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-delete="false"
:show-add="false"
@refresh="getData"
@add="handleAdd"
/>
</div>
</template>
<ATable
ref="wrapperEl"
:columns="columns"
:data-source="data"
:loading="loading"
row-key="rateLimitName"
size="small"
:pagination="mobilePagination"
:scroll="scrollConfig"
class="h-full"
/>
</ACard>
<!-- 新增限速配置弹窗 -->
<AModal
v-model:visible="showModal"
:title="isEdit ? t('page.ratelimit.change') : t('page.ratelimit.add')"
@ok="handleOk"
@cancel="handleCancel"
>
<AForm
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 9 }"
:wrapper-col="{ span: 13 }"
>
<AFormItem :label="t('page.ratelimit.name')" name="rateLimitName">
<AInput v-model:value="formState.rateLimitName" :placeholder="t('page.ratelimit.plename')" />
</AFormItem>
<AFormItem :label="t('page.ratelimit.upratelimit')" name="upLimitEnable">
<ACheckbox v-model:checked="formState.upLimitEnable">{{ t('page.ratelimit.useuplimit') }}</ACheckbox>
</AFormItem>
<AFormItem
v-if="formState.upLimitEnable"
:label="t('page.ratelimit.uplimit')"
name="upLimit"
>
<div class="flex gap-2">
<AFormItem name="upLimit" class="mb-0 flex-1">
<AInputNumber
v-model:value="formState.upLimit"
:placeholder="t('page.ratelimit.pleuplimit')"
:min="1"
:max="1024"
:precision="0"
style="width: 100%"
/>
</AFormItem>
<AFormItem name="upLimitUnit" class="mb-0">
<ASelect
v-model:value="formState.upLimitUnit"
:options="bandwidthUnits.map(unit => ({ label: unit, value: unit }))"
style="width: 100px"
/>
</AFormItem>
</div>
</AFormItem>
<AFormItem :label="t('page.ratelimit.downratelimit')" name="downLimitEnable">
<ACheckbox v-model:checked="formState.downLimitEnable">{{ t('page.ratelimit.usedownlimit') }}</ACheckbox>
</AFormItem>
<AFormItem
v-if="formState.downLimitEnable"
:label="t('page.ratelimit.downlimit')"
name="downLimit"
>
<div class="flex gap-2">
<AFormItem name="downLimit" class="mb-0 flex-1">
<AInputNumber
v-model:value="formState.downLimit"
:placeholder="t('page.ratelimit.pledownlimit')"
:min="1"
:max="1024"
:precision="0"
style="width: 100%"
/>
</AFormItem>
<AFormItem name="downLimitUnit" class="mb-0">
<ASelect
v-model:value="formState.downLimitUnit"
:options="bandwidthUnits.map(unit => ({ label: unit, value: unit }))"
style="width: 100px"
/>
</AFormItem>
</div>
</AFormItem>
</AForm>
</AModal>
</div>
</SimpleScrollbar>
</template>
<script setup lang="tsx">
import { useTable } from '@/hooks/common/table';
import { SimpleScrollbar } from '~/packages/materials/src';
import { computed, shallowRef, ref } from 'vue';
import { useElementSize } from '@vueuse/core';
import { fetchRateLimitList, addRateLimit, editRateLimit, removeRateLimit } from '@/service/api/auth';
import { message, Modal, Button } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { bandwidthUnits, convertBandwidth, formatBandwidth, type BandwidthUnit } from '@/utils/units';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
interface RateLimitForm extends Omit<Api.Auth.RateLimitAdd, 'upLimit' | 'downLimit'> {
upLimit: number;
downLimit: number;
upLimitUnit: BandwidthUnit;
downLimitUnit: BandwidthUnit;
}
const wrapperEl = shallowRef<HTMLElement | null>(null);
const { height: wrapperElHeight } = useElementSize(wrapperEl);
const scrollConfig = computed(() => {
return {
y: wrapperElHeight.value - 72,
x: 1000
};
});
const showModal = ref(false);
const formRef = ref();
const formState = ref<RateLimitForm>({
rateLimitName: '',
upLimitEnable: false,
downLimitEnable: false,
upLimit: 0,
downLimit: 0,
upLimitUnit: 'Mbps',
downLimitUnit: 'Mbps'
});
const rules: Record<string, Rule[]> = {
rateLimitName: [{ required: true, message: t('page.ratelimit.plename'), trigger: 'blur' }],
upLimit: [
{
required: true,
message: t('page.ratelimit.pleuplimit'),
trigger: 'blur',
validator: (_rule: Rule, value: number) => {
if (!formState.value.upLimitEnable) return Promise.resolve();
if (!value || value <= 0) return Promise.reject(t('page.ratelimit.reject'));
return Promise.resolve();
}
}
],
downLimit: [
{
required: true,
message: t('page.ratelimit.pledownlimit'),
trigger: 'blur',
validator: (_rule: Rule, value: number) => {
if (!formState.value.downLimitEnable) return Promise.resolve();
if (!value || value <= 0) return Promise.reject(t('page.ratelimit.reject'));
return Promise.resolve();
}
}
]
};
const isEdit = ref(false);
const editId = ref<number>();
const handleAdd = () => {
isEdit.value = false;
editId.value = undefined;
formState.value = {
rateLimitName: '',
upLimitEnable: false,
downLimitEnable: false,
upLimit: 0,
downLimit: 0,
upLimitUnit: 'Mbps',
downLimitUnit: 'Mbps'
};
showModal.value = true;
};
const handleEdit = (record: Api.Auth.RateLimit) => {
isEdit.value = true;
editId.value = record.id;
formState.value = {
rateLimitName: record.rateLimitName,
upLimitEnable: record.upLimitEnable,
downLimitEnable: record.downLimitEnable,
upLimit: convertBandwidth(record.upLimit, 'Kbps', 'Mbps'),
downLimit: convertBandwidth(record.downLimit, 'Kbps', 'Mbps'),
upLimitUnit: 'Mbps',
downLimitUnit: 'Mbps'
};
showModal.value = true;
};
const handleDelete = (record: Api.Auth.RateLimit) => {
Modal.confirm({
title: t('page.ratelimit.confirmdelete'),
icon: () => <ExclamationCircleOutlined />,
content: `${t('page.ratelimit.deletecontant')} "${record.rateLimitName}"?`,
async onOk() {
try {
await removeRateLimit(record.id);
message.success(t('page.ratelimit.deletesuc'));
getData();
} catch (error) {
// console.error('error:', error);
}
}
});
};
const handleCancel = () => {
showModal.value = false;
formState.value = {
rateLimitName: '',
upLimitEnable: false,
downLimitEnable: false,
upLimit: 0,
downLimit: 0,
upLimitUnit: 'Mbps',
downLimitUnit: 'Mbps'
};
};
const handleOk = async () => {
try {
await formRef.value?.validate();
const submitData = {
...formState.value,
upLimit: formState.value.upLimitEnable
? Math.round(convertBandwidth(formState.value.upLimit, formState.value.upLimitUnit, 'Kbps'))
: 0,
downLimit: formState.value.downLimitEnable
? Math.round(convertBandwidth(formState.value.downLimit, formState.value.downLimitUnit, 'Kbps'))
: 0
};
if (isEdit.value) {
if (!editId.value) {
message.error(t('page.ratelimit.idnull'));
return;
}
const {error} = await editRateLimit({
id: editId.value,
rateLimitName: submitData.rateLimitName,
upLimitEnable: submitData.upLimitEnable,
downLimitEnable: submitData.downLimitEnable,
upLimit: submitData.upLimit,
downLimit: submitData.downLimit
});
if(!error) {
message.success(t('page.ratelimit.changesuc'));
}
} else {
const{ error } = await addRateLimit(submitData);
if(!error) {
message.success(t('page.ratelimit.addsuc'));
}
}
handleCancel();
getData();
} catch (error) {
// message.error(isEdit.value ? 'change error' : 'add error');
}
};
const {
columns,
columnChecks,
data,
loading,
getData,
mobilePagination
} = useTable({
apiFn: async () => {
try {
const response = await fetchRateLimitList();
return {
data: {
rows: response.data || [],
total: response.data?.length || 0
}
};
} catch (err) {
return {
data: {
rows: [],
total: 0
}
};
}
},
immediate: true,
rowKey: 'id',
columns: (): AntDesign.TableColumn<Api.Auth.RateLimit>[] => [
{
key: 'rateLimitName',
dataIndex: 'rateLimitName',
title: t('page.ratelimit.name'),
align: 'center'
},
{
key: 'upLimit',
dataIndex: 'upLimit',
title: t('page.ratelimit.upratelimit'),
align: 'center',
customRender: ({ text, record }) => {
if (!record.upLimitEnable) return t('page.ratelimit.unlimit');
const { value, unit } = formatBandwidth(text);
return `${value} ${unit}`;
}
},
{
key: 'downLimit',
dataIndex: 'downLimit',
title: t('page.ratelimit.downratelimit'),
align: 'center',
customRender: ({ text, record }) => {
if (!record.downLimitEnable) return t('page.ratelimit.unlimit');
const { value, unit } = formatBandwidth(text);
return `${value} ${unit}`;
}
},
{
title: t('page.ratelimit.operate'),
key: 'operate',
align: 'center',
width: 200,
fixed: 'right',
customRender: ({ record }) => (
<div class="flex justify-center gap-2">
<Button type="link" onClick={() => handleEdit(record)}>
{ t('page.ratelimit.changed') }
</Button>
<Button type="link" danger onClick={() => handleDelete(record)}>
{t('page.ratelimit.delete')}
</Button>
</div>
)
}
]
});
</script>
<style scoped></style>
+393
View File
@@ -0,0 +1,393 @@
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<!-- 货币设置卡片 -->
<ACard
:title="t('page.rule.title')"
:bordered="false"
class="flex-col-stretch"
>
<div class="currency-selector">
<ASpace size="large">
<div class="selector-item">
<span class="selector-label">{{ t('page.rule.currencySelect') }}:</span>
<ASelect
v-model:value="selectedCurrency"
style="width: 120px"
@change="handleCurrencyChange"
>
<ASelect.Option
v-for="currency in availableCurrencies"
:key="currency.code"
:value="currency.code"
>
{{ currency.code }}
</ASelect.Option>
</ASelect>
</div>
<div class="selector-item">
<span class="selector-label">{{ t('page.rule.currencySymbol') }}:</span>
<span class="currency-symbol">{{ currencyStore.symbol }}</span>
</div>
</ASpace>
</div>
<!-- 支付方式配置 -->
<div class="payment-selector">
<ASpace size="large">
<div class="selector-item">
<span class="selector-label">{{ t('page.rule.paymentMethods') }}:</span>
</div>
<div class="selector-item">
<ACheckbox.Group
v-model:value="selectedPaymentMethods"
:options="paymentOptions"
@change="handlePaymentMethodsChange"
/>
</div>
</ASpace>
</div>
<!-- 计费规则表格 -->
<ATable
:columns="columns"
:data-source="data"
:loading="loading"
row-key="id"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<ASpace>
<AButton type="primary" ghost @click="handleEdit(record)">{{ t('page.rule.edit') }}</AButton>
</ASpace>
</template>
</template>
</ATable>
</ACard>
<!-- 编辑抽屉 -->
<ADrawer
v-model:visible="drawerVisible"
:title="drawerTitle"
placement="right"
width="500px"
@close="closeDrawer"
>
<AForm
v-if="currentRule"
ref="formRef"
:model="currentRule"
:rules="rules"
layout="vertical"
>
<AFormItem :label="t('page.rule.traffic')" name="traffic">
<AInputNumber
v-model:value="currentRule.traffic"
:min="1"
:precision="0"
style="width: 100%"
:placeholder="t('page.rule.pletraffic')"
/>
</AFormItem>
<AFormItem :label="t('page.rule.price')" name="price">
<AInputNumber
v-model:value="currentRule.price"
:min="0"
:precision="2"
style="width: 100%"
:placeholder="t('page.rule.pleprice')"
/>
</AFormItem>
<AFormItem :label="t('page.rule.unit')" name="unit">
<ASelect
v-model:value="currentRule.unit"
style="width: 100%"
:placeholder="t('page.rule.pleunit')"
>
<ASelect.Option
v-for="option in unitOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</ASelect.Option>
</ASelect>
</AFormItem>
<AFormItem :label="t('page.rule.status')" name="enable">
<ASwitch v-model:checked="currentRule.enable" />
</AFormItem>
</AForm>
<template #footer>
<ASpace>
<AButton @click="closeDrawer">{{ t('page.rule.close') }}</AButton>
<AButton type="primary" :loading="submitLoading" @click="handleSubmit">{{ t('page.rule.confirm') }}</AButton>
</ASpace>
</template>
</ADrawer>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { fetchBillRuleList, updateBillRule, updatePaymentConfig, getPaymentConfig } from '@/service/api/auth';
import {
Card as ACard,
Table as ATable,
Space as ASpace,
Switch as ASwitch,
Button as AButton,
Drawer as ADrawer,
Form as AForm,
FormItem as AFormItem,
InputNumber as AInputNumber,
message,
Select as ASelect,
Descriptions as ADescriptions,
DescriptionsItem as ADescriptionsItem,
Checkbox as ACheckbox
} from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { useI18n } from 'vue-i18n';
import { useCurrencyStore, availableCurrencies } from '@/views/billing/rule/modules/currency';
const { t } = useI18n();
const currencyStore = useCurrencyStore();
const selectedCurrency = ref(currencyStore.code);
interface BillRule {
id: number;
traffic: number;
price: number;
unit: number;
enable: boolean;
createBy: string | null;
createTime: string;
updateBy: string | null;
updateTime: string | null;
}
interface BillRuleUpdate {
traffic: number;
price: number;
unit: number;
enable: boolean;
}
const loading = ref(false);
const submitLoading = ref(false);
const data = ref<BillRule[]>([]);
const drawerVisible = ref(false);
const drawerTitle = ref(t('page.rule.modaltitle'));
const currentRule = ref<BillRuleUpdate | null>(null);
const formRef = ref<FormInstance>();
// 单位选项
const unitOptions = [
{ label: 'B', value: 0 },
{ label: 'KB', value: 1 },
{ label: 'MB', value: 2 },
{ label: 'GB', value: 3 },
{ label: 'TB', value: 4 }
];
// 获取单位显示文本
const getUnitText = (unit: number) => {
return unitOptions.find(option => option.value === unit)?.label || '';
};
// 支付方式配置
const paymentOptions = [
{ label: 'PayPal', value: 'paypal' },
{ label: 'Stripe', value: 'stripe' },
{ label: 'Alipay', value: 'alipay' },
{ label: 'WeChat Pay', value: 'wxpay' },
];
const selectedPaymentMethods = ref<string[]>(['paypal','stripe']);
// 处理货币变更
const handleCurrencyChange = async (value: string) => {
currencyStore.setCurrency(value);
// 同步更新支付配置
try {
await updatePaymentConfig({
currency: value,
currencySymbol: currencyStore.symbol,
paymentMethods: selectedPaymentMethods.value
});
message.success(t('page.rule.updatesuc'));
} catch (error) {
message.error(t('page.rule.updatefail'));
}
};
// 处理支付方式变更
const handlePaymentMethodsChange = async (values: string[]) => {
selectedPaymentMethods.value = values;
try {
await updatePaymentConfig({
currency: selectedCurrency.value,
currencySymbol: currencyStore.symbol,
paymentMethods: values
});
message.success(t('page.rule.updatesuc'));
} catch (error) {
message.error(t('page.rule.updatefail'));
}
};
const columns: ColumnsType = [
{
title: t('page.rule.traffic'),
dataIndex: 'traffic',
key: 'traffic',
align: 'center',
width: 150
},
{
title: t('page.rule.price'),
dataIndex: 'price',
key: 'price',
align: 'center',
width: 150,
customRender: ({ text }) => `${currencyStore.symbol}${text}`
},
{
title: t('page.rule.unit'),
dataIndex: 'unit',
key: 'unit',
align: 'center',
width: 150,
customRender: ({ text }) => getUnitText(text)
},
{
title: t('page.rule.status'),
dataIndex: 'enable',
key: 'enable',
align: 'center',
width: 100,
customRender: ({ text: enable }) => (enable ? t('page.rule.use') : t('page.rule.unuse'))
},
{
title: t('page.rule.action'),
key: 'action',
align: 'center',
width: 100,
fixed: 'right'
}
];
const rules = {
traffic: [{ required: true, message: t('page.rule.pletraffic') }],
price: [{ required: true, message: t('page.rule.pleprice') }],
unit: [{ required: true, message: t('page.rule.pleunit') }]
};
// 获取列表数据
const getData = async () => {
loading.value = true;
try {
const res = await fetchBillRuleList();
if (res.data && Array.isArray(res.data)) {
data.value = res.data;
} else {
data.value = [];
}
} catch (error) {
data.value = [];
} finally {
loading.value = false;
}
};
// 获取支付配置
const getPayConfig = async () => {
try {
const res = await getPaymentConfig();
if (res.data) {
// 更新货币和支付方式
selectedCurrency.value = res.data.currency;
currencyStore.setCurrency(res.data.currency);
selectedPaymentMethods.value = res.data.paymentMethods;
}
} catch (error) {
message.error(t('page.rule.getConfigFail'));
}
};
// 打开编辑抽屉
const handleEdit = (record: BillRule) => {
currentRule.value = { ...record };
drawerVisible.value = true;
};
// 关闭抽屉
const closeDrawer = () => {
drawerVisible.value = false;
currentRule.value = null;
formRef.value?.resetFields();
};
// 提交表单
const handleSubmit = async () => {
if (!currentRule.value) return;
try {
await formRef.value?.validate();
submitLoading.value = true;
await updateBillRule(currentRule.value);
message.success(t('page.rule.updatesuc'));
closeDrawer();
getData();
} catch (error) {
} finally {
submitLoading.value = false;
}
};
onMounted(() => {
getData();
getPayConfig(); // 获取支付配置
});
</script>
<style scoped>
.currency-selector {
margin-bottom: 16px;
padding: 8px 0;
}
.selector-item {
display: flex;
align-items: center;
gap: 8px;
}
.selector-label {
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
}
.currency-symbol {
font-size: 16px;
font-weight: 500;
min-width: 20px;
display: inline-block;
}
.payment-selector {
margin-bottom: 16px;
padding: 8px 0;
}
:deep(.ant-form-item) {
margin-bottom: 24px;
}
:deep(.ant-descriptions-item) {
padding-bottom: 16px;
}
</style>
@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
interface CurrencyOption {
code: string
symbol: string
name: string
}
interface CurrencyState {
currentCurrency: CurrencyOption
}
export const availableCurrencies: CurrencyOption[] = [
{ code: 'USD', symbol: '$', name: '美元' },
{ code: 'CNY', symbol: '¥', name: '人民币' },
{ code: 'EUR', symbol: '€', name: '欧元' },
{ code: 'GBP', symbol: '£', name: '英镑' },
{ code: 'AUD', symbol: '$', name: '澳元' },
]
export const useCurrencyStore = defineStore('currency', {
state: (): CurrencyState => ({
currentCurrency: availableCurrencies[0]
}),
actions: {
setCurrency(currencyCode: string) {
const currency = availableCurrencies.find(c => c.code === currencyCode)
if (currency) {
this.currentCurrency = currency
}
}
},
getters: {
symbol: (state: CurrencyState): string => state.currentCurrency.symbol,
code: (state: CurrencyState): string => state.currentCurrency.code,
name: (state: CurrencyState): string => state.currentCurrency.name
}
})
@@ -11,22 +11,22 @@ import CreativityBanner from './modules/creativity-banner.vue';
<ASpace direction="vertical" :size="16">
<HeaderBanner />
<CardData />
<ARow :gutter="[16, 16]">
<ACol :span="24" :lg="14">
<LineChart />
</ACol>
<ACol :span="24" :lg="10">
<PieChart />
</ACol>
</ARow>
<ARow :gutter="[16, 16]">
<ACol :span="24" :lg="14">
<ProjectNews />
</ACol>
<ACol :span="24" :lg="10">
<CreativityBanner />
</ACol>
</ARow>
<!-- <ARow :gutter="[16, 16]">-->
<!-- <ACol :span="24" :lg="14">-->
<!-- <LineChart />-->
<!-- </ACol>-->
<!-- <ACol :span="24" :lg="10">-->
<!-- <PieChart />-->
<!-- </ACol>-->
<!-- </ARow>-->
<!-- <ARow :gutter="[16, 16]">-->
<!-- <ACol :span="24" :lg="14">-->
<!-- <ProjectNews />-->
<!-- </ACol>-->
<!-- <ACol :span="24" :lg="10">-->
<!-- <CreativityBanner />-->
<!-- </ACol>-->
<!-- </ARow>-->
</ASpace>
</template>
+662
View File
@@ -0,0 +1,662 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { TableColumnType } from 'ant-design-vue';
import {
EnvironmentOutlined,
SearchOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
InfoCircleOutlined
} from '@ant-design/icons-vue';
import { getDashboardSiteList, addSite, deleteSite, getSiteConfig, updateSite, getMeshConfig, updateMeshConfig, getRoamingConfig, updateRoamingConfig } from '@/service/api/auth';
import { useI18n } from 'vue-i18n';
import { Form, Modal, Divider, Checkbox, Select, Input, Button } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { regionOptions, timeZoneOptions } from '@/constants/site-options';
const { t } = useI18n();
defineOptions({
name: 'CardData'
});
// 表单实例
const formRef = ref();
const useForm = Form.useForm;
// 弹窗控制
const showAddDialog = ref(false);
// 表单数据
const formData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
});
// 表单验证规则
const { validate, validateInfos } = useForm(formData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }],
'deviceAccountSetting.username': [
{ required: true, message: t('page.carddata.usernameRequired') },
{
pattern: /^[\x21-\x7E]{1,64}$/,
message: t('page.carddata.usernameInvalid')
}
],
'deviceAccountSetting.password': [
{ required: true, message: t('page.carddata.passwordRequired') },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!#\$%&'\(\)\*\+,\-\.\/\:;\<\=\>\@\[\\\]\^_`\{\|\}\~])(?!.*[\x00-\x20\x22\x3f\x0f7]).{8,64}$/,
message: t('page.carddata.passwordInvalid')
}
]
});
// 使用场景选项
const scenarioOptions = [
{ label: t('page.carddata.office'), value: 'office' },
{ label: t('page.carddata.hotel'), value: 'hotel' },
{ label: t('page.carddata.education'), value: 'education' },
{ label: t('page.carddata.retail'), value: 'retail' },
{ label: t('page.carddata.other'), value: 'other' }
];
// 处理添加站点
const handleAddSite = async () => {
try {
await validate();
const response = await addSite(formData.value);
if (response) {
message.success(t('page.carddata.addSuccess'));
showAddDialog.value = false;
fetchSiteList(); // 刷新列表
}
} catch (error) {
console.error('Add site failed:', error);
message.error(t('page.carddata.addFailed'));
}
};
// 打开添加对话框
const openAddDialog = () => {
formData.value = {
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
};
showAddDialog.value = true;
};
// 搜索和分页状态
const searchValue = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const loading = ref(false);
const total = ref(0);
const siteData = ref<Api.DashboardSite[]>([]);
const fetchSiteList = async () => {
try {
loading.value = true;
const { data } = await getDashboardSiteList({
pageNum: currentPage.value,
pageSize: pageSize.value,
searchKey: searchValue.value
});
console.log('API Response:', data);
if (data) {
siteData.value = data.rows || [];
total.value = data.total || 0;
console.log('Processed Site Data:', siteData.value);
console.log('Total:', total.value);
} else {
siteData.value = [];
total.value = 0;
}
}catch (error){
console.error('Failed to fetch site list:', error);
siteData.value = [];
total.value = 0;
}finally {
loading.value = false;
}
};
// 修改监听方式
watch([currentPage, pageSize, searchValue], () => {
fetchSiteList();
}, { immediate: true });
// 表格列定义
const columns: TableColumnType<Api.DashboardSite>[] = [
{
title: t('page.carddata.sitename'),
key: 'name',
dataIndex: 'name'
},
{
title: t('page.carddata.country'),
key: 'region',
dataIndex: 'region'
},
{
title: t('page.carddata.alert'),
key: 'alerts',
width: 100
},
// {
// title: t('page.carddata.gateway'),
// key: 'gateway',
// width: 100
// },
// {
// title: t('page.carddata.switches'),
// key: 'switches',
// width: 100
// },
// {
// title: 'OLTS',
// key: 'olts',
// width: 100
// },
{
title: 'EAPS',
key: 'eaps',
width: 100
},
{
title: t('page.carddata.clients'),
key: 'clients',
width: 150
},
{
title: 'ACTION',
key: 'action',
width: 100,
fixed: 'right'
}
];
// 按钮操作处理函数
//
// const handleCopy = (record: Api.DashboardSite) => {
// console.log('Copy:', record);
// };
//
const handleDelete = (record: Api.DashboardSite) => {
Modal.confirm({
title: t('page.carddata.deleteConfirmTitle'),
content: t('page.carddata.deleteConfirmContent', { name: record.name }),
okText: t('page.carddata.confirm'),
cancelText: t('page.carddata.cancel'),
okType: 'danger',
async onOk() {
try {
await deleteSite(record.siteId);
message.success(t('page.carddata.deleteSuccess'));
fetchSiteList(); // 刷新列表
} catch (error) {
}
}
});
};
//
// const handleHome = (record: Api.DashboardSite) => {
// console.log('Home:', record);
// };
// 分页处理函数
const handlePageChange = (page: number, pageSize: number) => {
currentPage.value = page;
if (pageSize !== undefined) {
handlePageSizeChange(pageSize);
}
};
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
};
// 编辑弹窗控制
const showEditDialog = ref(false);
// 编辑表单数据
const editFormData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
meshEnable: false,
autoFailoverEnable: false,
defGatewayEnable: true,
gateway: '',
fullSector: false,
fastRoamingEnable: false,
nonStickRoamingEnable: false,
aiRoamingEnable: false
});
// 当前编辑的站点ID
const currentEditSiteId = ref('');
// 编辑表单验证规则
const { validate: validateEdit, validateInfos: validateEditInfos } = useForm(editFormData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }]
});
// IP分段输入
const gatewayIpParts = ref(['', '', '', '']);
// 拆分IP到4段
function splitGatewayIp(ip: string) {
if (!ip) return ['', '', '', ''];
const parts = ip.split('.');
return [parts[0] || '', parts[1] || '', parts[2] || '', parts[3] || ''];
}
// 拼接4段为IP
function joinGatewayIp(parts: string[]) {
return parts.map(p => p.trim()).join('.');
}
// 弹窗打开时同步分段
watch(() => editFormData.value.gateway, (val: any) => {
gatewayIpParts.value = splitGatewayIp(val);
}, { immediate: true });
// 分段输入时同步到gateway
watch(gatewayIpParts, (val: any) => {
editFormData.value.gateway = joinGatewayIp(val);
}, { deep: true });
// 处理编辑按钮点击
const handleEdit = async (record: Api.DashboardSite) => {
try {
currentEditSiteId.value = record.siteId;
const [siteRes, meshRes, roamingRes] = await Promise.all([
getSiteConfig(record.siteId),
getMeshConfig(record.siteId),
getRoamingConfig(record.siteId)
]);
if (siteRes.data) {
editFormData.value = {
name: siteRes.data.name,
region: siteRes.data.region,
timeZone: siteRes.data.timeZone,
scenario: siteRes.data.scenario,
meshEnable: meshRes.data?.mesh?.meshEnable ?? false,
autoFailoverEnable: meshRes.data?.mesh?.autoFailoverEnable ?? false,
defGatewayEnable: meshRes.data?.mesh?.defGatewayEnable ?? true,
gateway: meshRes.data?.mesh?.gateway ?? '',
fullSector: meshRes.data?.mesh?.fullSector ?? false,
fastRoamingEnable: roamingRes.data?.roaming?.fastRoamingEnable ?? false,
nonStickRoamingEnable: roamingRes.data?.roaming?.nonStickRoamingEnable ?? false,
aiRoamingEnable: roamingRes.data?.roaming?.aiRoamingEnable ?? false
};
gatewayIpParts.value = splitGatewayIp(editFormData.value.gateway);
showEditDialog.value = true;
}
} catch (error) {
console.error('Get site config failed:', error);
message.error(t('page.carddata.getConfigFailed'));
}
};
// 处理更新站点
const handleUpdateSite = async () => {
try {
await validateEdit();
// 先保存基础配置
await updateSite(currentEditSiteId.value, {
name: editFormData.value.name,
region: editFormData.value.region,
timeZone: editFormData.value.timeZone,
scenario: editFormData.value.scenario
});
// mesh 配置为嵌套结构,meshEnable为true时才携带defGatewayEnable、gateway、fullSector
const meshData: any = { mesh: { meshEnable: editFormData.value.meshEnable } };
if (editFormData.value.meshEnable) {
meshData.mesh.autoFailoverEnable = editFormData.value.autoFailoverEnable;
meshData.mesh.defGatewayEnable = editFormData.value.defGatewayEnable;
meshData.mesh.fullSector = editFormData.value.fullSector;
if (editFormData.value.defGatewayEnable === false) {
meshData.mesh.gateway = editFormData.value.gateway;
}
}
// roaming 配置为嵌套结构
const roamingData: any = { roaming: { fastRoamingEnable: editFormData.value.fastRoamingEnable } };
if (editFormData.value.fastRoamingEnable) {
roamingData.roaming.nonStickRoamingEnable = editFormData.value.nonStickRoamingEnable;
roamingData.roaming.aiRoamingEnable = editFormData.value.aiRoamingEnable;
}else {
roamingData.roaming.nonStickRoamingEnable=false;
roamingData.roaming.aiRoamingEnable = false;
}
await Promise.all([
updateMeshConfig(currentEditSiteId.value, meshData),
updateRoamingConfig(currentEditSiteId.value, roamingData)
]);
message.success(t('page.carddata.updateSuccess'));
showEditDialog.value = false;
fetchSiteList(); // 刷新列表
} catch (error) {
console.error('Update site failed:', error);
message.error(t('page.carddata.updateFailed'));
}
};
</script>
<template>
<ACard :bordered="false" size="small" class="card-wrapper">
<div class="flex justify-between items-center mb-16px">
<div class="flex items-center gap-8px">
<span class="text-16px font-medium">{{ t('page.carddata.sitelist') }}</span>
</div>
<div class="flex items-center gap-16px">
<AInput
v-model:value="searchValue"
:placeholder="t('page.carddata.search')"
class="w-240px"
allow-clear
>
<template #prefix>
<search-outlined />
</template>
</AInput>
<AButton type="primary" @click="openAddDialog">
<template #icon>
<plus-outlined />
</template>
{{ t('page.carddata.addsite') }}
</AButton>
</div>
</div>
<ATable
:columns="columns"
:data-source="siteData"
:pagination="{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${t('page.carddata.total')} ${total} `,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange
}"
row-key="siteId"
:scroll="{ x: 1200 }"
:loading="loading"
>
<!-- 添加一个调试信息 -->
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<!-- 添加调试信息 -->
{{ console.log('Rendering cell:', column.key, record) }}
<template v-if="column.key === 'name'">
<div class="flex items-center gap-8px">
<environment-outlined class="text-16px" />
<span>{{ record.name }}</span>
</div>
</template>
<template v-else-if="column.key === 'region'">
<span>{{ record.region }}</span>
</template>
<template v-else-if="column.key === 'alerts'">
<span>0</span>
</template>
<template v-else-if="column.key === 'gateway'">
<span>{{ record.connectedGatewayNum }}/{{ record.disconnectedGatewayNum }}</span>
</template>
<template v-else-if="column.key === 'switches'">
<span>{{ record.connectedSwitchNum }}/{{ record.disconnectedSwitchNum }}</span>
</template>
<template v-else-if="column.key === 'olts'">
<span>0/0</span>
</template>
<template v-else-if="column.key === 'eaps'">
<span>{{ record.connectedApNum }}/{{ record.disconnectedApNum }}/{{ record.isolatedApNum }}</span>
</template>
<template v-else-if="column.key === 'clients'">
<span>{{ record.wiredClientNum }}/{{ record.wirelessClientNum }}/{{ record.guestNum }}</span>
</template>
<template v-else-if="column.key === 'action'">
<div class="flex items-center gap-8px">
<edit-outlined class="cursor-pointer text-primary" @click="handleEdit(record)" />
<!-- <copy-outlined class="cursor-pointer text-primary" @click="handleCopy(record)" />-->
<delete-outlined class="cursor-pointer text-red-500" @click="handleDelete(record)" />
<!-- <home-outlined class="cursor-pointer text-primary" @click="handleHome(record)" />-->
</div>
</template>
</template>
</ATable>
<!-- 添加站点对话框 -->
<AModal
v-model:visible="showAddDialog"
:title="t('page.carddata.addsite')"
@ok="handleAddSite"
@cancel="showAddDialog = false"
:maskClosable="false"
>
<AForm
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateInfos.name"
>
<AInput v-model:value="formData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateInfos.region"
>
<ASelect
v-model:value="formData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateInfos.timeZone"
>
<ASelect
v-model:value="formData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateInfos.scenario"
>
<ASelect
v-model:value="formData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
<AFormItem
name="deviceAccountSetting.username"
:label="t('page.carddata.username')"
v-bind="validateInfos['deviceAccountSetting.username']"
>
<AInput v-model:value="formData.deviceAccountSetting.username" />
</AFormItem>
<AFormItem
name="deviceAccountSetting.password"
:label="t('page.carddata.password')"
v-bind="validateInfos['deviceAccountSetting.password']"
>
<AInputPassword v-model:value="formData.deviceAccountSetting.password" />
</AFormItem>
</AForm>
</AModal>
<!-- 编辑站点对话框 -->
<AModal
v-model:visible="showEditDialog"
:title="t('page.carddata.editsite')"
@ok="handleUpdateSite"
@cancel="showEditDialog = false"
:maskClosable="false"
:width="700"
>
<AForm
:model="editFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 24 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateEditInfos.name"
>
<AInput v-model:value="editFormData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateEditInfos.region"
>
<ASelect
v-model:value="editFormData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateEditInfos.timeZone"
>
<ASelect
v-model:value="editFormData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateEditInfos.scenario"
>
<ASelect
v-model:value="editFormData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
<a-divider />
<div style="margin-bottom: 8px; font-weight: bold;">{{ t('page.carddata.wireless') }}</div>
<AFormItem>
<a-checkbox v-model:checked="editFormData.meshEnable">Mesh</a-checkbox>
</AFormItem>
<AFormItem v-if="editFormData.meshEnable" style="margin-left: 24px;">
{{t('page.carddata.conDetection')}}
<ASelect v-model:value="editFormData.defGatewayEnable" style="width: 200px;">
<ASelectOption :value="true">{{ t('page.carddata.autoRec') }}</ASelectOption>
<ASelectOption :value="false">{{ t('page.carddata.custom') }}</ASelectOption>
</ASelect>
<template v-if="editFormData.defGatewayEnable === false">
<div style="display: inline-flex; align-items: center; margin-left: 12px;">
<AInput v-for="(part, idx) in gatewayIpParts" :key="idx" v-model:value="gatewayIpParts[idx]" maxlength="3" style="width: 48px; text-align: center; margin-right: 4px;" />
<span v-if="idx < 3" v-for="idx in 3" :key="'dot'+idx">.</span>
</div>
<a-tooltip placement="topLeft" :title="t('page.carddata.meshtext')">
<a-button type="ghost" color="blue"><InfoCircleOutlined /></a-button>
</a-tooltip>
</template>
</AFormItem>
<AFormItem v-if="editFormData.meshEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.autoFailoverEnable">{{ t('page.carddata.autoFailover') }}</a-checkbox>
</AFormItem>
<AFormItem v-if="editFormData.meshEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.fullSector">Full-Sector DFS</a-checkbox>
</AFormItem>
<AFormItem>
<a-checkbox v-model:checked="editFormData.fastRoamingEnable">{{ t('page.carddata.fastRoaming') }}</a-checkbox>
</AFormItem>
<!-- <AFormItem v-if="editFormData.fastRoamingEnable" style="margin-left: 24px;">-->
<!-- <a-checkbox v-model:checked="editFormData.nonStickRoamingEnable">Non-Stick Roaming</a-checkbox>-->
<!-- </AFormItem>-->
<AFormItem v-if="editFormData.fastRoamingEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.aiRoamingEnable">{{ t('page.carddata.aiRoaming') }}</a-checkbox>
</AFormItem>
</AForm>
</AModal>
</ACard>
</template>
<style scoped>
.card-wrapper {
margin-bottom: 16px;
}
.cursor-pointer {
cursor: pointer;
}
.w-240px {
width: 240px;
}
</style>
@@ -0,0 +1,341 @@
<script setup lang="ts">
import {
EnvironmentOutlined,
SafetyOutlined,
HddOutlined,
GroupOutlined,
UserAddOutlined,
UserSwitchOutlined
} from '@ant-design/icons-vue';
import { computed, ref, onMounted } from 'vue';
import { getDashboardOverview } from '@/service/api/auth';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
const { t } = useI18n();
const router = useRouter();
//AP跳转
const handleApClick = () => {
router.push('/device/apdevice');
};
//用户跳转
const handleUserClick = () =>{
router.push('/user-center/user')
};
//终端跳转
const handleterminalClick = () => {
router.push('/device/terminal');
};
//告警跳转
const handAlertsClick = () => {
router.push('/device/alerts');
};
defineOptions({
name: 'HeaderBanner'
});
const overviewData = ref<Api.DashboardOverview>({
cloud: { connected: false },
siteNum: 0,
totalGatewayNum: 0,
connectedGatewayNum: 0,
disconnectedGatewayNum: 0,
totalSwitchNum: 0,
connectedSwitchNum: 0,
disconnectedSwitchNum: 0,
totalApNum: 0,
connectedApNum: 0,
disconnectedApNum: 0,
isolatedApNum: 0,
totalClientNum: 0,
wiredClientNum: 0,
wirelessClientNum: 0,
guestNum: 0,
registerUserNum: 0,
onlineUserNum: 0
});
const fetchOverviewData = async () => {
try {
const { data } = await getDashboardOverview();
if (data) {
overviewData.value = {
cloud: { connected: data.cloud?.connected ?? false },
siteNum: data.siteNum ?? 0,
totalGatewayNum: data.totalGatewayNum ?? 0,
connectedGatewayNum: data.connectedGatewayNum ?? 0,
disconnectedGatewayNum: data.disconnectedGatewayNum ?? 0,
totalSwitchNum: data.totalSwitchNum ?? 0,
connectedSwitchNum: data.connectedSwitchNum ?? 0,
disconnectedSwitchNum: data.disconnectedSwitchNum ?? 0,
totalApNum: data.totalApNum ?? 0,
connectedApNum: data.connectedApNum ?? 0,
disconnectedApNum: data.disconnectedApNum ?? 0,
isolatedApNum: data.isolatedApNum ?? 0,
totalClientNum: data.totalClientNum ?? 0,
wiredClientNum: data.wiredClientNum ?? 0,
wirelessClientNum: data.wirelessClientNum ?? 0,
guestNum: data.guestNum ?? 0,
registerUserNum: data.registerUserNum ?? 0,
onlineUserNum: data.onlineUserNum ?? 0
};
}
} catch (error) {
}
};
onMounted(() => {
fetchOverviewData();
});
const deviceStatus = computed(() => {
const status = {
ap: {
connected: Number(overviewData.value.connectedApNum) || 0,
disconnected: Number(overviewData.value.disconnectedApNum) || 0,
isolated: Number(overviewData.value.isolatedApNum) || 0
},
client: {
wiredUsers: Number(overviewData.value.wiredClientNum) || 0,
wirelessUsers: Number(overviewData.value.wirelessClientNum) || 0,
wirelessGuests: Number(overviewData.value.guestNum) || 0
},
users: {
registered: Number(overviewData.value.registerUserNum) || 0,
online: Number(overviewData.value.onlineUserNum) || 0
},
alerts: 0
};
return status;
});
const siteInfo = computed(() => ({
total: Number(overviewData.value.siteNum) || 0
}));
const otherDevices = computed(() => ({
gateways: {
total: Number(overviewData.value.totalGatewayNum) || 0,
connected: Number(overviewData.value.connectedGatewayNum) || 0,
disconnected: Number(overviewData.value.disconnectedGatewayNum) || 0
},
switches: {
total: Number(overviewData.value.totalSwitchNum) || 0,
connected: Number(overviewData.value.connectedSwitchNum) || 0,
disconnected: Number(overviewData.value.disconnectedSwitchNum) || 0
},
olts: {
total: 0,
connected: 0,
disconnected: 0
}
}));
</script>
<template>
<ACard :bordered="false" class="card-wrapper">
<div class="text-16px font-bold mb-4px">{{ t('page.headerbanner.controller') }}</div>
<ARow :gutter="[16, 16]">
<ACol :span="24">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8px">
<!-- Connected Status -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-blue-50 rounded-lg">
<icon-mdi-cloud class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.connected') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="text-12px text-gray-500">
{{ t('page.headerbanner.cloud') }}
</div>
</div>
<!-- Sites -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-gray-100 rounded-lg relative">
<environment-outlined class="text-24px text-primary" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ siteInfo.total }}</span>
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.sites') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.totalsite') }}: {{ siteInfo.total }}</span>
</div>
</div>
<!-- &lt;!&ndash; Gateways &ndash;&gt;-->
<!-- <div class="flex flex-col p-6px">-->
<!-- <div class="flex items-center gap-6px mb-6px">-->
<!-- <div class="size-48px flex-center bg-gray-100 rounded-lg relative">-->
<!-- <safety-outlined class="text-24px text-primary" />-->
<!-- <span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.gateways.connected + otherDevices.gateways.disconnected }}</span>-->
<!-- </div>-->
<!-- <div class="text-16px font-medium">{{ t('page.headerbanner.gateway') }}</div>-->
<!-- </div>-->
<!-- <div class="border-t border-gray-100 my-4px"></div>-->
<!-- <div class="flex flex-col text-12px text-gray-500">-->
<!-- <span>{{ t('page.headerbanner.connect') }}: {{ otherDevices.gateways.connected }}</span>-->
<!-- <span>{{ t('page.headerbanner.disconnect') }}: {{ otherDevices.gateways.disconnected }}</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- &lt;!&ndash; Switches &ndash;&gt;-->
<!-- <div class="flex flex-col p-6px">-->
<!-- <div class="flex items-center gap-6px mb-6px">-->
<!-- <div class="size-48px flex-center bg-gray-100 rounded-lg relative">-->
<!-- <hdd-outlined class="text-24px text-primary" />-->
<!-- <span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.switches.connected + otherDevices.switches.disconnected }}</span>-->
<!-- </div>-->
<!-- <div class="text-16px font-medium">{{ t('page.headerbanner.switches') }}</div>-->
<!-- </div>-->
<!-- <div class="border-t border-gray-100 my-4px"></div>-->
<!-- <div class="flex flex-col text-12px text-gray-500">-->
<!-- <span>{{ t('page.headerbanner.connect') }}: {{ otherDevices.switches.connected }}</span>-->
<!-- <span>{{ t('page.headerbanner.disconnect') }}: {{ otherDevices.switches.disconnected }}</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- &lt;!&ndash; OLTs &ndash;&gt;-->
<!-- <div class="flex flex-col p-6px">-->
<!-- <div class="flex items-center gap-6px mb-6px">-->
<!-- <div class="size-48px flex-center bg-gray-100 rounded-lg relative">-->
<!-- <group-outlined class="text-24px text-primary" />-->
<!-- <span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.olts.total }}</span>-->
<!-- </div>-->
<!-- <div class="text-16px font-medium">OLTs</div>-->
<!-- </div>-->
<!-- <div class="border-t border-gray-100 my-4px"></div>-->
<!-- <div class="flex flex-col text-12px text-gray-500">-->
<!-- <span>{{ t('page.headerbanner.connect') }}: {{ otherDevices.olts.connected }}</span>-->
<!-- <span>{{ t('page.headerbanner.disconnect') }}: {{ otherDevices.olts.disconnected }}</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- Register Users -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-blue-50 rounded-lg relative hover:bg-green-100">
<user-add-outlined class="text-primary text-24px cursor-pointer" @click="handleUserClick"/>
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.register') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.registuser') }}: {{ deviceStatus.users.registered }}</span>
</div>
</div>
<!-- Online Users -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-green-50 rounded-lg">
<user-switch-outlined class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.online') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.onlineuser') }}: {{ deviceStatus.users.online }}</span>
</div>
</div>
<!-- AP Status -->
<div class="flex flex-col p-6px" >
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-green-50 rounded-lg relative hover:bg-green-100">
<icon-mdi-access-point class="text-primary text-24px cursor-pointer" @click="handleApClick" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">
{{ overviewData.totalApNum }}
</span>
</div>
<div class="text-16px font-medium">AP</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.connect') }}: {{ deviceStatus.ap.connected }}</span>
<span>{{ t('page.headerbanner.disconnect') }}: {{ deviceStatus.ap.disconnected }}</span>
<span>{{ t('page.headerbanner.isolated') }}: {{ deviceStatus.ap.isolated }}</span>
</div>
</div>
<!-- Client Status -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-purple-50 rounded-lg relative hover:bg-green-100">
<icon-mdi-laptop class="text-primary text-24px cursor-pointer" @click="handleterminalClick"/>
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">
{{ overviewData.totalClientNum }}
</span>
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.client') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.wired') }}: {{ deviceStatus.client.wiredUsers }}</span>
<span>{{ t('page.headerbanner.wireless') }}: {{ deviceStatus.client.wirelessUsers }}</span>
<span>{{ t('page.headerbanner.guest') }}: {{ deviceStatus.client.wirelessGuests }}</span>
</div>
</div>
<!-- Alerts -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-yellow-50 rounded-lg relative hover:bg-green-100">
<icon-mdi-alert class="text-warning text-24px cursor-pointer" @click="handAlertsClick"/>
</div>
<div class="text-16px font-medium">{{ t('page.headerbanner.alerts') }}</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>{{ t('page.headerbanner.alert') }}: {{ deviceStatus.alerts || 0 }}</span>
</div>
</div>
</div>
</ACol>
</ARow>
</ACard>
</template>
<style scoped>
.card-wrapper {
margin-bottom: 16px;
padding: 12px;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.size-48px {
width: 48px;
height: 48px;
position: relative;
}
.flex-col > span {
line-height: 1.5;
}
.bg-gray-50 {
background-color: #fafafa;
}
.p-6px {
padding: 6px;
}
.mt-8px {
margin-top: 8px;
}
.mb-6px {
margin-bottom: 6px;
}
</style>
+682
View File
@@ -0,0 +1,682 @@
<template>
<SimpleScrollbar>
<a-card
:bordered="false"
style="box-shadow: 0 2px 8px #f0f1f2; border-radius: 8px; padding: 0 24px; margin: 24px;"
>
<div>
<!-- 标题 -->
<h2 style="font-weight: bold; font-size: 22px; margin-bottom: 8px;">{{ t('page.access.title') }}</h2>
<!-- 站点选择框 -->
<div style="display: flex; align-items: center; margin-bottom: 24px;">
<a-select
v-model:value="selectedSiteId"
:loading="siteLoading"
:placeholder="t('page.access.plesite')"
style="width: 200px; margin-right: 16px;"
@change="handleSiteChange"
>
<a-select-option v-for="site in siteList" :key="site.siteId" :value="site.siteId">
{{ site.name }}
</a-select-option>
</a-select>
</div>
<!-- Pre-Authentication Access -->
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px;">
<span>{{ t('page.access.authaccess') }}</span>
<a-switch v-model:checked="preAuthEnabled" />
<span>{{ t('page.access.enable') }}</span>
<a-tooltip title="Pre-Authentication Access 说明">
<i class="anticon anticon-info-circle" style="color: #13c2c2; margin-left: 4px;"></i>
</a-tooltip>
</div>
<div v-if="preAuthEnabled">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<span>{{ t('page.access.authaccesslist') }}</span>
<a-button type="link" style="color: #13c2c2; font-weight: bold;" @click="onAddPreAuth"><i class="anticon anticon-plus-circle" />
{{ t('page.access.add') }}</a-button>
</div>
<a-table
:columns="preAuthColumns"
:dataSource="preAuthList"
:pagination="false"
bordered
size="middle"
rowKey="key"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-tooltip :title="t('page.access.delete')">
<a-button
type="link"
size="small"
style="color: #ff4d4f"
@click="() => handleDeletePreAuth(record)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</template>
</template>
</a-table>
</div>
<!-- Authentication-Free Client -->
<div style="margin: 32px 0 16px 0; display: flex; align-items: center; gap: 12px;">
<span>{{ t('page.access.authclient') }}</span>
<a-switch v-model:checked="freeClientEnabled" />
<span>{{ t('page.access.enable') }}</span>
<a-tooltip title="Authentication-Free Client 说明">
<i class="anticon anticon-info-circle" style="color: #13c2c2; margin-left: 4px;"></i>
</a-tooltip>
</div>
<div v-if="freeClientEnabled">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<span>{{ t('page.access.authclientlist') }}</span>
<a-button type="link" style="color: #13c2c2; font-weight: bold;" @click="onAddFreeClient"><i class="anticon anticon-plus-circle" />
{{ t('page.access.add') }}</a-button>
</div>
<a-table
:columns="freeClientColumns"
:dataSource="freeClientList"
:pagination="false"
bordered
size="middle"
rowKey="key"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-tooltip :title="t('page.access.delete')">
<a-button
type="link"
size="small"
style="color: #ff4d4f"
@click="() => handleDeleteFreeClient(record)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</template>
</template>
</a-table>
</div>
<!-- 操作按钮 -->
<div style="margin-top: 32px; display: flex; gap: 16px;">
<a-button type="primary" @click="handleApply">{{ t('page.access.apply') }}</a-button>
<a-button @click="handleCancel">{{ t('page.access.cancel') }}</a-button>
</div>
</div>
</a-card>
<!-- 新增:添加预认证白名单弹窗 -->
<a-modal
v-model:visible="addPreAuthVisible"
:title="t('page.access.addauth')"
@ok="handleAddPreAuthOk"
@cancel="() => addPreAuthVisible = false"
destroyOnClose
>
<a-form layout="vertical">
<a-form-item label="TYPE">
<a-select v-model:value="addPreAuthType" style="width: 100%;">
<a-select-option :value="1">IP</a-select-option>
<a-select-option :value="2">URL</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="addPreAuthType === 1" label="IP">
<div style="display: flex; align-items: center; gap: 4px;">
<a-input
v-model:value="addPreAuthIpParts[0]"
style="width: 56px;"
:maxlength="3"
@input="validateIpPart(0)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addPreAuthIpParts[1]"
style="width: 56px;"
:maxlength="3"
@input="validateIpPart(1)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addPreAuthIpParts[2]"
style="width: 56px;"
:maxlength="3"
@input="validateIpPart(2)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addPreAuthIpParts[3]"
style="width: 56px;"
:maxlength="3"
@input="validateIpPart(3)"
placeholder="0-255"
/>
<span>/</span>
<a-input
v-model:value="addPreAuthIpParts[4]"
style="width: 56px;"
:maxlength="2"
@input="validateMaskPart"
placeholder="0-32"
/>
</div>
<div v-if="ipValidationError" style="color: #ff4d4f; font-size: 12px; margin-top: 4px;">
{{ ipValidationError }}
</div>
</a-form-item>
<a-form-item v-if="addPreAuthType === 2" label="URL">
<a-input-group compact>
<span style="display: inline-block; width: 90px; text-align: center; background: #f5f5f5; border: 1px solid #d9d9d9; border-right: none; border-radius: 6px 0 0 6px; line-height: 32px;">http(s)://</span>
<a-input v-model:value="addPreAuthValue" style="width: calc(100% - 90px); border-radius: 0 6px 6px 0;" placeholder="请输入URL" />
</a-input-group>
</a-form-item>
</a-form>
</a-modal>
<!-- 新增:添加免认证白名单弹窗 -->
<a-modal
v-model:visible="addFreeClientVisible"
:title="t('page.access.addclient')"
@ok="handleAddFreeClientOk"
@cancel="() => addFreeClientVisible = false"
destroyOnClose
>
<a-form layout="vertical">
<a-form-item label="TYPE">
<a-select v-model:value="addFreeClientType" style="width: 100%;">
<a-select-option :value="3">IP</a-select-option>
<a-select-option :value="4">MAC</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="addFreeClientType === 3" label="IP">
<div style="display: flex; align-items: center; gap: 4px;">
<a-input
v-model:value="addFreeClientIpParts[0]"
style="width: 56px;"
:maxlength="3"
@input="validateFreeClientIpPart(0)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addFreeClientIpParts[1]"
style="width: 56px;"
:maxlength="3"
@input="validateFreeClientIpPart(1)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addFreeClientIpParts[2]"
style="width: 56px;"
:maxlength="3"
@input="validateFreeClientIpPart(2)"
placeholder="0-255"
/>
<span>.</span>
<a-input
v-model:value="addFreeClientIpParts[3]"
style="width: 56px;"
:maxlength="3"
@input="validateFreeClientIpPart(3)"
placeholder="0-255"
/>
</div>
<div v-if="freeClientIpValidationError" style="color: #ff4d4f; font-size: 12px; margin-top: 4px;">
{{ freeClientIpValidationError }}
</div>
</a-form-item>
<a-form-item v-if="addFreeClientType === 4" label="MAC">
<div style="display: flex; align-items: center; gap: 4px;">
<a-input v-model:value="addFreeClientMacParts[0]" style="width: 40px;" :maxlength="2" />
<span>-</span>
<a-input v-model:value="addFreeClientMacParts[1]" style="width: 40px;" :maxlength="2" />
<span>-</span>
<a-input v-model:value="addFreeClientMacParts[2]" style="width: 40px;" :maxlength="2" />
<span>-</span>
<a-input v-model:value="addFreeClientMacParts[3]" style="width: 40px;" :maxlength="2" />
<span>-</span>
<a-input v-model:value="addFreeClientMacParts[4]" style="width: 40px;" :maxlength="2" />
<span>-</span>
<a-input v-model:value="addFreeClientMacParts[5]" style="width: 40px;" :maxlength="2" />
</div>
</a-form-item>
</a-form>
</a-modal>
</SimpleScrollbar>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, h } from 'vue'
import { fetchSiteList, fetchAccessControl, updateAccessControl } from '@/service/api/auth'
import { DeleteOutlined } from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
import { SimpleScrollbar } from '~/packages/materials/src';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const preAuthEnabled = ref(true)
const freeClientEnabled = ref(true)
const preAuthList = ref([]) // 预认证白名单列表
const freeClientList = ref([]) // 免认证白名单列表
const siteList = ref<{ siteId: string, name: string }[]>([])
const selectedSiteId = ref('')
const siteLoading = ref(false)
// 新增:添加预认证白名单弹窗相关变量
const addPreAuthVisible = ref(false)
const addPreAuthType = ref(1) // 1: IP, 2: URL
const addPreAuthValue = ref('')
const addPreAuthIpParts = ref(['', '', '', '', '']) // [ip1, ip2, ip3, ip4, mask]
const ipValidationError = ref('')
// 新增:添加免认证白名单弹窗相关变量
const addFreeClientVisible = ref(false)
const addFreeClientType = ref(3) // 3: IP, 4: MAC
const addFreeClientIpParts = ref(['', '', '', ''])
const addFreeClientMacParts = ref(['', '', '', '', '', ''])
const freeClientIpValidationError = ref('')
// 定义表格列配置
const preAuthColumns = [
{
title: t('page.access.type'),
dataIndex: 'type',
key: 'type',
customRender: ({ record }: { record: any }) => {
if (record.type === 1) return 'IP'
if (record.type === 2) return 'URL'
return record.type
}
},
{
title: t('page.access.info'),
dataIndex: 'url',
key: 'url',
customRender: ({ record }: { record: any }) => {
if (record.type === 1) {
const ipDisplay = record.subnetMask ? `${record.ip}/${record.subnetMask}` : record.ip
return ipDisplay
}
if (record.type === 2) return record.url
return '-'
}
},
{
title: t('page.access.action'),
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
customRender: ({ record }: { record: any }) => {
return h(
'a-button',
{
type: 'link',
size: 'small',
style: { color: '#ff4d4f' },
onClick: () => handleDeletePreAuth(record)
},
{
icon: () => h(DeleteOutlined)
}
)
}
}
]
const freeClientColumns = [
{
title: t('page.access.type'),
dataIndex: 'type',
key: 'type',
customRender: ({ record }: { record: any }) => {
if (record.type === 3) return 'IP'
if (record.type === 4) return 'MAC'
return record.type
}
},
{
title: t('page.access.info'),
dataIndex: 'clientIp',
key: 'clientIp',
customRender: ({ record }: { record: any }) => {
if (record.type === 3) return record.clientIp
if (record.type === 4) return record.clientMac
return '-'
}
},
{
title: t('page.access.action'),
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
customRender: ({ record }: { record: any }) => {
return h(
'a-button',
{
type: 'link',
size: 'small',
style: { color: '#ff4d4f' },
onClick: () => handleDeleteFreeClient(record)
},
{
icon: () => h(DeleteOutlined)
}
)
}
}
]
const getSiteList = async () => {
siteLoading.value = true
const { data, error } = await fetchSiteList({ pageNum: 1, pageSize: 100 })
console.log('siteList接口返回', data, error)
if (!error) {
siteList.value = data.rows || []
console.log('siteList.value', siteList.value)
if (siteList.value.length > 0) {
selectedSiteId.value = siteList.value[0].siteId
console.log('getSiteList设置selectedSiteId', selectedSiteId.value)
await getAccessControl()
}
}
siteLoading.value = false
}
const getAccessControl = async () => {
console.log('getAccessControl调用,selectedSiteId', selectedSiteId.value)
if (!selectedSiteId.value) return
const { data, error } = await fetchAccessControl(selectedSiteId.value)
console.log('accessControl接口返回', data, error)
if (!error && data) {
preAuthEnabled.value = !!data.preAuthAccessEnable
freeClientEnabled.value = !!data.freeAuthClientEnable
preAuthList.value = (data.preAuthAccessPolicies || []).map((item: any) => ({
key: String(item.idInt),
type: item.type,
ip: item.ip,
subnetMask: item.subnetMask,
url: item.url,
...item
}))
freeClientList.value = (data.freeAuthClientPolicies || []).map((item: any) => ({
key: String(item.idInt),
type: item.type,
clientIp: item.clientIp,
clientMac: item.clientMac,
...item
}))
console.log('preAuthList', preAuthList.value)
console.log('freeClientList', freeClientList.value)
} else {
console.log('getAccessControl未进入赋值分支', { error, data })
}
console.log('getAccessControl结束', preAuthList.value, freeClientList.value)
}
const handleSiteChange = async (value: string) => {
selectedSiteId.value = value
// 不需要手动调用 getAccessControl,watch 会自动触发
}
const onAddPreAuth = () => {
addPreAuthType.value = 1
addPreAuthValue.value = ''
addPreAuthIpParts.value = ['', '', '', '', '']
ipValidationError.value = ''
addPreAuthVisible.value = true
}
const handleAddPreAuthOk = () => {
if (addPreAuthType.value === 1) {
// 拼接IP
const ip = addPreAuthIpParts.value.slice(0, 4).join('.')
const mask = addPreAuthIpParts.value[4]
// IP地址格式校验
const ipRegex = /\b((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\b/
if (!ipRegex.test(ip)) {
message.error(t('page.access.iperror'))
return
}
// 子网掩码校验(如果输入了掩码)
if (mask) {
const maskNum = parseInt(mask)
if (isNaN(maskNum) || maskNum < 0 || maskNum > 32) {
message.error(t('page.access.subnetMask'))
return
}
}
preAuthList.value.push({
key: Date.now() + Math.random(), // 简单唯一key
type: 1,
ip: ip,
subnetMask: mask || undefined,
url: null
})
} else {
// 只保存输入框内容,不带http(s)://
preAuthList.value.push({
key: Date.now() + Math.random(),
type: 2,
ip: null,
subnetMask: undefined,
url: addPreAuthValue.value
})
}
addPreAuthVisible.value = false
}
const onAddFreeClient = () => {
addFreeClientType.value = 3
addFreeClientIpParts.value = ['', '', '', '']
addFreeClientMacParts.value = ['', '', '', '', '', '']
freeClientIpValidationError.value = ''
addFreeClientVisible.value = true
}
const handleAddFreeClientOk = () => {
if (addFreeClientType.value === 3) {
// 拼接IP
const ip = addFreeClientIpParts.value.join('.')
// IP地址格式校验
const ipRegex = /\b((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\.((?!\d\d\d)\d+|1\d\d|2[0-4]\d|25[0-5])\b/
if (!ipRegex.test(ip)) {
message.error(t('page.access.iperror'))
return
}
freeClientList.value.push({
key: Date.now() + Math.random(),
type: 3,
clientIp: ip,
clientMac: null
})
} else {
// 拼接MAC
const mac = addFreeClientMacParts.value.map(x => x.padStart(2, '0')).join('-')
freeClientList.value.push({
key: Date.now() + Math.random(),
type: 4,
clientIp: null,
clientMac: mac
})
}
addFreeClientVisible.value = false
}
const handleDeletePreAuth = (record: any) => {
Modal.confirm({
title: t('page.access.deletetitle'),
content: t('page.access.deletebody'),
okText: t('page.access.ok'),
cancelText: t('page.access.cancel'),
onOk: () => {
preAuthList.value = preAuthList.value.filter(item => item.key !== record.key)
}
})
}
const handleDeleteFreeClient = (record: any) => {
Modal.confirm({
title: t('page.access.deletetitle'),
content: t('page.access.deletebody'),
okText: t('page.access.ok'),
cancelText: t('page.access.cancel'),
onOk: () => {
freeClientList.value = freeClientList.value.filter(item => item.key !== record.key)
}
})
}
// 监听 selectedSiteId,变化时自动拉取白名单数据
watch(selectedSiteId, (val) => {
console.log('watch触发,selectedSiteId变为', val)
if (val) getAccessControl()
})
// 在 script setup 中添加提交函数
const handleApply = async () => {
if (!selectedSiteId.value) {
// message.warning('请先选择站点')
return
}
try {
// 组装提交数据
const submitData = {
preAuthAccessEnable: preAuthEnabled.value,
freeAuthClientEnable: freeClientEnabled.value,
preAuthAccessPolicies: preAuthList.value.map(item => ({
type: item.type,
ip: item.type === 1 ? item.ip : undefined,
subnetMask: item.type === 1 ? item.subnetMask : undefined,
url: item.type === 2 ? item.url : undefined
})),
freeAuthClientPolicies: freeClientList.value.map(item => ({
type: item.type,
clientIp: item.type === 3 ? item.clientIp : undefined,
clientMac: item.type === 4 ? item.clientMac : undefined
}))
}
// 调用更新接口
const { error } = await updateAccessControl(selectedSiteId.value, submitData)
if (!error) {
// message.error('提交失败')
message.success(t('page.access.applysuccess'))
return
}
// message.success('提交成功')
// 重新获取最新数据
await getAccessControl()
} catch (e) {
console.error('提交出错:', e)
// message.error('提交失败')
}
}
// 在 script setup 中添加 Cancel 处理函数
const handleCancel = async () => {
if (!selectedSiteId.value) {
// message.warning('请先选择站点')
return
}
try {
await getAccessControl()
// message.success('已刷新数据')
} catch (e) {
console.error('刷新数据出错:', e)
// message.error('刷新数据失败')
}
}
const validateIpPart = (index: number) => {
const value = addPreAuthIpParts.value[index]
if (value && !/^\d+$/.test(value)) {
ipValidationError.value = t('page.access.plenumber')
return
}
if (value) {
const num = parseInt(value)
if (num < 0 || num > 255) {
ipValidationError.value = `IP${index + 1}must between 0-255`
return
}
}
// 清除错误信息
ipValidationError.value = ''
}
const validateMaskPart = () => {
const value = addPreAuthIpParts.value[4]
if (value && !/^\d+$/.test(value)) {
ipValidationError.value = t('page.access.plenumber')
return
}
if (value) {
const num = parseInt(value)
if (num < 0 || num > 32) {
ipValidationError.value = t('page.access.subnetMask')
return
}
}
// 清除错误信息
ipValidationError.value = ''
}
const validateFreeClientIpPart = (index: number) => {
const value = addFreeClientIpParts.value[index]
if (value && !/^\d+$/.test(value)) {
freeClientIpValidationError.value = t('page.access.plenumber')
return
}
if (value) {
const num = parseInt(value)
if (num < 0 || num > 255) {
freeClientIpValidationError.value = `IP${index + 1}must in 0-255`
return
}
}
// 清除错误信息
freeClientIpValidationError.value = ''
}
onMounted(async () => {
console.log('onMounted执行')
await getSiteList()
console.log('onMounted后selectedSiteId', selectedSiteId.value)
if (selectedSiteId.value) {
await getAccessControl()
}
})
</script>
<style scoped>
.anticon-info-circle {
font-size: 18px;
}
</style>
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more