260 Commits
Author SHA1 Message Date
caiyuchao 30dcf6460a fix: paypal按钮刷新渲染 2025-06-23 18:55:44 +08:00
zhongzm f98a1cca4f feat:部分按钮的双端隐藏和显示配置 2025-06-20 17:25:11 +08:00
zhongzm adfd2426b4 feat:修复首页控制台报错 2025-06-20 15:53:08 +08:00
zhongzm f62bc444ce feat:提交补充 2025-06-19 16:04:47 +08:00
zhongzm b9ef0e4e3f feat:菜单顺序调整 2025-06-19 15:39:18 +08:00
zhongzm d2890a25b6 Merge remote-tracking branch 'origin/main' 2025-06-18 19:55:01 +08:00
zhongzm 39c2e05121 feat:账单记录界面移动端适配 2025-06-18 19:54:49 +08:00
caiyuchao 9c2d91ba96 feat: 统一修改系统名补充 2025-06-18 17:44:17 +08:00
caiyuchao ace15a79c6 feat: 统一修改系统名 2025-06-18 17:15:02 +08:00
zhongzm 6c3499b470 feat:账单系统 2025-06-17 18:40:51 +08:00
zhongzm 7c77b10c8c Merge remote-tracking branch 'origin/main' 2025-04-30 11:36:35 +08:00
zhongzm 191ecd4c83 feat:图标更改 2025-04-30 11:36:24 +08:00
caiyuchao 4d4727b17e feat: paypal配置 2025-04-29 18:07:23 +08:00
caiyuchao b72bd2121a feat: palpay支付成功刷新 2025-04-28 23:50:59 +08:00
caiyuchao ac4e201f98 feat: paypal中英文适配 2025-04-28 19:00:31 +08:00
zhongzm 3cc72b946d feat:多支付以及货币符号显示 2025-04-27 20:17:35 +08:00
caiyuchao 621932f514 feat: stripe支付 2025-04-25 11:35:41 +08:00
caiyuchao 3cbaebb5d3 feat: paypal支付 2025-04-23 16:01:56 +08:00
zhongzm 092864e661 fix:登录界面校验删除以及显示修改 2025-04-15 20:03:23 +08:00
zhongzm d4c901d1c6 fix:登录界面增加中英文切换功能 2025-03-20 11:46:09 +08:00
zhongzm 74d2cbe323 fix:提交 2025-03-19 18:08:41 +08:00
caiyuchao b18a0c1872 feat: 删除移动端调试工具 2025-03-19 17:06:48 +08:00
caiyuchao 70b86aa171 feat: 更换和统一logo 2025-03-19 15:00:10 +08:00
caiyuchao 03df615d78 feat: 国际化语言默认为英语 2025-03-19 11:04:18 +08:00
zhongzm 45fbc12de5 fix:登录界面样式优化 2025-03-13 16:22:37 +08:00
zhongzm 6696e5bde1 fix:登录界面样式优化 2025-03-13 16:21:59 +08:00
zhongzm 1f33e8295b fix:界面按钮间隔 2025-03-12 15:19:04 +08:00
zhongzm 3ff07c3183 fix:CDR界面中英修复 2025-03-12 15:18:47 +08:00
zhongzm f94f226bf5 fix:隐藏用户名修改项 2025-03-11 15:25:33 +08:00
zhongzm 335d6ade0c fix:余额为0时余额支付校验 2025-03-11 14:43:53 +08:00
zhongzm 46fc4d33ec fix:上网记录界面添加条件查询,排序,添加时长字段 2025-03-07 17:38:48 +08:00
zhongzm 9e1d12db30 fix:kyc拒绝原因显示 2025-03-05 15:10:40 +08:00
zhongzm 7a56fd8fa8 fix:余额支付额外确认 2025-03-05 15:03:01 +08:00
zhongzm c0c4e81cf6 fix:套餐状态判断套餐禁止办理 2025-02-27 18:34:01 +08:00
lai 7899640132 勾选项ID 2025-02-27 15:08:46 +08:00
lai 9b526fffbd 修改赋值机制 2025-02-27 14:54:27 +08:00
TsMask 2ab8b3bb9b style: 使用默认布局设置 2025-02-27 11:57:14 +08:00
lai d25c9b1ef4 Merge branch 'main' of http://192.168.2.166:3180/WFC/fe.wfc.user 2025-02-26 21:08:14 +08:00
lai 063ecfe3df 修改分页 2025-02-26 21:08:09 +08:00
zhongzm f3ea268389 fix:密码校验格式统一 2025-02-26 21:06:16 +08:00
zhongzm 7f533c4956 fix:仪表盘无套餐有余额 2025-02-26 18:33:43 +08:00
zhongzm e1dfa18b30 fix:性别改为必选项,0男1女 2025-02-26 16:54:34 +08:00
zhongzm 73cc576b5e fix:登录部分错误信息修复 2025-02-26 15:40:15 +08:00
zhongzm 0a51eb0fab fix:个人信息部分错误信息修复 2025-02-26 15:35:50 +08:00
zhongzm ee4338b2fb fix:账单部分错误信息修复 2025-02-26 15:29:21 +08:00
zhongzm 63bda4da8c fix:设备部分错误信息修复 2025-02-26 15:24:38 +08:00
zhongzm 824a447124 fix:仪表盘错误信息修复 2025-02-26 15:22:33 +08:00
TsMask a6c63088af fix: 布局horizontal-mix选择目录跳转首个子菜单 2025-02-26 14:26:07 +08:00
lai f81b8a0c41 放出暗黑按钮 2025-02-21 17:03:45 +08:00
caiyuchao 8569fcd040 fix: 图片路径 2025-02-21 15:56:44 +08:00
caiyuchao 68353ef23a fix: 支付图片路径 2025-02-21 15:46:57 +08:00
lai d6b972f363 暗色模式适配 2025-02-21 14:26:13 +08:00
lai 6ec114fd60 修改密码复杂度 2025-02-20 17:35:23 +08:00
lai 6bfba0a7c9 修改接口 2025-02-20 17:25:44 +08:00
lai 9172a03e11 修复忘记密码时提示错误问题 2025-02-20 16:08:46 +08:00
lai 1dd6d27d64 修复分页器 2025-02-20 14:48:27 +08:00
lai 8368a082ea 适配暗黑色 2025-02-20 14:40:21 +08:00
zhangsz 725c77da78 fix: en message 2025-02-20 09:41:35 +08:00
caiyuchao 24f4dc23cc fix: 个人信息修改出生日期 2025-02-19 16:33:18 +08:00
zhangsz f59ea203ec fix: en message 2025-02-17 16:33:06 +08:00
zhangsz f72e296d60 fix: update user package select page 2025-02-17 10:57:06 +08:00
zhongzm 984335b054 feat:个人信息部分背景暗色适配 2025-02-15 16:26:38 +08:00
zhongzm e897b14230 feat:设备部分背景暗色适配 2025-02-15 16:19:56 +08:00
zhongzm 924d30b28e feat:cdr背景暗色模式适配 2025-02-15 16:00:01 +08:00
zhongzm 97c8e41b95 feat:仪表盘无套餐时显示修改 2025-02-15 15:12:03 +08:00
zhongzm d18d8bf393 feat:仪表盘无套餐时显示修改 2025-02-15 14:58:40 +08:00
zhongzm f92107dbbb feat:增加修改密码后的提示 2025-02-15 14:35:00 +08:00
zhongzm 93579c7dde feat:增加余额支付功能 2025-02-13 20:35:43 +08:00
zhongzm 29f58edef6 feat:忘记密码功能完善 2025-02-13 16:51:07 +08:00
zhongzm e471f24764 feat:添加邮箱重置密码功能 2025-02-13 16:20:59 +08:00
zhongzm 1f92cc6b3f Merge remote-tracking branch 'origin/main' 2025-02-13 15:34:33 +08:00
zhongzm c836ede0b2 feat:仪表盘数据中英修复(暂时刷新界面处理) 2025-02-13 15:34:24 +08:00
caiyuchao d5f4f26ed0 fix: 修复支付问题 2025-02-12 20:35:14 +08:00
zhongzm de502c4806 feat:仪表盘居中显示 2025-02-12 16:27:29 +08:00
zhongzm a6d944f98f feat:kyc状态显示问题修复 2025-02-12 10:52:27 +08:00
zhongzm 59533750c6 Merge remote-tracking branch 'origin/main' 2025-02-08 15:27:18 +08:00
zhongzm beefbaedbe feat:流量格式化问题修复 2025-02-08 15:27:08 +08:00
caiyuchao 8e43f61a84 fix: 邮箱验证码 2025-02-07 19:31:39 +08:00
zhongzm 16f6c413e3 feat:充值显示和菜单图标修改 2025-02-07 16:57:22 +08:00
zhongzm a791720536 feat:证件类型中英文修复 2025-02-07 15:07:56 +08:00
zhongzm ceef70fdb2 feat:余额充值溢出和显示修复 2025-02-07 09:51:00 +08:00
zhongzm f9b486888d feat:修复其他信息的enable判断逻辑 2025-01-24 19:34:11 +08:00
zhongzm 09a31c46b6 feat:流量是否无限制判断修复 2025-01-24 19:28:16 +08:00
zhongzm 900382b0c7 feat:kyc认证界面中英适配 2025-01-24 19:19:15 +08:00
zhongzm 67e4f85a4e feat:仪表盘中英文修复 2025-01-23 20:56:06 +08:00
zhongzm 6199041d21 feat:仪表盘中英文修复 2025-01-23 20:54:30 +08:00
zhongzm ec087caaaf feat:亮色模式修复 2025-01-23 20:34:57 +08:00
zhongzm 83cc4555b7 feat:隐藏主题颜色变化按钮 2025-01-23 20:29:13 +08:00
zhongzm 35173135c5 feat:余额充值中英适配 2025-01-23 19:38:03 +08:00
zhongzm 34cdce8a87 feat:套餐办理界面中英适配 2025-01-23 17:25:13 +08:00
zhongzm 9dee7d17eb feat:仪表盘占比修改 2025-01-22 21:12:43 +08:00
zhongzm bf35afa9e2 feat:套餐记录界面增加套餐显示以及中英文显示 2025-01-22 20:27:22 +08:00
zhongzm 837a752021 feat:速率比1000改1024 2025-01-22 19:40:20 +08:00
zhongzm 0ef88eab88 feat:token修改 2025-01-22 18:40:00 +08:00
zhongzm 3149c19384 feat:取消登录默认存在的账号密码 2025-01-22 18:39:05 +08:00
zhongzm b4c01b8544 feat:仪表盘界面修改以及中英文适配 2025-01-22 18:38:24 +08:00
caiyuchao 737999885f fix: 修复退出登录问题 2025-01-22 17:12:27 +08:00
zhongzm 09922f048e feat:支付宝、微信支付对接 2025-01-22 12:03:18 +08:00
zhongzm 70f281974b fix:支付方式对接 2025-01-22 10:27:05 +08:00
zhongzm 96d19e323f fix:支付方式对接 2025-01-21 21:53:55 +08:00
caiyuchao e0ea1e1764 fix: 支付宝支付放行 2025-01-21 17:15:37 +08:00
caiyuchao 9aa3ea373c feat: 支付宝手机端支付测试 2025-01-21 17:05:45 +08:00
zhongzm b04c243acd fix:菜单文字显示修改 2025-01-21 16:40:16 +08:00
zhongzm 1d00eb3b5c fix:套餐有效期检验和显示 2025-01-21 16:39:13 +08:00
caiyuchao 107ff2dfbb feat: 微信支付 2025-01-20 17:04:18 +08:00
zhongzm 83d820cbce fix:kyc状态认证修改 2025-01-20 16:48:10 +08:00
zhongzm 0d10922b84 fix:kyc证件类型修改 2025-01-20 16:47:33 +08:00
caiyuchao 0902f6d4b2 fix: 充值记录 2025-01-18 17:28:52 +08:00
zhongzm 6fb3954a4d fix:菜单结构修改 2025-01-17 16:34:56 +08:00
zhongzm db34e59755 fix:隐藏历史账单(未接通) 2025-01-17 16:26:41 +08:00
zhongzm 333cc78ac5 fix:修改菜单英文 2025-01-17 16:23:19 +08:00
zhongzm 63439ad22c fix:修改菜单布局 2025-01-17 16:20:10 +08:00
zhongzm ebd243040f fix:隐藏忘记密码 2025-01-17 16:14:24 +08:00
caiyuchao 5bfb858f06 fix: 登录接口补充wifi参数修正 2025-01-17 15:07:19 +08:00
caiyuchao d242ac1b80 fix: 登录接口补充wifi参数 2025-01-17 14:45:41 +08:00
caiyuchao 5bd35af13a fix: 修复请求响应不报错问题 2025-01-17 11:44:05 +08:00
zhongzm 4162ec4c54 fix:重置密码界面接通 2025-01-16 19:48:12 +08:00
caiyuchao adc5ca37b1 fix: 修复门户传参 2025-01-16 17:25:20 +08:00
zhongzm 517e816f2b fix:注册界面传递参数格式修改 2025-01-16 16:26:45 +08:00
zhongzm dc520a14c4 fix:重置密码界面修改重置条件 2025-01-15 18:15:17 +08:00
zhongzm fc177c6b88 fix:注册界面用户名和电话号码格式校验修改 2025-01-15 14:25:51 +08:00
zhongzm 3d072513f1 fix:注册界面格式校验修复 2025-01-14 16:36:04 +08:00
zhongzm f546fc466a fix:充值界面底部显示修改 2025-01-14 16:09:48 +08:00
caiyuchao d663ffee2c feat: 支付宝测试对接 2025-01-14 15:50:47 +08:00
zhongzm 0b48107e2e fix:移动端底部按钮修改 2025-01-14 12:05:14 +08:00
zhongzm e1c33e54ab fix:充值服务界面合并 2025-01-14 12:04:49 +08:00
zhongzm 150677138d fix:个人信息界面修改 2025-01-14 12:04:06 +08:00
zhongzm a146567078 Merge remote-tracking branch 'origin/main' 2025-01-14 12:01:29 +08:00
zhongzm 192e58fc0b feat:KYC认证 2025-01-14 12:01:18 +08:00
zhangsz c45d67f033 fix: disable redirect to 8.8.8.8 2025-01-13 16:45:14 +08:00
zhongzm 434767a902 fix:修改错误提示 2025-01-10 19:07:31 +08:00
zhongzm b12d8fb218 fix:修改错误提示 2025-01-10 18:38:08 +08:00
zhongzm 5dcaf7d475 fix:删除上网检测 2025-01-10 17:14:55 +08:00
zhongzm 4c8b84d058 fix:充值服务菜单显示修复 2025-01-10 17:09:54 +08:00
zhongzm 98291bfec7 fix:路由信息修改 2025-01-10 16:54:56 +08:00
zhongzm 7133fe84e4 fix:billservice界面显示信息修改 2025-01-10 16:51:18 +08:00
zhongzm c6952919f9 fix:usercard界面移动端报错修复 2025-01-10 16:48:38 +08:00
zhongzm c125de3d9f fix:注册界面信息格式修改 2025-01-10 16:48:18 +08:00
zhongzm d8f28a5036 fix:充值界面提示错误修复 2025-01-10 16:21:23 +08:00
zhongzm 58723fd191 fix:套餐界面提示错误修复 2025-01-10 16:17:23 +08:00
zhongzm 98052ca417 fix:账单服务界面 2025-01-10 14:50:39 +08:00
zhongzm b2fc689d34 fix:套餐记录界面(未接通) 2025-01-10 14:50:10 +08:00
zhongzm 3358c19f60 fix:菜单路由修改 2025-01-10 14:49:45 +08:00
zhongzm c0a51e2044 fix:pc默认菜单模式修改 2025-01-10 14:49:14 +08:00
zhongzm 9390aace95 fix:充值记录界面 2025-01-10 14:48:46 +08:00
zhongzm 10153e2c6b feat:历史账单界面接通 2025-01-09 16:19:19 +08:00
zhongzm 671498b216 feat:套餐与充值界面修改 2025-01-03 18:03:41 +08:00
zhangsz 9ba3922f6d feat: add terser dependency to user portal 2025-01-02 10:11:10 +08:00
zhongzm 4fbb71c681 fix:仪表盘金额精确度修改 2024-12-31 17:02:20 +08:00
zhongzm a70546756a fix:旧浏览器存储方法兼容修改 2024-12-31 17:01:36 +08:00
zhongzm 3c8c9d2f12 fix:仪表盘显示修复 2024-12-30 19:19:18 +08:00
caiyuchao 8d8609d80c fix: 修改请求超时时间为30秒 2024-12-27 17:37:08 +08:00
caiyuchao cbc3bd345e fix: 增加请求超时时间为1分钟 2024-12-27 17:30:29 +08:00
zhongzm 7c36e9f9ef Revert "fix:模拟上网判断"
This reverts commit 09152873
2024-12-27 15:47:25 +08:00
zhongzm ef6277d8b8 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/views/home/modules/header-banner.vue
2024-12-27 15:46:10 +08:00
zhongzm 579c5e8eaf fix:取消前端模拟上网判断 2024-12-27 15:45:26 +08:00
zhongzm 0915287381 fix:模拟上网判断 2024-12-27 10:42:35 +08:00
zhongzm ac9af85eea fix:修复密码登录返回处理 2024-12-26 21:03:51 +08:00
zhongzm 9903aafe38 fix:参考文档进行的CSS逻辑属性兼容性处理 2024-12-26 19:55:41 +08:00
zhongzm cafca0ec11 fix:样式下调:where选择器 2024-12-26 18:35:30 +08:00
zhongzm 5ea674bc00 fix:浏览器版本兼容修复 2024-12-25 20:41:06 +08:00
zhongzm 7cc1af269b fix:套餐流量计算修改 2024-12-25 19:55:12 +08:00
zhongzm 1c81fa2ec0 fix:home界面组件更新联动 2024-12-25 18:51:47 +08:00
zhongzm 8edc1e19c7 fix:仪表盘数据对接修改 2024-12-25 18:51:09 +08:00
zhongzm 248ee105ab fix:套餐显示修改 2024-12-25 15:14:06 +08:00
zhongzm ad83b9c94c fix:参数无限制判断 2024-12-25 10:48:41 +08:00
zhongzm 7faf830a77 fix:充值界面接口对接 2024-12-24 19:12:07 +08:00
zhongzm b214cc0552 fix:套餐界面修改参数 2024-12-24 19:11:44 +08:00
zhongzm 70979b17e1 fix:移动和pc自适应菜单 2024-12-24 18:46:45 +08:00
zhongzm f3ca00d88a fix:菜单路由修改 2024-12-24 18:46:04 +08:00
zhongzm dd1ac7b66a fix:充值界面修改参数 2024-12-24 18:42:05 +08:00
zhongzm ca36402f45 fix:套餐界面路径修改调用组件 2024-12-24 18:41:27 +08:00
zhongzm 4c60b7a6c0 fix:历史账单界面按钮添加 2024-12-24 18:40:46 +08:00
zhongzm a9777e1f03 fix:上网详单界面按钮添加 2024-12-24 18:40:26 +08:00
zhongzm f9fba724fb fix:充值记录界面按钮添加 2024-12-24 18:40:03 +08:00
zhongzm a99456f70a fix:历史设备界面路径修改以及组件封装 2024-12-24 18:39:26 +08:00
zhongzm b40b81ffb8 fix:cdr记录界面路径修改以及组件封装 2024-12-24 18:38:20 +08:00
zhongzm 96422a4723 fix:当前设备界面路径修改以及组件封装 2024-12-24 18:38:01 +08:00
zhongzm 331bc14686 Merge remote-tracking branch 'origin/main' 2024-12-24 18:37:01 +08:00
zhongzm 87fe7bd5df fix:账单服务界面 2024-12-24 18:36:22 +08:00
zhangsz 710cf00b46 build update 2024-12-20 09:36:54 +08:00
zhongzm f3667813ae fix:首页仪表盘部分接口对接 2024-12-18 17:28:48 +08:00
zhongzm 086ed07666 fix:当前设备界面接口对接 2024-12-18 17:28:30 +08:00
zhongzm f9b1274328 fix:cdr记录接口对接 2024-12-18 17:28:12 +08:00
zhongzm e2226aa944 Merge remote-tracking branch 'origin/main' 2024-12-18 17:24:38 +08:00
zhongzm a6342d7036 fix:历史记录界面后端对接 2024-12-18 17:24:26 +08:00
TsMask edfec45390 feat: 测试上网功能 2024-12-13 21:07:55 +08:00
zhongzm 7cf6249fd8 fix:修改个人信息界面以及接口 2024-12-13 19:14:29 +08:00
zhongzm bf911bac96 fix:注册界面修改 2024-12-13 19:13:34 +08:00
zhongzm 25f4a2c290 fix:样式修复以及提示修复 2024-12-11 15:32:04 +08:00
zhongzm 9105c533be feat:设备管理界面 2024-12-11 15:00:36 +08:00
zhongzm c632d4f088 feat:修改密码界面 2024-12-11 15:00:18 +08:00
zhongzm 1d20f8c726 feat:修改信息界面 2024-12-11 15:00:03 +08:00
zhongzm dd46f6b19f feat:个人信息界面改动 2024-12-11 14:59:32 +08:00
zhongzm 598a2946fd feat:当前接入设备界面 2024-12-11 09:41:41 +08:00
zhongzm 7f2f51b541 feat:CDR记录界面及路径 2024-12-11 09:39:39 +08:00
zhongzm 9af2d1d676 feat:充值记录界面 2024-12-11 09:38:34 +08:00
zhongzm 16eb770cf1 feat:上网详单界面 2024-12-11 09:37:58 +08:00
zhongzm b8111b27c7 feat:上网详单界面 2024-12-11 09:36:39 +08:00
zhongzm 81053482b0 feat:当前接入设备界面及中英适配 2024-12-11 09:35:33 +08:00
TsMask 54abd0ce0a style: 主题切换显示 2024-12-06 21:31:59 +08:00
TsMask e637781987 test: 测试连通上网 2024-12-06 21:30:39 +08:00
TsMask 850ccff852 feat: 移动端调试工具 2024-12-06 21:29:44 +08:00
TsMask 2c9d6e4e07 feat: 门户拦截定向跳转 2024-12-06 21:28:43 +08:00
zhongzm 222ecc7558 fix:首页home 2024-12-06 09:47:16 +08:00
zhongzm 8d6714061a fix:历史账单 2024-12-06 09:46:51 +08:00
zhongzm 4448c663c1 fix:首页充值 2024-12-06 09:46:24 +08:00
zhongzm b5a5aa3afd fix:首页仪表盘中英显示和充值联动 2024-12-06 09:45:50 +08:00
zhongzm 2974f4ff63 Merge remote-tracking branch 'origin/main' 2024-12-06 09:35:05 +08:00
zhongzm 9c4a966ee4 fix:套餐界面 2024-12-06 09:34:51 +08:00
TsMask a8ec9ff797 fix: 设置默认语言为en-US 2024-12-04 17:28:24 +08:00
TsMask d438b525b2 fix: 菜单固定顶部,路由隐藏排序 2024-12-04 17:19:22 +08:00
simonzhangsz 79354a845c hide fe menu 2024-12-04 11:07:21 +08:00
simonzhangsz 28fb4d3346 update package name 2024-12-04 10:07:07 +08:00
zhongzm 0782ad0ebe fix:首页头部仪表盘 2024-12-03 18:53:10 +08:00
zhongzm 1668d4c4cc fix:登录界面布局修改 2024-12-03 18:42:08 +08:00
zhongzm 3145b8d676 fix:密码登录界面报错修复 2024-12-03 18:41:41 +08:00
zhongzm 4daceee6f0 feat:个人信息界面 2024-12-03 18:40:59 +08:00
zhongzm 65a891a73a fix:个人信息目录 2024-12-03 18:40:24 +08:00
zhongzm 36108a0ffa fix:修复重置密码时报错信息 2024-12-03 18:30:52 +08:00
TsMask 3be0ee5b5a fix: 指定超级用户标记为super 2024-12-03 16:45:14 +08:00
TsMask 44ba559d78 fix: 关闭个人中心页面 2024-12-03 14:25:28 +08:00
TsMask ff11c43c32 fix: 超级用户角色标识superuser 2024-12-03 12:17:51 +08:00
TsMask 77338dca48 fix: 默认演示123456为普通用户 2024-12-03 11:46:22 +08:00
TsMask cbd6ffb334 feat: 普通用户静态路由定义 2024-12-03 11:45:58 +08:00
TsMask 3f4ffdf09f feat: 补充路由菜单,账单,终端设备目录 2024-12-02 20:04:27 +08:00
TsMask 18dacb0c75 fix: 路由加载合并排序(还存在切换账号菜单缓存问题) 2024-12-02 20:03:31 +08:00
TsMask f6bfceab29 style: 替换Soybean为WANFi 2024-12-02 20:01:12 +08:00
TsMask a5fd5e0fd2 feat: 角色认证判断 2024-12-02 20:00:07 +08:00
TsMask c9f0fbaca5 fix:角色勾选根节点获取失败问题 2024-12-02 16:20:47 +08:00
TsMask 3f01adb2ad fix: 静态路由角色拦截限制 2024-12-02 16:20:03 +08:00
TsMask 5e54aaf24d fix: 注册功能接口变更 2024-11-30 17:31:47 +08:00
zhongzm 8f198f789d fix:添加查询是否注册,页面问题修复 2024-11-29 18:07:43 +08:00
TsMask 4cbdc12f90 perf: 路由重构 2024-11-29 17:18:45 +08:00
TsMask a57f669603 perf: 路由重构 2024-11-29 17:09:02 +08:00
TsMask d7d471e552 perf: 路由重构 2024-11-29 16:51:14 +08:00
TsMask 5d2b71bb37 feat: 删除无用依赖 2024-11-29 16:50:18 +08:00
zhongzm 2bf6f744cd fix:密码重置界面修改 2024-11-29 09:04:48 +08:00
zhongzm 56a8cb35a9 fix:密码登录界面修改 2024-11-29 09:04:30 +08:00
zhongzm 650a180778 fix:注册界面及相关功能 2024-11-29 09:04:06 +08:00
zhongzm a3a6aeb40e fix:登录框架修改 2024-11-29 09:02:09 +08:00
zhongzm 52699c74d2 fix:验证码校验修改 2024-11-29 09:01:12 +08:00
zhongzm 5700b1fc06 fix:校验修改 2024-11-29 09:00:25 +08:00
TsMask af1ed94499 feat: 修改生产API标识 2024-11-28 11:02:56 +08:00
TsMask 19d48eac0e feat: 修改生产API标识,修改默认布局 2024-11-28 10:36:01 +08:00
TsMask 1240d9aedf fix: 用户平台指定模块接口 2024-11-27 16:46:16 +08:00
zhongzm feefcfa7e6 style:协议文档 2024-11-27 11:40:17 +08:00
zhongzm 5df64af41c fix:用户注册界面 2024-11-27 11:39:45 +08:00
zhongzm 8bb7117b0e fix:登录界面框架修改 2024-11-27 11:38:45 +08:00
zhongzm 627d56b274 Merge remote-tracking branch 'origin/wfc-modules-user' into wfc-modules-user 2024-11-26 18:15:43 +08:00
caiyuchao 7d5ed9cdf5 fix: fix login page
(cherry picked from commit 074f9282dd410e25c8e3785b931fa10106e9ee8b)
2024-11-26 10:09:00 +08:00
caiyuchao 0331c5fa9d feat:add i18n header
(cherry picked from commit a1dbcd1f003c6e4ce8f431277affb952dab84195)
2024-11-26 10:08:48 +08:00
zhongzm b85288f0fc test 2024-11-25 10:47:53 +08:00
lichang 5ff4997e0a 更新 .env.prod 2024-11-22 17:51:43 +08:00
caiyuchao d5035fc13d fix: 修改prod配置 2024-11-15 19:05:05 +08:00
caiyuchao 0331bb48d7 fix: 修改prod配置 2024-11-15 18:22:27 +08:00
caiyuchao 4ffac789e1 初始化项目 2024-11-14 11:06:38 +08:00
simonzhangsz 988b9e6799 dd 2024-10-22 10:20:32 +08:00
simonzhangsz dc15bd29cd re-init 2024-10-22 10:19:24 +08:00
zhangsz 0db2513b55 添加 README.md 2024-10-21 10:31:24 +08:00
330 changed files with 41504 additions and 2 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
+38
View File
@@ -0,0 +1,38 @@
VITE_BASE_URL=/
VITE_APP_TITLE="Platform"
# the prefix of the icon name
VITE_ICON_PREFIX=icon
# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
# format {VITE_ICON_PREFIX}-{local icon name}
VITE_ICON_LOCAL_PREFIX=icon-local
# auth route mode: static | dynamic
VITE_AUTH_ROUTE_MODE=static
# default menu icon
VITE_MENU_ICON=mdi:menu
# whether to enable http proxy when is dev mode
VITE_HTTP_PROXY=Y
# vue-router mode: hash | history | memory
VITE_ROUTER_HISTORY_MODE=hash
# success code of backend service, when the code is received, the request is successful
VITE_SERVICE_SUCCESS_CODE=200
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
VITE_SERVICE_LOGOUT_CODES=401
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
VITE_SERVICE_MODAL_LOGOUT_CODES=401
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
VITE_PAYPAL_CLIENT_ID=AfPgwFAmo9K7KCqiiGpNRCyQMSxI6V33eH-nEMnVndJNVEYOEOEn5wSPkHUybfzcjDLnBejt-RKnIfqX
+13
View File
@@ -0,0 +1,13 @@
# vue-router mode: hash | history | memory
VITE_ROUTER_HISTORY_MODE=hash
# frontend service base url, prod environment
VITE_BASE_URL=/u
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=/u-api
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9529"
}`
+7
View File
@@ -0,0 +1,7 @@
# backend service base url, test environment
VITE_SERVICE_BASE_URL=http://192.168.9.50/u-api
# other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"
}`
+13
View File
@@ -0,0 +1,13 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.mjs" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.scss" eol=lf
"*.md" eol=lf
"*.yaml" eol=lf
"*.yml" eol=lf
+36
View File
@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
yarn.lock
.VSCodeCounter
**/.vitepress/cache
+3
View File
@@ -0,0 +1,3 @@
registry=https://registry.npmmirror.com/
shamefully-hoist=true
ignore-workspace-root-check=true
+22
View File
@@ -0,0 +1,22 @@
{
"recommendations": [
"afzalsayed96.icones",
"antfu.iconify",
"antfu.unocss",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"formulahendry.auto-close-tag",
"formulahendry.auto-complete-tag",
"formulahendry.auto-rename-tag",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",
"naumovs.color-highlight",
"pkief.material-icon-theme",
"sdras.vue-vscode-snippets",
"vue.volar",
"whtouche.vscode-js-console-utils",
"zhuangtongfa.material-theme"
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Vue Debugger",
"url": "http://localhost:9527",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "TS Debugger",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"program": "${file}"
}
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.experimental.useFlatConfig": true,
"editor.formatOnSave": false,
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.editor.preferEditor": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"],
"prettier.enable": false,
"unocss.root": ["./"],
"cSpell.words": [
"fullscreen",
"hexcode",
"Localforage",
"nacos",
"ryadmin",
"Simplebar",
"treeselect",
"userinfo"
]
}
+33 -2
View File
@@ -1,3 +1,34 @@
# fe.wfc.user
# WANFI 用户平台
前端-用户平台(wfc-modules-user)
## 使用
**环境准备**
确保你的环境满足以下要求:
- **git**: 你需要git来克隆和管理项目版本。
- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
**安装依赖**
npm install -g pnpm
```bash
npm install -g pnpm
pnpm i
```
> 由于本项目采用了 pnpm monorepo 的管理方式,因此请不要使用 npm 或 yarn 来安装依赖。
**启动项目**
```bash
pnpm dev
```
**构建项目**
```bash
pnpm build
```
+1
View File
@@ -0,0 +1 @@
export * from './proxy';
+36
View File
@@ -0,0 +1,36 @@
import type { ProxyOptions } from 'vite';
import { createServiceConfig } from '../../src/utils/service';
/**
* Set http proxy
*
* @param env - The current env
* @param isDev - Is development environment
*/
export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
if (!isEnableHttpProxy) return undefined;
const { baseURL, proxyPattern, other } = createServiceConfig(env);
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern });
other.forEach(item => {
Object.assign(proxy, createProxyItem(item));
});
return proxy;
}
function createProxyItem(item: App.Service.ServiceConfigItem) {
const proxy: Record<string, ProxyOptions> = {};
proxy[item.proxyPattern] = {
target: item.baseURL,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
};
return proxy;
}
+22
View File
@@ -0,0 +1,22 @@
import type { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import progress from 'vite-plugin-progress';
import { setupUnocss } from './unocss';
import { setupUnplugin } from './unplugin';
export function setupVitePlugins(viteEnv: Env.ImportMeta) {
const plugins: PluginOption = [
vue({
script: {
defineModel: true
}
}),
vueJsx(),
setupUnocss(viteEnv),
...setupUnplugin(viteEnv),
progress()
];
return plugins;
}
+32
View File
@@ -0,0 +1,32 @@
import process from 'node:process';
import path from 'node:path';
import unocss from '@unocss/vite';
import presetIcons from '@unocss/preset-icons';
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
export function setupUnocss(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
return unocss({
presets: [
presetIcons({
prefix: `${VITE_ICON_PREFIX}-`,
scale: 1,
extraProperties: {
display: 'inline-block'
},
collections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
warn: true
})
]
});
}
+69
View File
@@ -0,0 +1,69 @@
import process from 'node:process';
import path from 'node:path';
import type { PluginOption } from 'vite';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import AutoImport from 'unplugin-auto-import/vite';
export function setupUnplugin(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
const plugins: PluginOption[] = [
Icons({
compiler: 'vue3',
customCollections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dts: 'src/typings/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [
AntDesignVueResolver({
importStyle: false
}),
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
]
}),
createSvgIconsPlugin({
iconDirs: [localIconPath],
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
inject: 'body-last',
customDomId: '__SVG_ICON_LOCAL__'
}),
AutoImport({
imports: [
'vue',
'vue-router',
'vitest',
'@vueuse/core',
'pinia',
{ 'lodash-es': ['cloneDeep', 'assign', 'omit', 'pick'] },
{
'ant-design-vue': [
['message', '$message'],
['notification', '$notification'],
['Modal', '$modal']
]
}
],
dts: 'src/typings/auto-imports.d.ts',
dirs: ['src/utils/**', 'src/store/modules/**', 'src/hooks/common/**', 'src/service/api/**']
})
];
return plugins;
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Create service config by current env
*
* @param env The current env
*/
export function createServiceConfig(env: Env.ImportMeta) {
const mockURL = 'https://mock.apifox.com/m1/3109515-0-default';
const serviceConfigMap = {
dev: {
baseURL: 'http://localhost:8080',
otherBaseURL: {
demo: 'http://localhost:9528'
}
},
test: {
baseURL: mockURL,
otherBaseURL: {
demo: 'http://localhost:9529'
}
},
prod: {
baseURL: mockURL,
otherBaseURL: {
demo: 'http://localhost:9530'
}
}
};
const { VITE_SERVICE_ENV = 'dev' } = env;
return serviceConfigMap[VITE_SERVICE_ENV as keyof typeof serviceConfigMap];
}
/**
* Get proxy pattern of service url
*
* @param key If not set, will use the default key
*/
export function createProxyPattern(key?: App.Service.OtherBaseURLKey) {
if (!key) {
return '/proxy';
}
return `/proxy-${key}`;
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig } from '@soybeanjs/eslint-config';
export default defineConfig(
{ vue: true, unocss: true },
{
rules: {
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index', 'App', '[id]']
}
],
'vue/component-name-in-template-casing': [
'warn',
'PascalCase',
{
registeredComponentsOnly: false,
ignores: ['/^icon-/']
}
],
'order-attributify': 'off',
'no-warning-comments': 'off'
}
}
);
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<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>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
{
"name": "wanfi-user",
"type": "module",
"version": "1.0.0",
"scripts": {
"build": "vite build --mode prod",
"build:test": "vite build --mode test",
"dev": "vite --mode test",
"dev:prod": "vite --mode prod",
"lint": "eslint . --fix",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@paypal/paypal-js": "^8.2.0",
"@sa/axios": "workspace:*",
"@sa/color-palette": "workspace:*",
"@sa/hooks": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "10.10.0",
"ant-design-vue": "4.2.2",
"clipboard": "2.0.11",
"dayjs": "1.11.11",
"echarts": "5.5.0",
"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"
},
"devDependencies": {
"@elegant-router/vue": "0.3.7",
"@iconify/json": "2.2.217",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.3.6",
"@types/lodash-es": "4.17.12",
"@types/node": "20.14.2",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.60.4",
"@unocss/preset-attributify": "^0.60.4",
"@unocss/preset-icons": "0.60.4",
"@unocss/preset-tagify": "^0.60.4",
"@unocss/preset-uno": "0.60.4",
"@unocss/transformer-directives": "0.60.4",
"@unocss/transformer-variant-group": "0.60.4",
"@unocss/vite": "0.60.4",
"@vitejs/plugin-vue": "5.0.5",
"@vitejs/plugin-vue-jsx": "4.0.0",
"eslint": "9.4.0",
"eslint-plugin-vue": "9.26.0",
"lint-staged": "15.2.5",
"sass": "1.77.4",
"simple-git-hooks": "2.11.1",
"terser": "^5.37.0",
"tsx": "4.14.1",
"typescript": "5.4.5",
"unplugin-auto-import": "^0.17.6",
"unplugin-icons": "0.19.0",
"unplugin-vue-components": "0.27.0",
"vite": "5.2.13",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vitest": "^1.6.0",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.21"
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@sa/axios",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.6.8",
"axios-retry": "4.1.0",
"qs": "6.12.1"
},
"devDependencies": {
"@types/qs": "6.9.15"
}
}
+5
View File
@@ -0,0 +1,5 @@
/** request id key */
export const REQUEST_ID_KEY = 'X-Request-Id';
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
+191
View File
@@ -0,0 +1,191 @@
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
FlatRequestInstance,
MappedType,
RequestInstance,
RequestOption,
ResponseType
} from './type';
function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
axiosRetry(instance, retryOptions);
instance.interceptors.request.use(conf => {
const config: InternalAxiosRequestConfig = { ...conf };
// set request id
const requestId = nanoid();
config.headers.set(REQUEST_ID_KEY, requestId);
// config cancel token
const cancelTokenSource = axios.CancelToken.source();
config.cancelToken = cancelTokenSource.token;
cancelTokenSourceMap.set(requestId, cancelTokenSource);
// handle config by hook
const handledConfig = opts.onRequest?.(config) || config;
return handledConfig;
});
instance.interceptors.response.use(
async response => {
// 对仪表盘接口做特殊处理
if (response.config.url?.includes('/u/account/dashboard')) {
// 如果响应中包含数据但没有明确的错误信息,也视为成功
if (response.data && !response.data.error) {
return Promise.resolve(response);
}
}
// 支付宝支付接口放行
if (response.config.url?.includes('/payment/aliPay/')) {
return Promise.resolve(response);
}
if (opts.isBackendSuccess(response) || response.config?.responseType === "blob") {
return Promise.resolve(response);
}
const fail = await opts.onBackendFail(response, instance);
if (fail) {
return fail;
}
const backendError = new AxiosError<ResponseData>(
'the backend request error',
BACKEND_ERROR_CODE,
response.config,
response.request,
response
);
await opts.onError(backendError);
return Promise.reject(backendError);
},
async (error: AxiosError<ResponseData>) => {
await opts.onError(error);
return Promise.reject(error);
}
);
function cancelRequest(requestId: string) {
const cancelTokenSource = cancelTokenSourceMap.get(requestId);
if (cancelTokenSource) {
cancelTokenSource.cancel();
cancelTokenSourceMap.delete(requestId);
}
}
function cancelAllRequest() {
cancelTokenSourceMap.forEach(cancelTokenSource => {
cancelTokenSource.cancel();
});
cancelTokenSourceMap.clear();
}
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}
/**
* create a request instance
*
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
return opts.transformBackendResponse(response);
}
return response.data as MappedType<R, T>;
} as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;
request.state = {} as State;
return request;
}
/**
* create a flat request instance
*
* The response data is a flat object: { data: any, error: AxiosError }
*
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
T = any,
R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
try {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
const data = opts.transformBackendResponse(response);
return { data, error: null };
}
return { data: response.data as MappedType<R, T>, error: null };
} catch (error) {
return { data: null, error };
}
} as FlatRequestInstance<State, ResponseData>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State;
return flatRequest;
}
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
export type * from './type';
export type { CreateAxiosDefaults, AxiosError };
+48
View File
@@ -0,0 +1,48 @@
import type { CreateAxiosDefaults } from 'axios';
import type { IAxiosRetryConfig } from 'axios-retry';
import { stringify } from 'qs';
import { isHttpSuccess } from './shared';
import type { RequestOption } from './type';
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
const opts: RequestOption<ResponseData> = {
onRequest: async config => config,
isBackendSuccess: _response => true,
onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {}
};
Object.assign(opts, options);
return opts;
}
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
const retryConfig: IAxiosRetryConfig = {
retries: 3
};
Object.assign(retryConfig, config);
return retryConfig;
}
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
const TEN_SECONDS = 30 * 1000;
const axiosConfig: CreateAxiosDefaults = {
timeout: TEN_SECONDS,
headers: {
'Content-Type': 'application/json'
},
validateStatus: isHttpSuccess,
paramsSerializer: params => {
return stringify(params);
}
};
Object.assign(axiosConfig, config);
return axiosConfig;
}
+28
View File
@@ -0,0 +1,28 @@
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export function getContentType(config: InternalAxiosRequestConfig) {
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
return contentType;
}
/**
* check if http status is success
*
* @param status
*/
export function isHttpSuccess(status: number) {
const isSuccessCode = status >= 200 && status < 300;
return isSuccessCode || status === 304;
}
/**
* is response json
*
* @param response axios response
*/
export function isResponseJson(response: AxiosResponse) {
const { responseType } = response.config;
return responseType === 'json' || responseType === undefined;
}
+101
View File
@@ -0,0 +1,101 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export type ContentType =
| 'text/html'
| 'text/plain'
| 'multipart/form-data'
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param config Axios config
*/
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* The hook to check backend response is success or not
*
* @param response Axios response
*/
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
/**
* The hook after backend request fail
*
* For example: You can handle the expired token in this hook
*
* @param response Axios response
* @param instance Axios instance
*/
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}
interface ResponseMap {
blob: Blob;
text: string;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
? ResponseMap[R]
: JsonType;
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
responseType?: R;
};
export interface RequestInstanceCommon<T> {
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;
}
/** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<T = any> = {
data: T;
error: null;
};
export type FlatResponseFailData<ResponseData = any> = {
data: null;
error: AxiosError<ResponseData>;
};
export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<T>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@sa/color-palette",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"colord": "2.9.3"
}
}
+29
View File
@@ -0,0 +1,29 @@
import { colord, extend } from 'colord';
import type { HslColor } from 'colord';
import labPlugin from 'colord/plugins/lab';
extend([labPlugin]);
export function isValidColor(color: string) {
return colord(color).isValid();
}
export function getHex(color: string) {
return colord(color).toHex();
}
export function getRgb(color: string) {
return colord(color).toRgb();
}
export function getHsl(color: string) {
return colord(color).toHsl();
}
export function getDeltaE(color1: string, color2: string) {
return colord(color1).delta(color2);
}
export function transformHslToHex(color: HslColor) {
return colord(color).toHex();
}
+41
View File
@@ -0,0 +1,41 @@
import { getColorPaletteFamily } from './palette';
import { getColorName } from './name';
import type { ColorPalette, ColorPaletteFamily, ColorPaletteItem, ColorPaletteNumber } from './type';
import defaultPalettes from './json/palette.json';
/**
* Get color palette by provided color and color name
*
* @param color The provided color
* @param colorName Color name
*/
export function getColorPalette(color: string, colorName: string) {
const colorPaletteFamily = getColorPaletteFamily(color, colorName);
const colorMap = new Map<ColorPaletteNumber, ColorPaletteItem>();
colorPaletteFamily.palettes.forEach(palette => {
colorMap.set(palette.number, palette);
});
const mainColor = colorMap.get(500) as ColorPaletteItem;
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hexcode === color) as ColorPaletteItem;
const colorPalette: ColorPalette = {
...colorPaletteFamily,
colorMap,
main: mainColor,
match: matchColor
};
return colorPalette;
}
export default getColorPalette;
/** The builtin color palettes */
const colorPalettes = defaultPalettes as ColorPaletteFamily[];
export { getColorName, colorPalettes };
export type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily };
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,274 @@
[
{
"key": "red",
"palettes": [
{ "hexcode": "#fef2f2", "number": 50, "name": "Bridesmaid" },
{ "hexcode": "#fee2e2", "number": 100, "name": "Pippin" },
{ "hexcode": "#fecaca", "number": 200, "name": "Your Pink" },
{ "hexcode": "#fca5a5", "number": 300, "name": "Cornflower Lilac" },
{ "hexcode": "#f87171", "number": 400, "name": "Bittersweet" },
{ "hexcode": "#ef4444", "number": 500, "name": "Cinnabar" },
{ "hexcode": "#dc2626", "number": 600, "name": "Persian Red" },
{ "hexcode": "#b91c1c", "number": 700, "name": "Thunderbird" },
{ "hexcode": "#991b1b", "number": 800, "name": "Old Brick" },
{ "hexcode": "#7f1d1d", "number": 900, "name": "Falu Red" },
{ "hexcode": "#450a0a", "number": 950, "name": "Mahogany" }
]
},
{
"key": "orange",
"palettes": [
{ "hexcode": "#fff7ed", "number": 50, "name": "Serenade" },
{ "hexcode": "#ffedd5", "number": 100, "name": "Derby" },
{ "hexcode": "#fed7aa", "number": 200, "name": "Caramel" },
{ "hexcode": "#fdba74", "number": 300, "name": "Macaroni and Cheese" },
{ "hexcode": "#fb923c", "number": 400, "name": "Neon Carrot" },
{ "hexcode": "#f97316", "number": 500, "name": "Ecstasy" },
{ "hexcode": "#ea580c", "number": 600, "name": "Trinidad" },
{ "hexcode": "#c2410c", "number": 700, "name": "Tia Maria" },
{ "hexcode": "#9a3412", "number": 800, "name": "Tabasco" },
{ "hexcode": "#7c2d12", "number": 900, "name": "Pueblo" },
{ "hexcode": "#431407", "number": 950, "name": "Rebel" }
]
},
{
"key": "amber",
"palettes": [
{ "hexcode": "#fffbeb", "number": 50, "name": "Island Spice" },
{ "hexcode": "#fef3c7", "number": 100, "name": "Beeswax" },
{ "hexcode": "#fde68a", "number": 200, "name": "Sweet Corn" },
{ "hexcode": "#fcd34d", "number": 300, "name": "Mustard" },
{ "hexcode": "#fbbf24", "number": 400, "name": "Lightning Yellow" },
{ "hexcode": "#f59e0b", "number": 500, "name": "California" },
{ "hexcode": "#d97706", "number": 600, "name": "Christine" },
{ "hexcode": "#b45309", "number": 700, "name": "Vesuvius" },
{ "hexcode": "#92400e", "number": 800, "name": "Korma" },
{ "hexcode": "#78350f", "number": 900, "name": "Copper Canyon" },
{ "hexcode": "#451a03", "number": 950, "name": "Brown Pod" }
]
},
{
"key": "yellow",
"palettes": [
{ "hexcode": "#fefce8", "number": 50, "name": "Orange White" },
{ "hexcode": "#fef9c3", "number": 100, "name": "Lemon Chiffon" },
{ "hexcode": "#fef08a", "number": 200, "name": "Sweet Corn" },
{ "hexcode": "#fde047", "number": 300, "name": "Bright Sun" },
{ "hexcode": "#facc15", "number": 400, "name": "Candlelight" },
{ "hexcode": "#eab308", "number": 500, "name": "Corn" },
{ "hexcode": "#ca8a04", "number": 600, "name": "Pirate Gold" },
{ "hexcode": "#a16207", "number": 700, "name": "Mai Tai" },
{ "hexcode": "#854d0e", "number": 800, "name": "Korma" },
{ "hexcode": "#713f12", "number": 900, "name": "Sepia" },
{ "hexcode": "#422006", "number": 950, "name": "Dark Ebony" }
]
},
{
"key": "lime",
"palettes": [
{ "hexcode": "#f7fee7", "number": 50, "name": "Spring Sun" },
{ "hexcode": "#ecfccb", "number": 100, "name": "Chiffon" },
{ "hexcode": "#d9f99d", "number": 200, "name": "Gossip" },
{ "hexcode": "#bef264", "number": 300, "name": "Sulu" },
{ "hexcode": "#a3e635", "number": 400, "name": "Conifer" },
{ "hexcode": "#84cc16", "number": 500, "name": "Lima" },
{ "hexcode": "#65a30d", "number": 600, "name": "Christi" },
{ "hexcode": "#4d7c0f", "number": 700, "name": "Green Leaf" },
{ "hexcode": "#3f6212", "number": 800, "name": "Dell" },
{ "hexcode": "#365314", "number": 900, "name": "Clover" },
{ "hexcode": "#1a2e05", "number": 950, "name": "Deep Forest Green" }
]
},
{
"key": "green",
"palettes": [
{ "hexcode": "#f0fdf4", "number": 50, "name": "Ottoman" },
{ "hexcode": "#dcfce7", "number": 100, "name": "Blue Romance" },
{ "hexcode": "#bbf7d0", "number": 200, "name": "Magic Mint" },
{ "hexcode": "#86efac", "number": 300, "name": "Algae Green" },
{ "hexcode": "#4ade80", "number": 400, "name": "Emerald" },
{ "hexcode": "#22c55e", "number": 500, "name": "Malachite" },
{ "hexcode": "#16a34a", "number": 600, "name": "Salem" },
{ "hexcode": "#15803d", "number": 700, "name": "Jewel" },
{ "hexcode": "#166534", "number": 800, "name": "Jewel" },
{ "hexcode": "#14532d", "number": 900, "name": "Green Pea" },
{ "hexcode": "#052e16", "number": 950, "name": "English Holly" }
]
},
{
"key": "emerald",
"palettes": [
{ "hexcode": "#ecfdf5", "number": 50, "name": "White Ice" },
{ "hexcode": "#d1fae5", "number": 100, "name": "Granny Apple" },
{ "hexcode": "#a7f3d0", "number": 200, "name": "Magic Mint" },
{ "hexcode": "#6ee7b7", "number": 300, "name": "Bermuda" },
{ "hexcode": "#34d399", "number": 400, "name": "Shamrock" },
{ "hexcode": "#10b981", "number": 500, "name": "Mountain Meadow" },
{ "hexcode": "#059669", "number": 600, "name": "Green Haze" },
{ "hexcode": "#047857", "number": 700, "name": "Watercourse" },
{ "hexcode": "#065f46", "number": 800, "name": "Watercourse" },
{ "hexcode": "#064e3b", "number": 900, "name": "Evening Sea" },
{ "hexcode": "#022c22", "number": 950, "name": "Burnham" }
]
},
{
"key": "teal",
"palettes": [
{ "hexcode": "#f0fdfa", "number": 50, "name": "White Ice" },
{ "hexcode": "#ccfbf1", "number": 100, "name": "Scandal" },
{ "hexcode": "#99f6e4", "number": 200, "name": "Ice Cold" },
{ "hexcode": "#5eead4", "number": 300, "name": "Turquoise Blue" },
{ "hexcode": "#2dd4bf", "number": 400, "name": "Turquoise" },
{ "hexcode": "#14b8a6", "number": 500, "name": "Java" },
{ "hexcode": "#0d9488", "number": 600, "name": "Blue Chill" },
{ "hexcode": "#0f766e", "number": 700, "name": "Genoa" },
{ "hexcode": "#115e59", "number": 800, "name": "Eden" },
{ "hexcode": "#134e4a", "number": 900, "name": "Eden" },
{ "hexcode": "#042f2e", "number": 950, "name": "Tiber" }
]
},
{
"key": "cyan",
"palettes": [
{ "hexcode": "#ecfeff", "number": 50, "name": "Bubbles" },
{ "hexcode": "#cffafe", "number": 100, "name": "Oyster Bay" },
{ "hexcode": "#a5f3fc", "number": 200, "name": "Anakiwa" },
{ "hexcode": "#67e8f9", "number": 300, "name": "Spray" },
{ "hexcode": "#22d3ee", "number": 400, "name": "Bright Turquoise" },
{ "hexcode": "#06b6d4", "number": 500, "name": "Cerulean" },
{ "hexcode": "#0891b2", "number": 600, "name": "Bondi Blue" },
{ "hexcode": "#0e7490", "number": 700, "name": "Blue Chill" },
{ "hexcode": "#155e75", "number": 800, "name": "Blumine" },
{ "hexcode": "#164e63", "number": 900, "name": "Chathams Blue" },
{ "hexcode": "#083344", "number": 950, "name": "Tarawera" }
]
},
{
"key": "sky",
"palettes": [
{ "hexcode": "#f0f9ff", "number": 50, "name": "Alice Blue" },
{ "hexcode": "#e0f2fe", "number": 100, "name": "Pattens Blue" },
{ "hexcode": "#bae6fd", "number": 200, "name": "French Pass" },
{ "hexcode": "#7dd3fc", "number": 300, "name": "Malibu" },
{ "hexcode": "#38bdf8", "number": 400, "name": "Picton Blue" },
{ "hexcode": "#0ea5e9", "number": 500, "name": "Cerulean" },
{ "hexcode": "#0284c7", "number": 600, "name": "Lochmara" },
{ "hexcode": "#0369a1", "number": 700, "name": "Bahama Blue" },
{ "hexcode": "#075985", "number": 800, "name": "Venice Blue" },
{ "hexcode": "#0c4a6e", "number": 900, "name": "Chathams Blue" },
{ "hexcode": "#082f49", "number": 950, "name": "Blue Whale" }
]
},
{
"key": "blue",
"palettes": [
{ "hexcode": "#eff6ff", "number": 50, "name": "Zumthor" },
{ "hexcode": "#dbeafe", "number": 100, "name": "Hawkes Blue" },
{ "hexcode": "#bfdbfe", "number": 200, "name": "Tropical Blue" },
{ "hexcode": "#93c5fd", "number": 300, "name": "Malibu" },
{ "hexcode": "#60a5fa", "number": 400, "name": "Cornflower Blue" },
{ "hexcode": "#3b82f6", "number": 500, "name": "Dodger Blue" },
{ "hexcode": "#2563eb", "number": 600, "name": "Royal Blue" },
{ "hexcode": "#1d4ed8", "number": 700, "name": "Cerulean Blue" },
{ "hexcode": "#1e40af", "number": 800, "name": "Persian Blue" },
{ "hexcode": "#1e3a8a", "number": 900, "name": "Bay of Many" },
{ "hexcode": "#172554", "number": 950, "name": "Bunting" }
]
},
{
"key": "indigo",
"palettes": [
{ "hexcode": "#eef2ff", "number": 50, "name": "Zircon" },
{ "hexcode": "#e0e7ff", "number": 100, "name": "Hawkes Blue" },
{ "hexcode": "#c7d2fe", "number": 200, "name": "Periwinkle" },
{ "hexcode": "#a5b4fc", "number": 300, "name": "Perano" },
{ "hexcode": "#818cf8", "number": 400, "name": "Portage" },
{ "hexcode": "#6366f1", "number": 500, "name": "Royal Blue" },
{ "hexcode": "#4f46e5", "number": 600, "name": "Royal Blue" },
{ "hexcode": "#4338ca", "number": 700, "name": "Governor Bay" },
{ "hexcode": "#3730a3", "number": 800, "name": "Governor Bay" },
{ "hexcode": "#312e81", "number": 900, "name": "Minsk" },
{ "hexcode": "#1e1b4b", "number": 950, "name": "Port Gore" }
]
},
{
"key": "violet",
"palettes": [
{ "hexcode": "#f5f3ff", "number": 50, "name": "Titan White" },
{ "hexcode": "#ede9fe", "number": 100, "name": "Titan White" },
{ "hexcode": "#ddd6fe", "number": 200, "name": "Fog" },
{ "hexcode": "#c4b5fd", "number": 300, "name": "Melrose" },
{ "hexcode": "#a78bfa", "number": 400, "name": "Dull Lavender" },
{ "hexcode": "#8b5cf6", "number": 500, "name": "Medium Purple" },
{ "hexcode": "#7c3aed", "number": 600, "name": "Purple Heart" },
{ "hexcode": "#6d28d9", "number": 700, "name": "Purple Heart" },
{ "hexcode": "#5b21b6", "number": 800, "name": "Purple Heart" },
{ "hexcode": "#4c1d95", "number": 900, "name": "Daisy Bush" },
{ "hexcode": "#2e1065", "number": 950, "name": "Violent Violet" }
]
},
{
"key": "purple",
"palettes": [
{ "hexcode": "#faf5ff", "number": 50, "name": "Magnolia" },
{ "hexcode": "#f3e8ff", "number": 100, "name": "Blue Chalk" },
{ "hexcode": "#e9d5ff", "number": 200, "name": "Blue Chalk" },
{ "hexcode": "#d8b4fe", "number": 300, "name": "Mauve" },
{ "hexcode": "#c084fc", "number": 400, "name": "Heliotrope" },
{ "hexcode": "#a855f7", "number": 500, "name": "Medium Purple" },
{ "hexcode": "#9333ea", "number": 600, "name": "Electric Violet" },
{ "hexcode": "#7e22ce", "number": 700, "name": "Purple Heart" },
{ "hexcode": "#6b21a8", "number": 800, "name": "Seance" },
{ "hexcode": "#581c87", "number": 900, "name": "Daisy Bush" },
{ "hexcode": "#3b0764", "number": 950, "name": "Christalle" }
]
},
{
"key": "fuchsia",
"palettes": [
{ "hexcode": "#fdf4ff", "number": 50, "name": "White Pointer" },
{ "hexcode": "#fae8ff", "number": 100, "name": "White Pointer" },
{ "hexcode": "#f5d0fe", "number": 200, "name": "Mauve" },
{ "hexcode": "#f0abfc", "number": 300, "name": "Mauve" },
{ "hexcode": "#e879f9", "number": 400, "name": "Heliotrope" },
{ "hexcode": "#d946ef", "number": 500, "name": "Heliotrope" },
{ "hexcode": "#c026d3", "number": 600, "name": "Fuchsia Pink" },
{ "hexcode": "#a21caf", "number": 700, "name": "Violet Eggplant" },
{ "hexcode": "#86198f", "number": 800, "name": "Seance" },
{ "hexcode": "#701a75", "number": 900, "name": "Seance" },
{ "hexcode": "#4a044e", "number": 950, "name": "Clairvoyant" }
]
},
{
"key": "pink",
"palettes": [
{ "hexcode": "#fdf2f8", "number": 50, "name": "Wisp Pink" },
{ "hexcode": "#fce7f3", "number": 100, "name": "Carousel Pink" },
{ "hexcode": "#fbcfe8", "number": 200, "name": "Classic Rose" },
{ "hexcode": "#f9a8d4", "number": 300, "name": "Lavender Pink" },
{ "hexcode": "#f472b6", "number": 400, "name": "Persian Pink" },
{ "hexcode": "#ec4899", "number": 500, "name": "Brilliant Rose" },
{ "hexcode": "#db2777", "number": 600, "name": "Cerise" },
{ "hexcode": "#be185d", "number": 700, "name": "Maroon Flush" },
{ "hexcode": "#9d174d", "number": 800, "name": "Disco" },
{ "hexcode": "#831843", "number": 900, "name": "Disco" },
{ "hexcode": "#500724", "number": 950, "name": "Cab Sav" }
]
},
{
"key": "rose",
"palettes": [
{ "hexcode": "#fff1f2", "number": 50, "name": "Lavender blush" },
{ "hexcode": "#ffe4e6", "number": 100, "name": "Cosmos" },
{ "hexcode": "#fecdd3", "number": 200, "name": "Pastel Pink" },
{ "hexcode": "#fda4af", "number": 300, "name": "Sweet Pink" },
{ "hexcode": "#fb7185", "number": 400, "name": "Froly" },
{ "hexcode": "#f43f5e", "number": 500, "name": "Radical Red" },
{ "hexcode": "#e11d48", "number": 600, "name": "Amaranth" },
{ "hexcode": "#be123c", "number": 700, "name": "Cardinal" },
{ "hexcode": "#9f1239", "number": 800, "name": "Shiraz" },
{ "hexcode": "#881337", "number": 900, "name": "Claret" },
{ "hexcode": "#4c0519", "number": 950, "name": "Cab Sav" }
]
}
]
+46
View File
@@ -0,0 +1,46 @@
import { getHex, getHsl, getRgb } from './color';
import colorNames from './json/color-name.json';
export function getColorName(color: string) {
const hex = getHex(color);
const rgb = getRgb(color);
const hsl = getHsl(color);
let ndf = 0;
let ndf1 = 0;
let ndf2 = 0;
let cl = -1;
let df = -1;
let name = '';
colorNames.some((item, index) => {
const [hexValue, colorName] = item;
const hexcode = `#${hexValue}`;
const match = hex === hexcode;
if (match) {
name = colorName;
} else {
const { r, g, b } = getRgb(hexcode);
const { h, s, l } = getHsl(hexcode);
ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
ndf = ndf1 + ndf2 * 2;
if (df < 0 || df > ndf) {
df = ndf;
cl = index;
}
}
return match;
});
name = cl < 0 ? 'Invalid Color' : colorNames[cl][1];
return name;
}
+95
View File
@@ -0,0 +1,95 @@
import { getDeltaE, getHsl, isValidColor, transformHslToHex } from './color';
import { getColorName } from './name';
import type { ColorPaletteFamily, ColorPaletteFamilyWithNearestPalette } from './type';
import defaultPalettes from './json/palette.json';
export function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
const familyWithConfig = families.map(family => {
const palettes = family.palettes.map(palette => {
return {
...palette,
delta: getDeltaE(color, palette.hexcode)
};
});
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
return {
...family,
palettes,
nearestPalette
};
});
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
);
const { l } = getHsl(color);
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
...nearestPaletteFamily,
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
const { l: prevLightness } = getHsl(prev.hexcode);
const { l: currLightness } = getHsl(curr.hexcode);
const deltaPrev = Math.abs(prevLightness - l);
const deltaCurr = Math.abs(currLightness - l);
return deltaPrev < deltaCurr ? prev : curr;
})
};
return paletteFamily;
}
export function getColorPaletteFamily(color: string, colorName: string) {
if (!isValidColor(color)) {
throw new Error('Invalid color, please check color value!');
}
const { h: h1, s: s1 } = getHsl(color);
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(
color,
defaultPalettes as ColorPaletteFamily[]
);
const { number, hexcode } = nearestLightnessPalette;
const { h: h2, s: s2 } = getHsl(hexcode);
const deltaH = h1 - h2 || h2;
const sRatio = s1 / s2;
const colorPaletteFamily: ColorPaletteFamily = {
key: colorName,
palettes: palettes.map(palette => {
let hexValue = color;
const isSame = number === palette.number;
if (!isSame) {
const { h: h3, s: s3, l } = getHsl(palette.hexcode);
const newH = deltaH < 0 ? h3 + deltaH : deltaH;
const newS = s3 * sRatio;
hexValue = transformHslToHex({
h: newH,
s: newS,
l
});
}
return {
hexcode: hexValue,
number: palette.number,
name: getColorName(hexValue)
};
})
};
return colorPaletteFamily;
}
+49
View File
@@ -0,0 +1,49 @@
/** The color palette number */
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
/** The color palette item */
export type ColorPaletteItem = {
/** The color hexcode */
hexcode: string;
/**
* The color number
*
* @link {@link ColorPaletteNumber}
*/
number: ColorPaletteNumber;
/** The color name */
name: string;
};
export type ColorPaletteFamily = {
/** The color palette family key */
key: string;
/** The color palette family's palettes */
palettes: ColorPaletteItem[];
};
export type ColorPaletteWithDelta = ColorPaletteItem & {
delta: number;
};
export type ColorPaletteItemWithName = ColorPaletteItem & {
name: string;
};
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
nearestPalette: ColorPaletteWithDelta;
nearestLightnessPalette: ColorPaletteWithDelta;
};
export type ColorPalette = ColorPaletteFamily & {
/** The color map of the palette */
colorMap: Map<ColorPaletteNumber, ColorPaletteItem>;
/**
* The main color of the palette
*
* Which number is 500
*/
main: ColorPaletteItemWithName;
/** The match color of the palette */
match: ColorPaletteItemWithName;
};
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
};
+42
View File
@@ -0,0 +1,42 @@
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
root: true,
env: {
browser: true,
node: true,
commonjs: true,
es2024: true
},
parserOptions: {
ecmaVersion: 2024,
ecmaFeatures: {
jsx: true
},
sourceType: 'module'
},
ignorePatterns: [
'node_modules',
'*.min.*',
'CHANGELOG.md',
'dist',
'LICENSE*',
'output',
'coverage',
'public',
'temp',
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
'__snapshots__',
'!.github',
'!.vitepress',
'!.vscode'
],
plugins: ['n', 'promise'],
extends: [require.resolve('../rules/all.js'), 'plugin:import/recommended'],
rules: {
// import
'import/no-mutable-exports': 'error',
'import/no-named-as-default': 'off'
}
};
@@ -0,0 +1,9 @@
const prettierRules = require('../rules/prettier');
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': ['error', prettierRules]
}
};
+59
View File
@@ -0,0 +1,59 @@
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
plugins: ['@typescript-eslint'],
extends: [require.resolve('./js.js'), 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended'],
settings: {
'import/resolver': {
typescript: {
project: ['tsconfig.json', 'packages/*/tsconfig.json', 'examples/*/tsconfig.json', 'docs/*/tsconfig.json']
}
}
},
overrides: [
{
files: ['*.ts', '*.tsx', '*.mts', '*.cts'],
parser: '@typescript-eslint/parser'
},
{
files: ['*.js', '*.mjs', '*.cjs', '*.cts'],
rules: {
'@typescript-eslint/no-var-requires': 'off'
}
}
],
rules: {
// TS
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: false }],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true
}
],
// Override JS
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'all',
ignoreRestSiblings: false,
varsIgnorePattern: '^_',
argsIgnorePattern: '^_'
}
],
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
// off
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
}
};
+28
View File
@@ -0,0 +1,28 @@
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = {
extends: ['plugin:vue/vue3-recommended', require.resolve('./base.js')],
overrides: [
{
files: ['*.vue'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: {
js: 'espree',
jsx: 'espree',
ts: '@typescript-eslint/parser',
tsx: '@typescript-eslint/parser'
},
extraFileExtensions: ['.vue'],
ecmaFeatures: {
jsx: true
}
},
rules: {
'no-undef': 'off' // TS will check un declared variables, if the script code is is in a .vue file, this rule should not disabled
}
}
],
rules: {
'vue/multi-word-component-names': 'off'
}
};
+4
View File
@@ -0,0 +1,4 @@
const baseConfig = require('./configs/base');
/** @type {import('eslint').ESLint.ConfigData} */
module.exports = baseConfig;
+23
View File
@@ -0,0 +1,23 @@
{
"name": "eslint-config-sa",
"version": "1.0.0",
"description": "WANFIAdmin's eslint config resets",
"exports": {
".": "./index.js",
"./vue": "./configs/vue.js"
},
"devDependencies": {
"@types/eslint": "8.56.1",
"@typescript-eslint/eslint-plugin": "6.18.0",
"@typescript-eslint/parser": "6.18.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.6.1",
"eslint-plugin-prettier": "5.1.2",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.19.2",
"prettier": "3.1.1"
}
}
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
/** @type {import('prettier').Options} */
module.exports = {
printWidth: 120,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'none',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
rangeStart: 0,
rangeEnd: Number.POSITIVE_INFINITY,
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore',
vueIndentScriptAndStyle: false,
endOfLine: 'lf',
embeddedLanguageFormatting: 'auto',
singleAttributePerLine: false
};
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@sa/hooks",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*"
}
}
+10
View File
@@ -0,0 +1,10 @@
import useBoolean from './use-boolean';
import useLoading from './use-loading';
import useCountDown from './use-count-down';
import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export * from './use-table';
+31
View File
@@ -0,0 +1,31 @@
import { ref } from 'vue';
/**
* Boolean
*
* @param initValue Init value
*/
export default function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle
};
}
+96
View File
@@ -0,0 +1,96 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/**
* Use context
*
* @example
* ```ts
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
*
* // context.ts
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
* count.value++;
* }
*
* function decrement() {
* count.value--;
* }
*
* return {
* count,
* increment,
* decrement
* };
* })
* ``` // A.vue
* ```vue
* <template>
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
*
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
* <template>
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
*
* const { count, increment } = useStore();
* </script>
* ```;
*
* // C.vue is same as B.vue
*
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
return {
/** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
};
}
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvide(context: T) {
provide(injectKey, context);
return context;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
};
}
+49
View File
@@ -0,0 +1,49 @@
import { computed, onScopeDispose, ref } from 'vue';
import { useRafFn } from '@vueuse/core';
/**
* count down
*
* @param seconds - count down seconds
*/
export default function useCountDown(seconds: number) {
const FPS_PER_SECOND = 60;
const fps = ref(0);
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
const isCounting = computed(() => fps.value > 0);
const { pause, resume } = useRafFn(
() => {
if (fps.value > 0) {
fps.value -= 1;
} else {
pause();
}
},
{ immediate: false }
);
function start(updateSeconds: number = seconds) {
fps.value = FPS_PER_SECOND * updateSeconds;
resume();
}
function stop() {
fps.value = 0;
pause();
}
onScopeDispose(() => {
pause();
});
return {
count,
isCounting,
start,
stop
};
}
+16
View File
@@ -0,0 +1,16 @@
import useBoolean from './use-boolean';
/**
* Loading
*
* @param initValue Init value
*/
export default function useLoading(initValue = false) {
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
return {
loading,
startLoading,
endLoading
};
}
+79
View File
@@ -0,0 +1,79 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { createFlatRequest } from '@sa/axios';
import type {
AxiosError,
CreateAxiosDefaults,
CustomAxiosRequestConfig,
MappedType,
RequestOption,
ResponseType
} from '@sa/axios';
import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<T>;
error: Ref<null>;
};
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
data: Ref<null>;
error: Ref<AxiosError<ResponseData>>;
};
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData = any> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}
/**
* create a hook request instance
*
* @param axiosConfig
* @param options
*/
export default function createHookRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const request = createFlatRequest<ResponseData>(axiosConfig, options);
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const { loading, startLoading, endLoading } = useLoading();
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
startLoading();
request(config).then(res => {
if (res.data) {
data.value = res.data;
} else {
error.value = res.error;
}
endLoading();
});
return {
loading,
data,
error
};
} as HookRequestInstance<ResponseData>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest;
return hookRequest;
}
+50
View File
@@ -0,0 +1,50 @@
import { h } from 'vue';
import type { Component } from 'vue';
/**
* Svg icon render hook
*
* @param SvgIcon Svg icon component
*/
export default function useSvgIconRender(SvgIcon: Component) {
interface IconConfig {
/** Iconify icon name */
icon?: string;
/** Local icon name */
localIcon?: string;
/** Icon color */
color?: string;
/** Icon size */
fontSize?: number;
}
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
/**
* Svg icon VNode
*
* @param config
*/
const SvgIconVNode = (config: IconConfig) => {
const { color, fontSize, icon, localIcon } = config;
const style: IconStyle = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
if (!icon && !localIcon) {
return undefined;
}
return () => h(SvgIcon, { icon, localIcon, style });
};
return {
SvgIconVNode
};
}
+152
View File
@@ -0,0 +1,152 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheck = {
key: string;
title: string;
checked: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
rows: TableDataWithIndex<T>[];
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** row key */
rowKey?: string;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* callback when response fetched
*
* @param transformed transformed data
*/
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
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 allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
...col,
checked: checkMap.get(col.key) ?? col.checked
}));
}
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.rows;
setEmpty(transformed.rows.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
getData();
}
if (immediate) {
getData();
}
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "sa/vue",
"rules": {
"vue/multi-word-component-names": "off"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@sa/materials",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"@simonwep/pickr": "1.9.0",
"simplebar-vue": "2.3.3"
},
"devDependencies": {
"typed-css-modules": "0.9.1"
}
}
+7
View File
@@ -0,0 +1,7 @@
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
import PageTab from './libs/page-tab';
import SimpleScrollbar from './libs/simple-scrollbar';
import ColorPicker from './libs/color-picker';
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar, ColorPicker };
export * from './types';
@@ -0,0 +1,63 @@
/* @type */
.layout-header,
.layout-header-placement {
height: var(--soy-header-height);
}
.layout-header {
z-index: var(--soy-header-z-index);
}
.layout-tab {
top: var(--soy-header-height);
height: var(--soy-tab-height);
z-index: var(--soy-tab-z-index);
}
.layout-tab-placement {
height: var(--soy-tab-height);
}
.layout-sider {
width: var(--soy-sider-width);
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider {
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider-mask {
z-index: var(--soy-mobile-sider-z-index);
}
.layout-sider_collapsed {
width: var(--soy-sider-collapsed-width);
z-index: var(--soy-sider-z-index);
}
.layout-footer,
.layout-footer-placement {
height: var(--soy-footer-height);
}
.layout-footer {
z-index: var(--soy-footer-z-index);
}
.left-gap {
padding-left: var(--soy-sider-width);
}
.left-gap_collapsed {
padding-left: var(--soy-sider-collapsed-width);
}
.sider-padding-top {
padding-top: var(--soy-header-height);
}
.sider-padding-bottom {
padding-bottom: var(--soy-footer-height);
}
@@ -0,0 +1,17 @@
declare const styles: {
readonly 'layout-header': string;
readonly 'layout-header-placement': string;
readonly 'layout-tab': string;
readonly 'layout-tab-placement': string;
readonly 'layout-sider': string;
readonly 'layout-mobile-sider': string;
readonly 'layout-mobile-sider-mask': string;
readonly 'layout-sider_collapsed': string;
readonly 'layout-footer': string;
readonly 'layout-footer-placement': string;
readonly 'left-gap': string;
readonly 'left-gap_collapsed': string;
readonly 'sider-padding-top': string;
readonly 'sider-padding-bottom': string;
};
export default styles;
@@ -0,0 +1,5 @@
import AdminLayout from './index.vue';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
export default AdminLayout;
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
@@ -0,0 +1,237 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { AdminLayoutProps } from '../../types';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
import style from './index.module.css';
defineOptions({
name: 'AdminLayout'
});
const props = withDefaults(defineProps<AdminLayoutProps>(), {
mode: 'vertical',
scrollMode: 'content',
scrollElId: LAYOUT_SCROLL_EL_ID,
commonClass: 'transition-all-300',
fixedTop: true,
maxZIndex: LAYOUT_MAX_Z_INDEX,
headerVisible: true,
headerHeight: 56,
tabVisible: true,
tabHeight: 48,
siderVisible: true,
siderCollapse: false,
siderWidth: 220,
siderCollapsedWidth: 64,
footerVisible: true,
footerHeight: 48,
rightFooter: false
});
interface Emits {
/** Update siderCollapse */
(e: 'update:siderCollapse', collapse: boolean): void;
}
const emit = defineEmits<Emits>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/** Main */
default?: SlotFn;
/** Header */
header?: SlotFn;
/** Tab */
tab?: SlotFn;
/** Sider */
sider?: SlotFn;
/** Footer */
footer?: SlotFn;
};
const slots = defineSlots<Slots>();
const cssVars = computed(() => createLayoutCssVars(props));
// config visible
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
// scroll mode
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
const isContentScroll = computed(() => props.scrollMode === 'content');
// layout direction
const isVertical = computed(() => props.mode === 'vertical');
const isHorizontal = computed(() => props.mode === 'horizontal');
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
// css
const leftGapClass = computed(() => {
if (!props.fullContent && showSider.value) {
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
}
return '';
});
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
const footerLeftGapClass = computed(() => {
const condition1 = isVertical.value;
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
if (condition1 || condition2 || condition3) {
return leftGapClass.value;
}
return '';
});
const siderPaddingClass = computed(() => {
let cls = '';
if (showHeader.value && !headerLeftGapClass.value) {
cls += style['sider-padding-top'];
}
if (showFooter.value && !footerLeftGapClass.value) {
cls += ` ${style['sider-padding-bottom']}`;
}
return cls;
});
function handleClickMask() {
emit('update:siderCollapse', true);
}
</script>
<template>
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
class="h-full flex flex-col"
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-header'],
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="header"></slot>
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-header-placement']]"
></div>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
class="flex-shrink-0"
:class="[
style['layout-tab'],
commonClass,
tabClass,
{ 'top-0!': fullContent || !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="tab"></slot>
</div>
<div
v-show="fullContent || fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-tab-placement']]"
></div>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
class="absolute left-0 top-0 h-full"
:class="[
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
class="absolute left-0 top-0 h-full w-0 bg-white"
:class="[
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
<div
v-show="!siderCollapse"
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
:class="[style['layout-mobile-sider-mask']]"
@click="handleClickMask"
></div>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
class="flex flex-col flex-grow"
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
>
<slot></slot>
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-footer'],
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
]"
>
<slot name="footer"></slot>
</footer>
<div
v-show="!fullContent && fixedFooter"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-footer-placement']]"
></div>
</template>
</div>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,68 @@
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
/** The id of the scroll element of the layout */
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
/** The max z-index of the layout */
export const LAYOUT_MAX_Z_INDEX = 100;
/**
* Create layout css vars by css vars props
*
* @param props Css vars props
*/
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
const cssVars: LayoutCssVars = {
'--soy-header-height': `${props.headerHeight}px`,
'--soy-header-z-index': props.headerZIndex,
'--soy-tab-height': `${props.tabHeight}px`,
'--soy-tab-z-index': props.tabZIndex,
'--soy-sider-width': `${props.siderWidth}px`,
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
'--soy-sider-z-index': props.siderZIndex,
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
'--soy-footer-height': `${props.footerHeight}px`,
'--soy-footer-z-index': props.footerZIndex
};
return cssVars;
}
/**
* Create layout css vars
*
* @param props
*/
export function createLayoutCssVars(props: AdminLayoutProps) {
const {
mode,
isMobile,
maxZIndex = LAYOUT_MAX_Z_INDEX,
headerHeight,
tabHeight,
siderWidth,
siderCollapsedWidth,
footerHeight
} = props;
const headerZIndex = maxZIndex - 3;
const tabZIndex = maxZIndex - 5;
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
const footerZIndex = maxZIndex - 5;
const cssProps: LayoutCssVarsProps = {
headerHeight,
headerZIndex,
tabHeight,
tabZIndex,
siderWidth,
siderZIndex,
mobileSiderZIndex,
siderCollapsedWidth,
footerHeight,
footerZIndex
};
return createLayoutCssVarsByCssVarsProps(cssProps);
}
@@ -0,0 +1,3 @@
import ColorPicker from './index.vue';
export default ColorPicker;
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import ColorPicker from '@simonwep/pickr';
import '@simonwep/pickr/dist/themes/nano.min.css';
defineOptions({
name: 'ColorPicker'
});
interface Props {
color: string;
palettes?: string[];
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
palettes: () => [
'#3b82f6',
'#6366f1',
'#8b5cf6',
'#a855f7',
'#0ea5e9',
'#06b6d4',
'#f43f5e',
'#ef4444',
'#ec4899',
'#d946ef',
'#f97316',
'#f59e0b',
'#eab308',
'#84cc16',
'#22c55e',
'#10b981',
'#14b8a6'
]
});
interface Emits {
(e: 'update:color', value: string): void;
}
const emit = defineEmits<Emits>();
const domRef = ref<HTMLElement | null>(null);
const instance = ref<ColorPicker | null>(null);
function handleColorChange(hsva: ColorPicker.HSVaColor) {
const color = hsva.toHEXA().toString();
emit('update:color', color);
}
function initColorPicker() {
if (!domRef.value) return;
instance.value = ColorPicker.create({
el: domRef.value,
theme: 'nano',
swatches: props.palettes,
lockOpacity: true,
default: props.color,
disabled: props.disabled,
components: {
preview: true,
opacity: false,
hue: true,
interaction: {
hex: true,
rgba: true,
input: true
}
}
});
instance.value.on('change', handleColorChange);
}
function updateColor(color: string) {
if (!instance.value) return;
instance.value.setColor(color);
}
function updateDisabled(disabled: boolean) {
if (!instance.value) return;
if (disabled) {
instance.value.disable();
} else {
instance.value.enable();
}
}
watch(
() => props.color,
value => {
updateColor(value);
}
);
watch(
() => props.disabled,
value => {
updateDisabled(value);
}
);
onMounted(() => {
initColorPicker();
});
</script>
<template>
<div ref="domRef"></div>
</template>
<style scoped></style>
@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import style from './index.module.css';
defineOptions({
name: 'ButtonTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-1px rounded-4px border-solid px-12px py-4px"
:class="[
style['button-tab'],
{ [style['button-tab_dark']]: darkMode },
{ [style['button-tab_active']]: active },
{ [style['button-tab_active_dark']]: active && darkMode }
]"
>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'ChromeTabBg'
});
</script>
<template>
<svg class="size-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
</svg>
</g>
</svg>
</template>
<style scoped></style>
@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import ChromeTabBg from './chrome-tab-bg.vue';
import style from './index.module.css';
defineOptions({
name: 'ChromeTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
:class="[
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode }
]"
>
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'IconClose'
});
const emit = defineEmits<Emits>();
interface Emits {
(e: 'click'): void;
}
function handleClick() {
emit('click');
}
</script>
<template>
<div
class=":soy: relative inline-flex justify-center items-center w-16px h-16px text-14px rd-50%"
@click.stop="handleClick"
>
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
></path>
</svg>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,97 @@
/* @type */
.button-tab {
border-color: #e5e7eb;
}
.button-tab_dark {
border-color: #ffffff3d;
}
.button-tab:hover {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
}
.button-tab_active {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
background-color: var(--soy-primary-color-opacity1);
}
.button-tab_active_dark {
background-color: var(--soy-primary-color-opacity2);
}
.button-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: var(--soy-primary-color);
}
.button-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab:hover {
z-index: 9;
}
.chrome-tab_active {
z-index: 10;
color: var(--soy-primary-color);
}
.chrome-tab__bg {
color: transparent;
}
.chrome-tab_active .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_active_dark .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab:hover .chrome-tab__bg {
color: #dee1e6;
}
.chrome-tab_active:hover .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_dark:hover .chrome-tab__bg {
color: #333333;
}
.chrome-tab_active_dark:hover .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: #9ca3af;
}
.chrome-tab_active .svg-close:hover {
background-color: var(--soy-primary-color);
}
.chrome-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab_active .chrome-tab-divider {
opacity: 0;
}
.chrome-tab:hover .chrome-tab-divider {
opacity: 0;
}
.chrome-tab_dark .chrome-tab-divider {
background-color: rgba(255, 255, 255, 0.9);
}
@@ -0,0 +1,14 @@
declare const styles: {
readonly 'button-tab': string;
readonly 'button-tab_dark': string;
readonly 'button-tab_active': string;
readonly 'button-tab_active_dark': string;
readonly 'chrome-tab': string;
readonly 'chrome-tab_active': string;
readonly 'chrome-tab__bg': string;
readonly 'chrome-tab_active_dark': string;
readonly 'chrome-tab_dark': string;
readonly 'chrome-tab-divider': string;
readonly 'svg-close': string;
};
export default styles;
@@ -0,0 +1,3 @@
import PageTab from './index.vue';
export default PageTab;
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import type { PageTabMode, PageTabProps } from '../../types';
import { ACTIVE_COLOR, createTabCssVars } from './shared';
import ChromeTab from './chrome-tab.vue';
import ButtonTab from './button-tab.vue';
import SvgClose from './svg-close.vue';
import style from './index.module.css';
defineOptions({
name: 'PageTab'
});
const props = withDefaults(defineProps<PageTabProps>(), {
mode: 'chrome',
commonClass: 'transition-all-300',
activeColor: ACTIVE_COLOR,
closable: true
});
interface Emits {
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass } = props;
const tabComponentMap = {
chrome: {
component: ChromeTab,
class: chromeClass
},
button: {
component: ButtonTab,
class: buttonClass
}
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
return tabComponentMap[mode];
});
const cssVars = computed(() => createTabCssVars(props.activeColor));
const bindProps = computed(() => {
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
return rest;
});
function handleClose() {
emit('close');
}
</script>
<template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
<template #prefix>
<slot name="prefix"></slot>
</template>
<slot></slot>
<template #suffix>
<slot name="suffix">
<SvgClose v-if="closable" :class="[style['svg-close']]" @click="handleClose" />
</slot>
</template>
</component>
</template>
<style scoped></style>
@@ -0,0 +1,31 @@
import { addColorAlpha, transformColorWithOpacity } from '@sa/utils';
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
/** The active color of the tab */
export const ACTIVE_COLOR = '#1890ff';
function createCssVars(props: PageTabCssVarsProps) {
const cssVars: PageTabCssVars = {
'--soy-primary-color': props.primaryColor,
'--soy-primary-color1': props.primaryColor1,
'--soy-primary-color2': props.primaryColor2,
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
'--soy-primary-color-opacity3': props.primaryColorOpacity3
};
return cssVars;
}
export function createTabCssVars(primaryColor: string) {
const cssProps: PageTabCssVarsProps = {
primaryColor,
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
};
return createCssVars(cssProps);
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'SvgClose'
});
const emit = defineEmits<Emits>();
interface Emits {
(e: 'click'): void;
}
function handleClick() {
emit('click');
}
</script>
<template>
<div
class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px"
@click.stop="handleClick"
>
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,3 @@
import SimpleScrollbar from './index.vue';
export default SimpleScrollbar;
@@ -0,0 +1,18 @@
<script setup lang="ts">
import Simplebar from 'simplebar-vue';
import 'simplebar-vue/dist/simplebar.min.css';
defineOptions({
name: 'SimpleScrollbar'
});
</script>
<template>
<div class="h-full flex-1-hidden">
<Simplebar data-simplebar-auto-hide="false" class="h-full">
<slot />
</Simplebar>
</div>
</template>
<style scoped></style>
+294
View File
@@ -0,0 +1,294 @@
/** Header config */
interface AdminLayoutHeaderConfig {
/**
* Whether header is visible
*
* @default true
*/
headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/**
* Header height
*
* @default 56px
*/
headerHeight?: number;
}
/** Tab config */
interface AdminLayoutTabConfig {
/**
* Whether tab is visible
*
* @default true
*/
tabVisible?: boolean;
/**
* Tab class
*
* @default ''
*/
tabClass?: string;
/**
* Tab height
*
* @default 48px
*/
tabHeight?: number;
}
/** Sider config */
interface AdminLayoutSiderConfig {
/**
* Whether sider is visible
*
* @default true
*/
siderVisible?: boolean;
/**
* Sider class
*
* @default ''
*/
siderClass?: string;
/**
* Mobile sider class
*
* @default ''
*/
mobileSiderClass?: string;
/**
* Sider collapse status
*
* @default false
*/
siderCollapse?: boolean;
/**
* Sider width when collapse is false
*
* @default '220px'
*/
siderWidth?: number;
/**
* Sider width when collapse is true
*
* @default '64px'
*/
siderCollapsedWidth?: number;
}
/** Content config */
export interface AdminLayoutContentConfig {
/**
* Content class
*
* @default ''
*/
contentClass?: string;
/**
* Whether content is full the page
*
* If true, other elements will be hidden by `display: none`
*/
fullContent?: boolean;
}
/** Footer config */
export interface AdminLayoutFooterConfig {
/**
* Whether footer is visible
*
* @default true
*/
footerVisible?: boolean;
/**
* Whether footer is fixed
*
* @default true
*/
fixedFooter?: boolean;
/**
* Footer class
*
* @default ''
*/
footerClass?: string;
/**
* Footer height
*
* @default 48px
*/
footerHeight?: number;
/**
* Whether footer is on the right side
*
* When the layout is vertical, the footer is on the right side
*/
rightFooter?: boolean;
}
/**
* Layout mode
*
* - Horizontal
* - Vertical
*/
export type LayoutMode = 'horizontal' | 'vertical';
/**
* The scroll mode when content overflow
*
* - Wrapper: the layout component's wrapper element has a scrollbar
* - Content: the layout component's content element has a scrollbar
*
* @default 'wrapper'
*/
export type LayoutScrollMode = 'wrapper' | 'content';
/** Admin layout props */
export interface AdminLayoutProps
extends AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
AdminLayoutSiderConfig,
AdminLayoutContentConfig,
AdminLayoutFooterConfig {
/**
* Layout mode
*
* - {@link LayoutMode}
*/
mode?: LayoutMode;
/** Is mobile layout */
isMobile?: boolean;
/**
* Scroll mode
*
* - {@link ScrollMode}
*/
scrollMode?: LayoutScrollMode;
/**
* The id of the scroll element of the layout
*
* It can be used to get the corresponding Dom and scroll it
*
* @example
* use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*
* @default
* ```ts
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
* ```
*/
scrollElId?: string;
/** The class of the scroll element */
scrollElClass?: string;
/** The class of the scroll wrapper element */
scrollWrapperClass?: string;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/**
* Whether fix the header and tab
*
* @default true
*/
fixedTop?: boolean;
/**
* The max z-index of the layout
*
* The z-index of Header,Tab,Sider and Footer will not exceed this value
*/
maxZIndex?: number;
}
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
: S;
type Prefix = '--soy-';
export type LayoutCssVarsProps = Pick<
AdminLayoutProps,
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
> & {
headerZIndex?: number;
tabZIndex?: number;
siderZIndex?: number;
mobileSiderZIndex?: number;
footerZIndex?: number;
};
export type LayoutCssVars = {
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};
/**
* The mode of the tab
*
* - Button: button style
* - Chrome: chrome style
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome';
export interface PageTabProps {
/** Whether is dark mode */
darkMode?: boolean;
/**
* The mode of the tab
*
* - {@link TabMode}
*/
mode?: PageTabMode;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/** The class of the button tab */
buttonClass?: string;
/** The class of the chrome tab */
chromeClass?: string;
/** Whether the tab is active */
active?: boolean;
/** The color of the active tab */
activeColor?: string;
/**
* Whether the tab is closable
*
* Show the close icon when true
*/
closable?: boolean;
}
export type PageTabCssVarsProps = {
primaryColor: string;
primaryColor1: string;
primaryColor2: string;
primaryColorOpacity1: string;
primaryColorOpacity2: string;
primaryColorOpacity3: string;
};
export type PageTabCssVars = {
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@sa/uno-preset",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
}
}
+54
View File
@@ -0,0 +1,54 @@
// @unocss-include
import type { Preset } from '@unocss/core';
import type { Theme } from '@unocss/preset-uno';
export function presetSoybeanAdmin(): Preset<Theme> {
const preset: Preset<Theme> = {
name: 'preset-soybean-admin',
shortcuts: [
{
'flex-center': 'flex justify-center items-center',
'flex-x-center': 'flex justify-center',
'flex-y-center': 'flex items-center',
'flex-col': 'flex flex-col',
'flex-col-center': 'flex-center flex-col',
'flex-col-stretch': 'flex-col items-stretch',
'i-flex-center': 'inline-flex justify-center items-center',
'i-flex-x-center': 'inline-flex justify-center',
'i-flex-y-center': 'inline-flex items-center',
'i-flex-col': 'flex-col inline-flex',
'i-flex-col-stretch': 'i-flex-col items-stretch',
'flex-1-hidden': 'flex-1 overflow-hidden'
},
{
'absolute-lt': 'absolute left-0 top-0',
'absolute-lb': 'absolute left-0 bottom-0',
'absolute-rt': 'absolute right-0 top-0',
'absolute-rb': 'absolute right-0 bottom-0',
'absolute-tl': 'absolute-lt',
'absolute-tr': 'absolute-rt',
'absolute-bl': 'absolute-lb',
'absolute-br': 'absolute-rb',
'absolute-center': 'absolute-lt flex-center size-full',
'fixed-lt': 'fixed left-0 top-0',
'fixed-lb': 'fixed left-0 bottom-0',
'fixed-rt': 'fixed right-0 top-0',
'fixed-rb': 'fixed right-0 bottom-0',
'fixed-tl': 'fixed-lt',
'fixed-tr': 'fixed-rt',
'fixed-bl': 'fixed-lb',
'fixed-br': 'fixed-rb',
'fixed-center': 'fixed-lt flex-center size-full'
},
{
'nowrap-hidden': 'overflow-hidden whitespace-nowrap',
'ellipsis-text': 'nowrap-hidden text-ellipsis'
}
]
};
return preset;
}
export default presetSoybeanAdmin;
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@sa/utils",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"colord": "2.9.3",
"crypto-js": "4.2.0",
"localforage": "1.10.0",
"nanoid": "5.0.7"
},
"devDependencies": {
"@types/crypto-js": "4.2.2"
}
}
+252
View File
@@ -0,0 +1,252 @@
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import mixPlugin from 'colord/plugins/mix';
import type { AnyColor, HsvColor, RgbColor } from 'colord';
extend([namesPlugin, mixPlugin]);
/**
* Add color alpha
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
*/
export function addColorAlpha(color: string, alpha: number) {
return colord(color).alpha(alpha).toHex();
}
/**
* Mix color
*
* @param firstColor - First color
* @param secondColor - Second color
* @param ratio - The ratio of the second color (0 - 1)
*/
export function mixColor(firstColor: string, secondColor: string, ratio: number) {
return colord(firstColor).mix(secondColor, ratio).toHex();
}
/**
* Transform color with opacity to similar color without opacity
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
* @param bgColor Background color (usually white or black)
*/
export function transformColorWithOpacity(color: string, alpha: number, bgColor = '#ffffff') {
const originColor = addColorAlpha(color, alpha);
const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
function calRgb(or: number, bg: number, al: number) {
return bg + (or - bg) * al;
}
const resultRgb: RgbColor = {
r: calRgb(oR, bgR, alpha),
g: calRgb(oG, bgG, alpha),
b: calRgb(oB, bgB, alpha)
};
return colord(resultRgb).toHex();
}
/**
* Is white color
*
* @param color - Color
*/
export function isWhiteColor(color: string) {
return colord(color).isEqual('#ffffff');
}
/**
* Get rgb of color
*
* @param color Color
*/
export function getRgbOfColor(color: string) {
return colord(color).toRgb();
}
/** Hue step */
const hueStep = 2;
/** Saturation step, light color part */
const saturationStep = 16;
/** Saturation step, dark color part */
const saturationStep2 = 5;
/** Brightness step, light color part */
const brightnessStep1 = 5;
/** Brightness step, dark color part */
const brightnessStep2 = 15;
/** Light color count, main color up */
const lightColorCount = 5;
/** Dark color count, main color down */
const darkColorCount = 4;
/**
* The color index of color palette
*
* From left to right, the color is from light to dark, 6 is main color
*/
type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
/**
* Get color palette (from left to right, the color is from light to dark, 6 is main color)
*
* @param color - Color
* @param index - The color index of color palette (the main color index is 6)
* @returns Hex color
*/
export function getColorPalette(color: AnyColor, index: ColorIndex): string {
const transformColor = colord(color);
if (!transformColor.isValid()) {
throw new Error('invalid input color value');
}
if (index === 6) {
return colord(transformColor).toHex();
}
const isLight = index < 6;
const hsv = transformColor.toHsv();
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
const newHsv: HsvColor = {
h: getHue(hsv, i, isLight),
s: getSaturation(hsv, i, isLight),
v: getValue(hsv, i, isLight)
};
return colord(newHsv).toHex();
}
/** Map of dark color index and opacity */
const darkColorMap = [
{ index: 7, opacity: 0.15 },
{ index: 6, opacity: 0.25 },
{ index: 5, opacity: 0.3 },
{ index: 5, opacity: 0.45 },
{ index: 5, opacity: 0.65 },
{ index: 5, opacity: 0.85 },
{ index: 4, opacity: 0.9 },
{ index: 3, opacity: 0.95 },
{ index: 2, opacity: 0.97 },
{ index: 1, opacity: 0.98 }
];
/**
* Get color palettes
*
* @param color - Color
* @param darkTheme - Dark theme
* @param darkThemeMixColor - Dark theme mix color (default: #141414)
*/
export function getColorPalettes(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const patterns = indexes.map(index => getColorPalette(color, index));
if (darkTheme) {
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
const darkColor = colord(darkThemeMixColor).mix(patterns[index], opacity);
return darkColor;
});
return darkPatterns.map(item => colord(item).toHex());
}
return patterns;
}
/**
* Get hue
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
let hue: number;
const hsvH = Math.round(hsv.h);
if (hsvH >= 60 && hsvH <= 240) {
hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
} else {
hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
}
if (hue < 0) {
hue += 360;
}
if (hue >= 360) {
hue -= 360;
}
return hue;
}
/**
* Get saturation
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
let saturation: number;
if (isLight) {
saturation = hsv.s - saturationStep * i;
} else if (i === darkColorCount) {
saturation = hsv.s + saturationStep;
} else {
saturation = hsv.s + saturationStep2 * i;
}
if (saturation > 100) {
saturation = 100;
}
if (isLight && i === lightColorCount && saturation > 10) {
saturation = 10;
}
if (saturation < 6) {
saturation = 6;
}
return saturation;
}
/**
* Get value of hsv
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
let value: number;
if (isLight) {
value = hsv.v + brightnessStep1 * i;
} else {
value = hsv.v - brightnessStep2 * i;
}
if (value > 100) {
value = 100;
}
return value;
}
+27
View File
@@ -0,0 +1,27 @@
import CryptoJS from 'crypto-js';
export class Crypto<T extends object> {
/** Secret */
secret: string;
constructor(secret: string) {
this.secret = secret;
}
encrypt(data: T): string {
const dataString = JSON.stringify(data);
const encrypted = CryptoJS.AES.encrypt(dataString, this.secret);
return encrypted.toString();
}
decrypt(encrypted: string) {
const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
const dataString = decrypted.toString(CryptoJS.enc.Utf8);
try {
return JSON.parse(dataString) as T;
} catch {
// avoid parse error
return null;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './color';
export * from './crypto';
export * from './storage';
export * from './nanoid';
+3
View File
@@ -0,0 +1,3 @@
import { nanoid } from 'nanoid';
export { nanoid };
+204
View File
@@ -0,0 +1,204 @@
import localforage from 'localforage';
/** The storage type */
export type StorageType = 'local' | 'session';
// 内存存储的备用实现
class MemoryStorage {
private static instance: MemoryStorage;
private storage = new Map<string, string>();
static getInstance() {
if (!MemoryStorage.instance) {
MemoryStorage.instance = new MemoryStorage();
}
return MemoryStorage.instance;
}
setItem(key: string, value: string) {
this.storage.set(key, value);
}
getItem(key: string) {
return this.storage.get(key) || null;
}
removeItem(key: string) {
this.storage.delete(key);
}
clear() {
this.storage.clear();
}
}
// 全局备用存储实例
const memoryStorage = MemoryStorage.getInstance();
export function createStorage<T extends object>(type: StorageType) {
// 尝试获取存储实例,如果失败则使用内存存储
let stg: Storage | MemoryStorage;
try {
stg = type === 'session' ? window.sessionStorage : window.localStorage;
// 测试存储是否可用
stg.setItem('__storage_test__', '');
stg.removeItem('__storage_test__');
} catch (e) {
console.warn(`${type} storage is not available, using memory storage`);
stg = memoryStorage;
}
const storage = {
set<K extends keyof T>(key: K, value: T[K]) {
try {
const json = JSON.stringify(value);
stg.setItem(key as string, json);
} catch (e) {
console.warn(`Storage set error, falling back to memory storage`);
memoryStorage.setItem(key as string, JSON.stringify(value));
}
},
get<K extends keyof T>(key: K): T[K] | null {
try {
const json = stg.getItem(key as string);
if (!json) return null;
return JSON.parse(json);
} catch (e) {
console.warn(`Storage get error, trying memory storage`);
try {
const memoryValue = memoryStorage.getItem(key as string);
return memoryValue ? JSON.parse(memoryValue) : null;
} catch {
return null;
}
}
},
remove(key: keyof T) {
try {
stg.removeItem(key as string);
memoryStorage.removeItem(key as string);
} catch (e) {
console.warn(`Storage remove error`);
}
},
clear() {
try {
stg.clear();
memoryStorage.clear();
} catch (e) {
console.warn(`Storage clear error`);
}
}
};
return storage;
}
type LocalforageDriver = 'local' | 'indexedDB' | 'webSQL';
export function createLocalforage<T extends object>(driver: LocalforageDriver) {
const driverMap: Record<LocalforageDriver, string> = {
local: localforage.LOCALSTORAGE,
indexedDB: localforage.INDEXEDDB,
webSQL: localforage.WEBSQL
};
// 创建一个内存存储的备用实现
const memoryStore = new Map<string, any>();
// 创建备用存储对象
const fallbackStorage = {
getItem<K extends keyof T>(key: K): Promise<T[K] | null> {
return Promise.resolve(memoryStore.get(key as string) || null);
},
setItem<K extends keyof T>(key: K, value: T[K]): Promise<T[K]> {
memoryStore.set(key as string, value);
return Promise.resolve(value);
},
removeItem(key: keyof T): Promise<void> {
memoryStore.delete(key as string);
return Promise.resolve();
},
clear(): Promise<void> {
memoryStore.clear();
return Promise.resolve();
},
length(): Promise<number> {
return Promise.resolve(memoryStore.size);
},
key(keyIndex: number): Promise<string> {
return Promise.resolve(Array.from(memoryStore.keys())[keyIndex]);
},
keys(): Promise<string[]> {
return Promise.resolve(Array.from(memoryStore.keys()));
}
};
try {
// 配置 localforage
localforage.config({
driver: [
driverMap[driver],
localforage.LOCALSTORAGE,
localforage.INDEXEDDB,
localforage.WEBSQL
].filter(Boolean),
name: 'appStorage',
version: 1.0,
storeName: 'keyvaluepairs'
});
// 创建包装的存储对象
const storage = {
getItem<K extends keyof T>(key: K): Promise<T[K] | null> {
return localforage.getItem<T[K]>(key as string)
.catch(() => fallbackStorage.getItem(key));
},
setItem<K extends keyof T>(key: K, value: T[K]): Promise<T[K]> {
return localforage.setItem<T[K]>(key as string, value)
.catch(() => fallbackStorage.setItem(key, value));
},
removeItem(key: keyof T): Promise<void> {
return localforage.removeItem(key as string)
.catch(() => fallbackStorage.removeItem(key));
},
clear(): Promise<void> {
return localforage.clear()
.catch(() => fallbackStorage.clear());
},
length(): Promise<number> {
return localforage.length()
.catch(() => fallbackStorage.length());
},
key(keyIndex: number): Promise<string> {
return localforage.key(keyIndex)
.catch(() => fallbackStorage.key(keyIndex));
},
keys(): Promise<string[]> {
return localforage.keys()
.catch(() => fallbackStorage.keys());
}
};
return storage as LocalForage<T>;
} catch (e) {
console.warn('Localforage initialization failed, using memory storage', e);
return fallbackStorage as LocalForage<T>;
}
}
// 更新类型定义
interface LocalForage<T extends object> {
getItem<K extends keyof T>(key: K): Promise<T[K] | null>;
setItem<K extends keyof T>(key: K, value: T[K]): Promise<T[K]>;
removeItem(key: keyof T): Promise<void>;
clear(): Promise<void>;
length(): Promise<number>;
key(keyIndex: number): Promise<string>;
keys(): Promise<string[]>;
}
+8553
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
packages:
- "packages/*"
+10
View File
@@ -0,0 +1,10 @@
<?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>

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'
}
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ConfigProvider, StyleProvider } from 'ant-design-vue';
import { legacyLogicalPropertiesTransformer } from 'ant-design-vue';
import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme';
import { antdLocales } from './locales/antd';
defineOptions({
name: 'App'
});
const appStore = useAppStore();
const themeStore = useThemeStore();
const antdLocale = computed(() => {
appStore.changeLocale(appStore.locale);
return antdLocales[appStore.locale];
});
</script>
<template>
<ConfigProvider :theme="themeStore.antdTheme" :locale="antdLocale">
<StyleProvider
hash-priority="high"
:transformers="[legacyLogicalPropertiesTransformer]"
>
<AppProvider>
<RouterView class="bg-layout" />
</AppProvider>
</StyleProvider>
</ConfigProvider>
</template>
<style scoped></style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>

After

Width:  |  Height:  |  Size: 202 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="prefix__prefix__feather prefix__prefix__feather-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 006 0v-1a10 10 0 10-3.92 7.94"/></svg>

After

Width:  |  Height:  |  Size: 315 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.49 134.49"><path d="M134.49 67.25A67.25 67.25 0 1167.25 0a67.23 67.23 0 0167.24 67.25" fill="#f3f3f2"/><path fill="#f1caae" d="M64.47 103.16zm2.4-13.01c8.11-6.21 11-10.25 12.24-11a17.94 17.94 0 01-.69-4.23c-4 3.34-5.73 4.49-11 4.31-5.69.19-7.28-1.15-12-5.14l-.07-.09a19 19 0 01-.72 5.14c1.23.75 4.13 4.79 12.24 11.01z"/><path d="M61.56 121.66c.31-2 .61-4 .9-5.91v-.11c.93-6 1.7-10.86 2-12.48a1.17 1.17 0 01-.36-.34.6.6 0 01-.09-.13c-.16-.36-.32-.69-.48-1 0-.06-.07-.13-.1-.2a16.73 16.73 0 01-2-4.43c0-.15 0-.3-.07-.46a4 4 0 00-1 2.65c.33 3.56-10.83-9.56-10.65-14.6a38.55 38.55 0 01-4 1.9 219.52 219.52 0 0015.85 35.11zm-37.93-27.6h-.09.05s.03.01.04 0zM73.3 99.2a4 4 0 00-1-2.64c0 .16 0 .31-.06.46A7.17 7.17 0 0172 98a27.91 27.91 0 01-1.71 3.52l-.11.21c-.16.31-.32.65-.48 1a1 1 0 01-.09.13 1.1 1.1 0 01-.37.34c.29 1.6 1.06 6.35 2 12.32a2.34 2.34 0 000 .26c.23 1.54.48 3.15.73 4.81a208.85 208.85 0 0015.5-34.21A37.69 37.69 0 0184 84.7c0 5.12-11 18.03-10.7 14.5zm36.4-5.09a5.22 5.22 0 01.63.11l-.73-.28s.08.06.1.17z" fill="#9d9d9d"/><path d="M23.6 94.09h-.05a4.67 4.67 0 00-.84.15c-3 .81-5.08 8.11-6.94 16.26a67.51 67.51 0 0016.1 13.93 33.36 33.36 0 00-.42-4.63c-1.13-6.25-8.79-24.87-7.85-25.71zm94.03 17.67c-2-8.62-4-16.67-7.21-17.52h-.09a5.22 5.22 0 00-.63-.11c.39 1.91-6.82 19.65-7.91 25.69a34.76 34.76 0 00-.44 5.4" fill="#959494"/><path d="M71.29 119.06h-1.65v-3.73h1.58v.15c-.92-6-1.69-10.72-2-12.32l-.1.07a1.29 1.29 0 01-.28.08v.09H68l-.89.06h-.56l-.88-.06h-.86v-.09a1.2 1.2 0 01-.27-.08l-.11-.07c-.29 1.62-1.06 6.44-2 12.48v-.31h1.59v3.73h-1.56v-3.31c-.29 1.87-.59 3.86-.9 5.91 0-.34.1-.69.15-1v1.28l.75 1.25v-2.62h1.65v3.73h-1c.57.91 1.14 1.78 1.7 2.58v-1.09h1.69v2.82c.25-.29.5-.6.75-.91v-1.91h1.42c.34-.48.68-1 1-1.49v-3.73h1.65v1.22l.73-1.22c-.25-1.66-.5-3.27-.73-4.81zm-1.65-13.74c.15.91.33 2 .53 3.3h-.53zm0 4.79h.77c.18 1.16.38 2.4.58 3.73h-1.35zm-3.14-5.22v3.73h-1.65v-3.73zm-2.39 9h-1.36c.21-1.33.4-2.57.59-3.73h.77zm0-5.22h-.54l.54-3.31zm2.39 15.66h-1.65v-3.73h1.65zm0-5.22h-1.65v-3.73h1.65zm0-5.22h-1.65v-3.73h1.65zm2.39 10.44h-1.64v-3.73h1.64zm0-5.22h-1.64v-3.73h1.64zm0-5.22h-1.64v-3.73h1.64zm0-5.22h-1.64v-3.73h1.64z" fill="#f0f0f0"/><path d="M63.53 101.69zm-2.12-5.14zm10.86.51A7.17 7.17 0 0172 98a7.17 7.17 0 00.27-.94zm.07-.5zm-2.13 5.14zm-5.63 1.53a1.2 1.2 0 00.27.08v-3.64h1.65v3.73h-.79l.88.06h.56l.89-.06h-.79v-3.73h1.64v3.64a1.29 1.29 0 00.28-.08l.1-.07a1.1 1.1 0 00.37-.34v-3.15h1.64c-.29.57-.62 1.16-1 1.82A27.91 27.91 0 0072 98v-1.85c-.23-.29-.48-.57-.74-.86v2.89h-1.62v-3.73h.82c-.55-.52-1.11-1-1.63-1.49h-1.58v-1.5a3.56 3.56 0 01-.38-.47 2.79 2.79 0 01-.37.47V93h-1.59c-.52.47-1.08 1-1.62 1.49h.82v3.73h-1.65v-2.93c-.27.28-.52.57-.75.86V98a7.13 7.13 0 01-.23-.91 16.73 16.73 0 002 4.43c-.34-.65-.67-1.25-1-1.82h1.64v3.15a1.17 1.17 0 00.36.34zm2.67-8.78h1.64v3.73h-1.64zm-2.4 0h1.65v3.73h-1.65z" fill="#fff"/><path fill="#040000" d="M66.5 99.67h-1.65v3.73h1.65v-3.73zm-1.65 5.22h1.65v3.73h-1.65zm0-10.44h1.65v3.73h-1.65zm0 15.66h1.65v3.73h-1.65zm0 15.66v1.09c.49.7 1 1.35 1.45 2l.2-.23v-2.82zm0-5.22h1.65v3.73h-1.65zm0-5.22h1.65v3.73h-1.65zm-3.29 6.33l.15.26v-1.28c-.05.36-.1.68-.15 1.02zm.9-23.48h1.65v-3.73h-.82c-.29.28-.56.56-.83.84zm-.75-2.03l-.3.4c0 .16 0 .31.07.46a7.13 7.13 0 00.23.91zm2.4 3.52h-1.64c.29.57.62 1.17 1 1.82 0 .07.07.14.1.2.16.32.32.65.48 1a.6.6 0 00.09.13zm0 20.88h-1.65v2.62q.34.57.69 1.11h1zm0-1.49v-3.73h-1.59v3.73zm0-10.44v-3.31l-.54 3.31zm-1.36 5.22h1.36v-3.73h-.77c-.19 1.16-.34 2.4-.59 3.73zm6.89 6.71v3.73c.53-.8 1.06-1.63 1.6-2.51v-1.22zm0-5.22v3.73h1.65v-3.32a2.34 2.34 0 010-.26v-.15zM66.5 91.46c-.4.43-1 .94-1.59 1.5h1.59zM68.83 93c-.63-.56-1.19-1.07-1.58-1.5V93zm-1.58 17.11h1.65v3.73h-1.65zm0 10.44h1.65v3.73h-1.65zm0-5.22h1.65v3.73h-1.65zm1.64-15.66h-1.64v3.73h1.64v-3.73zm-1.64 5.22h1.65v3.73h-1.65zm0 22.79c.47-.6.94-1.23 1.42-1.91h-1.42zm0-33.23h1.65v3.73h-1.65zm2.39 10.87v3.3h.53c-.17-1.27-.38-2.39-.53-3.3zm0-5.65v3.15a1 1 0 00.09-.13c.16-.36.32-.68.48-1l.11-.21c.34-.66.67-1.25 1-1.82zM72 98a7.17 7.17 0 00.24-.91c0-.15 0-.3.06-.46-.1-.13-.2-.27-.31-.4V98zm-2.36.18h1.65v-2.89l-.83-.84h-.82zm.77 11.93h-.77v3.73H71c-.21-1.33-.41-2.57-.59-3.73z"/><path d="M60.45 99.2a4 4 0 011-2.65l.3-.4c.23-.29.48-.58.75-.86s.54-.56.83-.84c.54-.52 1.1-1 1.62-1.49s1.19-1.07 1.59-1.5a2.79 2.79 0 00.37-.47 3.56 3.56 0 00.38.47c.39.43 1 .94 1.58 1.5s1.08 1 1.63 1.49l.83.84c.26.29.51.57.74.86.11.13.21.27.31.4a4 4 0 011 2.64C73 102.73 84 89.82 84 84.7v-.36c-.09-.72-2.2-1.82-4.15-5.11-.15-.26-.36-.27-.68-.08-1.23.75-4.13 4.79-12.24 11-8.11-6.22-11-10.26-12.24-11-.31-.19-.53-.18-.68.08-2 3.29-4.05 4.39-4.14 5.11a2.17 2.17 0 000 .26c-.25 5.04 10.91 18.16 10.58 14.6z" fill="#838384"/><path d="M62.49 132.11a13.73 13.73 0 003.81-3.29c-.47-.61-1-1.26-1.45-2s-1.13-1.67-1.7-2.58q-.34-.54-.69-1.11l-.75-1.25-.15-.26A219.52 219.52 0 0145.81 86.5c-.49-1.25-21 7-22.17 7.56-.94.84 6.72 19.46 7.85 25.71a33.36 33.36 0 01.42 4.63A66.72 66.72 0 0062 134.27c.15-1.27.3-2.06.49-2.16z" fill="#28Line truncated

After

Width:  |  Height:  |  Size: 6.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#ebebeb" d="M0 382.4h500v.25H0zM416.78 398.49h33.12v.25h-33.12zM322.53 401.21h8.69v.25h-8.69zM396.59 389.21h19.19v.25h-19.19zM52.46 390.89h43.19v.25H52.46zM104.56 390.89h6.33v.25h-6.33zM131.47 395.11h93.68v.25h-93.68zM237 337.8H43.91a5.71 5.71 0 01-5.7-5.71V60.66a5.71 5.71 0 015.7-5.66H237a5.71 5.71 0 015.71 5.71v271.38a5.71 5.71 0 01-5.71 5.71zM43.91 55.2a5.46 5.46 0 00-5.45 5.46v271.43a5.46 5.46 0 005.45 5.46H237a5.47 5.47 0 005.46-5.46V60.66A5.47 5.47 0 00237 55.2zM453.31 337.8h-193.1a5.72 5.72 0 01-5.71-5.71V60.66a5.72 5.72 0 015.71-5.66h193.1a5.71 5.71 0 015.69 5.66v271.43a5.71 5.71 0 01-5.69 5.71zM260.21 55.2a5.47 5.47 0 00-5.46 5.46v271.43a5.47 5.47 0 005.46 5.46h193.1a5.47 5.47 0 005.46-5.46V60.66a5.47 5.47 0 00-5.46-5.46z"/><path fill="#e6e6e6" d="M409.9 275.41H301.09V78.79H409.9z"/><path fill="#f0f0f0" d="M407.56 275.41H296.08V78.79h111.48z"/><path fill="#e6e6e6" d="M409.9 280.07H301.09v-4.66H409.9z"/><path fill="#f0f0f0" d="M405.75 280.07H294.27v-4.66h111.48z"/><path fill="#fafafa" d="M400.52 85.82v182.56H303.1V85.82z"/><path fill="#fff" d="M330.66 268.38l27.42-182.56h19.48l-27.43 182.56h-19.47z"/><path fill="#fff" d="M333.75 268.38l27.43-182.56h19.47l-27.43 182.56h-19.47zM354.31 268.38l27.43-182.56h7.59l-27.42 182.56h-7.6z"/><path fill="#e6e6e6" d="M304 85.82v182.56h-.89V85.82z"/><path fill="#ebebeb" opacity=".6" d="M294.41 91.12h108.81l.64-2.57h-108.8l-.65 2.57zM294.41 95.32h108.81l.64-2.56h-108.8l-.65 2.56zM294.41 99.53h108.81l.64-2.56h-108.8l-.65 2.56zM294.41 103.74h108.81l.64-2.57h-108.8l-.65 2.57zM294.41 107.95h108.81l.64-2.57h-108.8l-.65 2.57zM294.41 112.15h108.81l.64-2.56h-108.8l-.65 2.56z"/><path fill="#f0f0f0" d="M274.53 67h156.92v3.79H274.53z"/><path fill="#fafafa" opacity=".8" d="M420.36 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M420.36 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M412.11 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M412.11 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M403.86 63.81h8.25v242.84h-8.25zM292.84 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M292.84 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M284.6 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M284.6 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M276.35 63.81h8.25v242.84h-8.25z"/><path fill="#e6e6e6" d="M194.15 275.41H85.34V78.79h108.81z"/><path fill="#f0f0f0" d="M191.8 275.41H80.32V78.79H191.8z"/><path fill="#e6e6e6" d="M194.15 280.07H85.34v-4.66h108.81z"/><path fill="#f0f0f0" d="M189.99 280.07H78.51v-4.66h111.48z"/><path fill="#fafafa" d="M184.77 85.82v182.56H87.35V85.82z"/><path fill="#fff" d="M114.91 268.38l27.42-182.56h19.48l-27.43 182.56h-19.47z"/><path fill="#fff" d="M118 268.38l27.42-182.56h19.48l-27.43 182.56H118zM138.56 268.38l27.43-182.56h7.59l-27.42 182.56h-7.6z"/><path fill="#e6e6e6" d="M88.25 85.81v182.56h-.89V85.81z"/><path fill="#ebebeb" opacity=".6" d="M78.66 91.12h108.81l.64-2.57H79.31l-.65 2.57zM78.66 95.32h108.81l.64-2.56H79.31l-.65 2.56zM78.66 99.53h108.81l.64-2.56H79.31l-.65 2.56zM78.66 103.74h108.81l.64-2.57H79.31l-.65 2.57zM78.66 107.95h108.81l.64-2.57H79.31l-.65 2.57zM78.66 112.15h108.81l.64-2.56H79.31l-.65 2.56z"/><path fill="#f0f0f0" d="M58.78 67H215.7v3.79H58.78z"/><path fill="#fafafa" opacity=".8" d="M204.61 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M204.61 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M196.36 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M196.36 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M188.11 63.81h8.25v242.84h-8.25zM77.09 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M77.09 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M68.84 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" opacity=".8" d="M68.84 63.81h2.14v242.84h-2.14z"/><path fill="#fafafa" opacity=".8" d="M60.59 63.81h8.25v242.84h-8.25z"/><path fill="#f0f0f0" d="M392.58 375.61h-16.81v-84h16.81z"/><path fill="#f0f0f0" d="M386.44 382.4H243.98v-6.79h142.46z"/><path fill="#f5f5f5" d="M237.85 291.61h137.92v84H237.85z"/><path fill="#f0f0f0" d="M250.41 300.51H363.2v27.94H250.41zM250.41 334.85H363.2v27.94H250.41z"/><path d="M293.44 304.36h26.73a2.41 2.41 0 002.41-2.41 2.42 2.42 0 00-2.41-2.41h-26.73A2.42 2.42 0 00291 302a2.41 2.41 0 002.44 2.36zM293.44 338.71h26.73a2.42 2.42 0 002.41-2.41 2.41 2.41 0 00-2.41-2.41h-26.73a2.41 2.41 0 00-2.44 2.41 2.42 2.42 0 002.44 2.41z" fill="#f5f5f5"/><path fill="#f0f0f0" d="M375.76 291.62H233.58v-3.28h142.18z"/><path fill="#e6e6e6" d="M375.76 288.33h21.28v3.28h-21.28z"/><path d="M108.34 376.83a7.25 7.25 0 01-7.23-7.23v-70.76a7.25 7.25 0 017.23-7.23 7.24 7.24 0 017.22 7.23v70.76a7.24 7.24 0 01-7.22 7.23z" fill="#f5f5f5"/><path d="M112.47 374a4.9 4.9 0 01-4.88-4.88v-69.78a4.9 4.9 0 014.88-4.88 4.9 4.9 0 014.89 4.88v69.76a4.9 4.9 0 01-4.89 4.Line truncated

After

Width:  |  Height:  |  Size: 19 KiB

Loaded 100 of 330 files, more files were not shown because too many files have changed in this diff. Show more