22 Commits

Author SHA1 Message Date
206b92c2a2 release: v3.4.1 2024-12-08 20:11:02 +08:00
7c509fa737 refactor: 优化路由守卫代码(同步 GiDemo 更新) 2024-12-08 19:56:35 +08:00
abacb267aa feat: 面包屑新增过渡动画效果(同步 GiDemo 更新) 2024-12-08 19:47:12 +08:00
KAI
43dd512b8a style: 菜单树地三层横向布局 2024-12-08 10:41:13 +00:00
51a2168822 refactor: 优化登录验证码开关代码 2024-12-05 19:26:23 +08:00
Gyq灬明
4cd892e288 feat: 新增验证码配置开关 2024-12-05 07:54:05 +00:00
KAI
3f871e102a !36 fix: 修复用户选择器超级管理员回显异常的问题
Merge pull request !36 from KAI/dev
2024-11-29 05:23:12 +00:00
21913350e7 chore: 调整关于项目路由结构 2024-11-23 23:28:46 +08:00
c2463fc450 feat: GiForm 支持 label 自定义渲染,以及插槽自定义渲染(同步 GiDemo 更新) 2024-11-23 23:05:30 +08:00
99f8edb729 refactor: 调整 eslint.config.js 2024-11-23 22:58:07 +08:00
7fe3ffe9da refactor: 彻底删除 useForm 组合函数 2024-11-23 22:57:44 +08:00
7fa42975cf chore: 新增关于项目菜单(该菜单从动态路由调整为静态,且不再需要鉴权) 2024-11-23 22:47:59 +08:00
b82ca81b79 style: 优化系统日志、系统配置标签样式 2024-11-23 21:33:58 +08:00
7402de5695 refactor: 优化搜索输入框 input => input-search 2024-11-23 21:27:29 +08:00
b030921189 feat(tabs): 标签页新增重新加载、关闭左侧操作 2024-11-23 21:17:22 +08:00
6c45483fae refactor: useForm => useResetReactive(同步 GiDemo 更新) 2024-11-23 21:04:48 +08:00
秋帆
be4356fa04 refactor: 行为验证码重复问题 2024-11-22 21:29:52 +08:00
930227ea0c refactor(system/config): 重构系统配置页面 2024-11-21 22:52:24 +08:00
61ef692c83 revert: 移除部分异步组件加载 2024-11-21 22:48:47 +08:00
e8941adde4 refactor: 拆分并调整路由守卫,优化顶部进度条展示 2024-11-21 22:47:54 +08:00
246d638a8f fix: 修复快捷操作代码生成链接错误 2024-11-20 21:28:36 +08:00
GXSZone
068d959d03 fix(GiCell): 修复 GiCellTags 组件的空数据问题
- 在渲染标签之前增加对 data 的非空检查,避免潜在的运行时错误
- 优化了组件的健壮性,确保在 data 为 null 或 undefined时不会抛出错误
2024-11-19 14:38:45 +08:00
95 changed files with 1160 additions and 616 deletions

View File

@@ -1,3 +1,32 @@
## [v3.4.1](https://github.com/continew-org/continew-admin-ui/compare/v3.4.0...v3.4.1) (2024-12-08)
### ✨ 新特性
* 标签页新增重新加载、关闭左侧操作 ([b030921](https://github.com/continew-org/continew-admin-ui/commit/b030921189e9093f07369cebebdfa8b12b3fb153))
* 新增关于项目菜单(该菜单从动态路由调整为静态,且不再需要鉴权) ([7fa4297](https://github.com/continew-org/continew-admin-ui/commit/7fa42975cfa32e1fb8eeca26e3a06be2e10d2aa3)) ([2191335](https://github.com/continew-org/continew-admin-ui/commit/21913350e7d8dfd0a06464efcf27d2d234270ab0))
* GiForm 支持 label 自定义渲染,以及插槽自定义渲染(同步 GiDemo 更新) ([c2463fc](https://github.com/continew-org/continew-admin-ui/commit/c2463fc4502acbd9274f1080f86a74ca43951927))
* 新增验证码配置开关 ([4cd892e](https://github.com/continew-org/continew-admin-ui/commit/4cd892e288c08b04f038bf6034c14ec022c0e919)) ([51a2168](https://github.com/continew-org/continew-admin-ui/commit/51a21688223346877f00f5142e277682e5774158)) (Gitee#37@@aiming317)
* 面包屑新增过渡动画效果(同步 GiDemo 更新) ([abacb26](https://github.com/continew-org/continew-admin-ui/commit/abacb267aaf96516480255f509b07b32d44abd27))
### 💎 功能优化
- 拆分并调整路由守卫,优化顶部进度条展示 ([e8941ad](https://github.com/continew-org/continew-admin-ui/commit/e8941adde4c5156bbe7f2d95f013add353aee61b))
- 移除部分异步组件加载 ([61ef692](https://github.com/continew-org/continew-admin-ui/commit/61ef692c8398b4f352f52f11a82d64dd9f7fa8e3))
- 重构系统配置页面 ([930227e](https://github.com/continew-org/continew-admin-ui/commit/930227ea0cc6f17545841a5548a91202fa0bc2a1))
- useForm => useResetReactive同步 GiDemo 更新) ([6c45483](https://github.com/continew-org/continew-admin-ui/commit/6c45483fae53677c57b9dc0c6a1e4c42b659d151)) ([7fe3ffe](https://github.com/continew-org/continew-admin-ui/commit/7fe3ffe9dab318d744d2dd8d7d1e793efdbc97d1))
- 优化搜索输入框 input => input-search ([7402de5](https://github.com/continew-org/continew-admin-ui/commit/7402de5695140b5d4a6228fd37ef23c793c8e5e7))
- 优化系统日志、系统配置标签样式 ([b82ca81](https://github.com/continew-org/continew-admin-ui/commit/b82ca81b79b56bfa728b7c467d151724b43792b2))
- 调整 eslint.config.js ([99f8edb](https://github.com/continew-org/continew-admin-ui/commit/99f8edb7295f913e36cd28c41ac4a6b536c982d9))
- 角色功能权限第三级扁平化处理 ([43dd512](https://github.com/continew-org/continew-admin-ui/commit/43dd512b8a359d794a2ad48dd4e05c22f7223391)) (Gitee#38@kiki1373639299)
- 优化路由守卫代码(同步 GiDemo 更新) ([7c509fa](https://github.com/continew-org/continew-admin-ui/commit/7c509fa7372de5bf60895bc5e5b66cc6355c8d97))
### 🐛 问题修复
- 修复 GiCellTags 组件的空数据问题 ([068d959](https://github.com/continew-org/continew-admin-ui/commit/068d959d0380f85053d6f001621990309c904519)) (Gitee#35@CoderZone)
- 修复快捷操作代码生成链接错误 ([246d638](https://github.com/continew-org/continew-admin-ui/commit/246d638a8f66bd5a98091bd12cc78f4a2083dd04))
- 修复行为验证码接口重复请求问题 ([be4356f](https://github.com/continew-org/continew-admin-ui/commit/be4356fa041108c46eade7e1f81897346338026b))
- 修复用户选择器超级管理员回显异常的问题 ([3f871e1](https://github.com/continew-org/continew-admin-ui/commit/3f871e102acee6481bfe3fb095279063713fe6e5)) (Gitee#36@kiki1373639299)
## [v3.4.0](https://github.com/continew-org/continew-admin-ui/compare/v3.3.0...v3.4.0) (2024-11-18)
### ✨ 新特性

View File

@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License" />
</a>
<a href="https://github.com/Charles7c/continew-admin-ui" target="_blank">
<img src="https://img.shields.io/badge/RELEASE-v3.4.0-%23ff3f59.svg" alt="Release" />
<img src="https://img.shields.io/badge/RELEASE-v3.4.1-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://github.com/Charles7c/continew-admin" target="_blank">
<img src="https://img.shields.io/github/stars/Charles7c/continew-admin?style=social" alt="GitHub stars" />

View File

@@ -11,7 +11,7 @@ export default function appInfo(): Plugin {
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`${bgGreen('ContiNew Admin v3.4.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/faq.html')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
`${bold(green(`${bgGreen('ContiNew Admin v3.4.1')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/faq.html')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
{
padding: 1,
margin: 1,

View File

@@ -8,6 +8,10 @@ export default antfu(
'vue/block-order': ['error', {
order: [['script', 'template'], 'style'],
}], // 强制组件顶级元素的顺序
'vue/define-macros-order': ['error', {
order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'],
defineExposeLast: true,
}], // 强制执行定义限制和定义弹出编译器宏的顺序
'vue/singleline-html-element-content-newline': 'off', // 要求在单行元素的内容前后换行
'vue/html-self-closing': ['off', {
html: {

View File

@@ -1,9 +1,10 @@
{
"name": "continew-admin-ui",
"type": "module",
"version": "3.4.0",
"version": "3.4.1",
"private": "true",
"scripts": {
"bootstrap": "pnpm install --registry=https://registry.npmmirror.com",
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"build:test": "vue-tsc --noEmit && vite build --mode test",

View File

@@ -3,6 +3,7 @@ export interface ImageCaptchaResp {
uuid: string
img: string
expireTime: number
isEnabled: boolean
}
/** 仪表盘公告类型 */

View File

@@ -293,6 +293,18 @@ export interface SiteConfig {
SITE_BEIAN: OptionResp
}
/** 安全配置类型 */
export interface SecurityConfig {
PASSWORD_ERROR_LOCK_COUNT: OptionResp
PASSWORD_ERROR_LOCK_MINUTES: OptionResp
PASSWORD_EXPIRATION_DAYS: OptionResp
PASSWORD_EXPIRATION_WARNING_DAYS: OptionResp
PASSWORD_REPETITION_TIMES: OptionResp
PASSWORD_MIN_LENGTH: OptionResp
PASSWORD_ALLOW_CONTAIN_USERNAME: OptionResp
PASSWORD_REQUIRE_SYMBOLS: OptionResp
}
/** 邮箱配置类型 */
export interface MailConfig {
MAIL_PROTOCOL: OptionResp
@@ -304,16 +316,9 @@ export interface MailConfig {
MAIL_SSL_PORT: OptionResp
}
/** 安全配置类型 */
export interface SecurityConfig {
PASSWORD_ERROR_LOCK_COUNT: OptionResp
PASSWORD_ERROR_LOCK_MINUTES: OptionResp
PASSWORD_EXPIRATION_DAYS: OptionResp
PASSWORD_EXPIRATION_WARNING_DAYS: OptionResp
PASSWORD_REPETITION_TIMES: OptionResp
PASSWORD_MIN_LENGTH: OptionResp
PASSWORD_ALLOW_CONTAIN_USERNAME: OptionResp
PASSWORD_REQUIRE_SYMBOLS: OptionResp
/** 登录配置类型 */
export interface LoginConfig {
LOGIN_CAPTCHA_ENABLED: OptionResp
}
/** 绑定三方账号信息 */

View File

@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 48 48" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 15a1 1 0 001 1h2a1 1 0 001-1V8h7a1 1 0 001-1V5a1 1 0 00-1-1H6a2 2 0 00-2 2v9zm4 18a1 1 0 00-1-1H5a1 1 0 00-1 1v9a2 2 0 002 2h9a1 1 0 001-1v-2a1 1 0 00-1-1H8v-7zm35-17a1 1 0 001-1V6a2 2 0 00-2-2h-9a1 1 0 00-1 1v2a1 1 0 001 1h7v7a1 1 0 001 1h2zm1 17a1 1 0 00-1-1h-2a1 1 0 00-1 1v7h-7a1 1 0 00-1 1v2a1 1 0 001 1h9a2 2 0 002-2v-9zM32.835 11h-6.108c-6.512 0-11.882 4.804-12.636 11h-1.992c-.382 0-.52.046-.66.134a.855.855 0 00-.325.378c-.074.162-.114.324-.114.77v1.436c0 .446.04.608.114.77.075.163.185.291.324.378.14.088.279.134.66.134h2.157c1.179 5.706 6.315 10 12.472 10h6.108c.405 0 .552-.041.7-.12a.819.819 0 00.344-.337c.079-.145.121-.29.121-.688V31h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-7h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-3.855c0-.398-.042-.543-.121-.688a.819.819 0 00-.344-.338c-.148-.078-.295-.119-.7-.119zm-2.744 3.571h-3.637c-5.02 0-9.09 3.998-9.09 8.929s4.07 8.929 9.09 8.929h3.637V14.57z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<svg width="48" height="48" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.72938 13.2709C1.31587 11.8234 1.31587 9.51246 2.72938 8.06503L7.9367 2.73274C9.39842 1.23594 11.8058 1.23594 13.2675 2.73274C14.681 4.18017 14.681 6.49116 13.2675 7.93859L8.06017 13.2709C6.59845 14.7677 4.19111 14.7677 2.72938 13.2709Z" fill="#12D2AC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.94084 2.7323C9.40256 1.23549 11.8099 1.23549 13.2716 2.7323L18.4789 8.06459C19.8925 9.51202 19.8925 11.823 18.4789 13.2704C17.0172 14.7672 14.6099 14.7672 13.1482 13.2704L7.94084 7.93815C6.52733 6.49071 6.52733 4.17973 7.94084 2.7323Z" fill="#307AF2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7384 2.93044C9.30781 1.32337 11.8925 1.32337 13.4619 2.93045L15.8075 5.33229L10.6002 10.6646L5.39285 5.33229L7.7384 2.93044Z" fill="#0057FE"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 48 48" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M26.5 6a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H9v30h29a1 1 0 001-1V22.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V42a2 2 0 01-2 2H7a2 2 0 01-2-2V8a2 2 0 012-2h19.5zm3.768 14.707l3.864 1.035-4.141 15.455-3.864-1.035 4.141-15.455zM21.485 20l2.829 2.828-5.683 5.682 5.268 5.268-2.828 2.829-7.778-7.779.025-.025-.318-.318L21.485 20zM39.5 4a.5.5 0 01.5.5V9h4.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H40v4.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V13h-4.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H36V4.5a.5.5 0 01.5-.5h3z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 621 B

View File

@@ -0,0 +1,9 @@
<svg width="33" height="33" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.8 204">
<path fill="#307AF2" d="M86.7,0l88,51v.2l-16.3,9.4v-.2L86.7,18.9Zm71.8,143.5,16.3,9.4v.2L86.8,204h0l-16.3-9.4,16.3-9.4h0l71.7-41.5v-.2Z"/>
<path fill="#12D2AC" d="M16.3,143.5v.2L58,167.8l-16.3,9.4L0,153.1v-.2Z"/>
<path fill="#12D2AC" d="M104.1,93,15.9,143.8l-.2-.1V124.9l.2.1L87.7,83.6,104.1,93Z"/>
<path fill="#0057FE" d="M88.1,0,.1,51v.2l16.3,9.4v-.2L88.1,18.9Z"/>
<path fill="#307AF2" d="M.1,50.9.2,152.6l.2.1,16.3-9.4-.2-.1-.1-82.9L.1,50.9Z"/>
<path fill="#0057FE" d="M174.7,50.9l-.1,101.7-.2.1-16.3-9.4.2-.1.1-82.9Z"/>
<path fill="#12D2AC" d="M41.7,158.5l16.1,9.4,100.6-58.7V90.4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve"> <image id="image0" width="96" height="96" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAA/UExURQAAANsgQNogP9sgPdogPtogP9ogPtogP9ogP9og
PtsgQN8gQNsgPtogPt0gPtggPdsgPtwgQNogPtogPv///3+ZkFoAAAATdFJOUwAgUHCfz9+/r4BA
EO9gb3CQMM9sefpCAAAAAWJLR0QUkt/JNQAAAAd0SU1FB+gLFw4SAP63MKEAAAM4SURBVGje7Vrd
mqMwCFUT8+9sHN//XdfWWasBEhL124td7mbaniMHAoS26/4561d7BngQclTLjyltrPP3gQu5Qx9t
lMMd6FYvtAXpLqH7KYf+I9fUHBRvQxF+c6OJggv/prD1ER8UG34TqjK0Ze1TMzU6ffHVOej0i61+
bIB/meRFoh8b8dejx2Ho66KbBKKMP7fI/7FiMl3EX8aSPhfxlyVfmy7pv1k2V30tfpzBv2SOQNbB
6/XshhqCryp49Vb7GziVCUDyVpMTTE+E05kml+L5zp1bZVDq/bcyYv7zIZES0CVvQt/qrB5f0Ebg
Td6xQwAzlFd+z3Vd0R+CGTSzCM4u0BHoAf7CnBfsIUiZSoQcAW4T3Bl0RlTEATZB109SaymyMZuW
Zol4hp2pO4dch+CrOx3Aqpy8Dvt3FbrVAfuwA51+2IHuaQfmpx0Yqk+xn6xcy8NmMlrh5tzQCGOc
PWQev7UFHQXxXLJGofzFJxisRMIHyihUHL2Ruw4YPRZaUFvCxyhAodC0A7zZzxY+FEn8mYW/zlQn
DcDL9ADrmARnEYAHdIy5HpxVAAT0wOL594chQ5ApRJws2kx9wgDOQZcx/hX3k0qyhqDrrRlZQgX6
ocq3Xe8GYddylz0We67gczXbXmu82QkDCPZEArnHG3sBUSr1XpN9SlC5miG13l9Iq11sJEhPyS51
yly6rpMmCQJQYVpXopYgAAXANhJE6jnTsxwaCb4pFKBRWx4l+X4o2UCj0BSFRIhjWwFFMjbgp6uI
Y0EAZ61BpPSed+7soKKG2noBbvLnR4TXzEoGgJ9Oh0gjYS9aV3OgRaSNHeu2kZ1LcNUEp08BCbgr
b2TTHZCegs7MunwdRzfRmLzESrPwDYcz2IfweiwWwjQx+PeTxB8qEM+UG0le14v9GuP7QURNzxZk
6Mp799ferji0RDodrm9+l3xDvINBZZPiOoMqTFVXGUr4VxnK+E1fcVXhdzWXgMQ0tzpObTJVTDt9
5VcJb3nq1pTVTvCbxx6JGgpGWUd0YlM0wW9CMVI2mEs74j7mS6wW13+dsPYVnETF6bbfPngnrNHj
uxesPUHLOM33/bDivz1pvwFpJhpiuTDDXwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNC0xMS0yM1Qx
NDoxODowMCswMDowMJCjfg0AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjQtMTEtMjNUMTQ6MTg6MDAr
MDA6MDDh/saxAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDI0LTExLTIzVDE0OjE4OjAwKzAwOjAw
tuvnbgAAAABJRU5ErkJggg==" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,14 +1,17 @@
<template>
<a-breadcrumb>
<a-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
<span
v-if="item.redirect === 'noRedirect' || item.redirect === '' || index === breadcrumbList.length - 1"
class="gi_line_1"
>
{{ item.meta.title }}
</span>
<span v-else class="gi_line_1 breadcrumb-item-title" @click="handleLink(item)">{{ item.meta.title }}</span>
</a-breadcrumb-item>
<transition-group name="breadcrumb">
<div v-for="(item, index) in breadcrumbList" :key="item.meta.title">
<a-breadcrumb-item>
<span
v-if="item.redirect === 'noRedirect' || item.redirect === '' || index === breadcrumbList.length - 1"
class="gi_line_1"
>{{ item.meta.title }}</span>
<span v-else class="gi_line_1 breadcrumb-item-title" @click="handleLink(item)">{{ item.meta.title }}</span>
<icon-right v-if="index !== breadcrumbList.length - 1" />
</a-breadcrumb-item>
</div>
</transition-group>
</a-breadcrumb>
</template>
@@ -59,6 +62,27 @@ function handleLink(item: RouteLocationMatched) {
</script>
<style scoped lang="scss">
/** breadcrumb-transform 面包屑动画 */
.breadcrumb-enter-active {
transition: all 0.2s;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(10px);
}
:deep(.arco-breadcrumb-item) {
padding: 0;
display: flex;
align-items: center;
.arco-icon-right {
margin: 0 4px;
}
}
.breadcrumb-item-title {
transition: all 0.3s;
cursor: pointer;

View File

@@ -49,8 +49,6 @@ const open = (cron: string = '') => {
visible.value = true
}
defineExpose({ open })
// 确定
const handlerOk = () => {
if (cronInputRef.value?.checkCron()) {
@@ -65,6 +63,8 @@ const handlerOk = () => {
const handlerClose = () => {
visible.value = false
}
defineExpose({ open })
</script>
<style lang="less" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<a-overflow-list v-if="data.length">
<a-overflow-list v-if="data && data.length">
<a-tag v-for="(item, index) in data" :key="index" size="small">
{{ item }}
</a-tag>

View File

@@ -55,8 +55,6 @@ const attrs = useAttrs()
const form = computed(() => ({ tableData: props.data }))
const formRef = useTemplateRef('formRef')
defineExpose({ formRef })
const headerCellClass = (col: ColumnItem) => {
return col.required ? 'gi_column_require' : ''
}
@@ -122,6 +120,7 @@ const isDisabled: Props['cellDisabled'] = (p) => {
if (typeof props?.cellDisabled === 'function') return props.cellDisabled(p)
return false
}
defineExpose({ formRef })
</script>
<style lang='scss' scoped></style>

View File

@@ -7,9 +7,13 @@
:span="item.span || options.gridItem?.span"
>
<a-form-item
v-bind="item.formItemProps" :label="item.label" :field="item.field" :rules="item.rules"
v-bind="item.formItemProps" :field="item.field" :rules="item.rules"
:disabled="isDisabled(item.disabled)"
>
<template #label>
<template v-if="typeof item.label === 'string'">{{ item.label }}</template>
<component :is="item.label" v-else></component>
</template>
<slot
v-if="!['group-title'].includes(item.type || '')" :name="item.field"
v-bind="{ disabled: isDisabled(item.disabled) }"
@@ -25,7 +29,12 @@
:is="`a-${item.type}`" v-else v-bind="getComponentBindProps(item)"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"
></component>
>
<template v-for="(slotValue, slotKey) in item?.slots" :key="slotKey" #[slotKey]>
<template v-if="typeof slotValue === 'string'">{{ slotValue }}</template>
<component :is="slotValue" v-else></component>
</template>
</component>
</slot>
<slot v-else name="group-title">
<a-alert v-bind="item.props">{{ item.label }}</a-alert>
@@ -38,7 +47,7 @@
<slot name="suffix">
<a-button type="primary" @click="emit('search')">
<template #icon><icon-search /></template>
<template #default>{{ options.btns?.searchBtnText || '查询' }}</template>
<template #default>{{ options.btns?.searchBtnText || '搜索' }}</template>
</a-button>
<a-button @click="emit('reset')">
<template #icon><icon-refresh /></template>
@@ -167,8 +176,6 @@ const isDisabled = (disabled?: ColumnsItemDisabled<boolean | object>) => {
}
}
defineExpose({ formRef })
props.columns.forEach((item) => {
if (item.request && typeof item.request === 'function' && item?.init) {
item.request(props.modelValue).then((res) => {
@@ -213,6 +220,8 @@ watch(cloneForm as any, (newVal, oldVal) => {
}
})
})
defineExpose({ formRef })
</script>
<style scoped lang="scss">

View File

@@ -1,4 +1,5 @@
import type * as A from '@arco-design/web-vue'
import type { VNode } from 'vue'
export type FormType =
| 'input'
@@ -69,7 +70,7 @@ export type ColumnsItemOptionsOrData =
export interface ColumnsItem<F = any> {
type?: FormType // 类型
label?: A.FormItemInstance['label'] // 标签
label?: A.FormItemInstance['label'] | (() => VNode) // 标签
field: A.FormItemInstance['field'] // 字段(必须唯一)
gridItemProps?: A.GridItemProps
formItemProps?: Omit<A.FormItemInstance['$props'], 'label' | 'field'> // a-form-item的props
@@ -106,6 +107,8 @@ export interface ColumnsItem<F = any> {
resultFormat?: ColumnsItemFormat // 结果集格式化
init?: boolean // 初始化请求
cascader?: string[] // 级联的field字段列表
slots?: Partial<Record<'prepend' | 'append' | 'suffix' | 'prefix', string | (() => VNode)>>
formItemSlots?: Partial<Record<'help' | 'extra', string | (() => VNode)>>
}
export interface Options {

View File

@@ -0,0 +1,37 @@
<template>
<a-spin class="gi-iframe" :loading="loading">
<iframe class="iframe" :src="props.src" @load="onLoad"></iframe>
</a-spin>
</template>
<script lang='ts' setup>
defineOptions({ name: 'GiIframe' })
const props = withDefaults(defineProps<Props>(), {
src: '',
})
interface Props {
src: string
}
const loading = ref(true)
const onLoad = () => {
loading.value = false
}
</script>
<style scoped lang="scss">
.gi-iframe {
flex: 1;
}
.iframe {
border: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
</style>

View File

@@ -19,7 +19,7 @@
>
<template #top>
<a-space wrap :size="[8, 8]">
<a-input v-model="queryForm.description" placeholder="用户名/昵称/描述" allow-clear @change="search" />
<a-input-search v-model="queryForm.description" placeholder="搜索用户名/昵称/描述" allow-clear @search="search" />
<a-tree-select
v-model="queryForm.deptId"
:data="deptList"
@@ -106,7 +106,7 @@ const queryForm = reactive<UserQuery>({
// 用户列表
const { tableData: dataList, loading, pagination, search } = useTable(
(page) => listUser({ ...queryForm, ...page }),
{ immediate: true, formatResult: (data) => data.map((i) => ({ ...i, disabled: false })) },
{ immediate: true, formatResult: (data) => data.map((i) => ({ ...i, id: `${i?.id}`, disabled: false })) },
)
// 表格列定义
@@ -232,7 +232,7 @@ onMounted(async () => {
const { data } = await listAllUser({ userIds: props.value })
if (props.multiple) {
// 使用 Map 存储用户,避免重复
data.forEach((item) => {
data.map((i) => ({ ...i, id: `${i?.id}`, disabled: false })).forEach((item) => {
if (props.value.includes(item.id)) {
selectedData.value.set(item.id, item)
}

View File

@@ -345,7 +345,7 @@ export default {
function init() {
text.value = explain.value
getPicture()
// getPicture()
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight

View File

@@ -3,7 +3,7 @@ export * from './modules/usePagination'
export * from './modules/useRequest'
export * from './modules/useChart'
export * from './modules/useTable'
export * from './modules/useForm'
export * from './modules/useDevice'
export * from './modules/useBreakpoint'
export * from './modules/useDownload'
export * from './modules/useResetReactive'

View File

@@ -1,17 +0,0 @@
import { reactive } from 'vue'
import { cloneDeep } from 'lodash-es'
export function useForm<F extends object>(initValue: F) {
const getInitValue = () => cloneDeep(initValue)
const form = reactive(getInitValue())
const resetForm = () => {
for (const key in form) {
delete form[key]
}
Object.assign(form, getInitValue())
}
return { form, resetForm }
}

View File

@@ -0,0 +1,15 @@
import { reactive } from 'vue'
import { cloneDeep } from 'lodash-es'
export function useResetReactive<T extends object>(value: T) {
const getInitValue = () => cloneDeep(value)
const state = reactive(getInitValue())
const reset = () => {
Object.keys(state).forEach((key) => delete state[key])
Object.assign(state, getInitValue())
}
return [state, reset] as const
}

View File

@@ -57,6 +57,11 @@ export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U
pagination.onChange(1)
}
// 刷新
const refresh = () => {
getTableData()
}
// 删除
const handleDelete = async <T>(
deleteApi: () => Promise<ApiRes<T>>,
@@ -89,5 +94,5 @@ export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U
})
}
return { loading, tableData, getTableData, search, pagination, selectedKeys, select, selectAll, handleDelete }
return { loading, tableData, getTableData, search, pagination, selectedKeys, select, selectAll, handleDelete, refresh }
}

View File

@@ -106,8 +106,6 @@ const open = () => {
visible.value = true
}
defineExpose({ open })
// 默认显示的主题色列表
const defaultColorList = [
'#165DFF',
@@ -139,6 +137,8 @@ const changeColor = (colorObj: ColorObj) => {
if (!/^#[0-9A-Z]{6}/i.test(colorObj.hex)) return
appStore.setThemeColor(colorObj.hex)
}
defineExpose({ open })
</script>
<style scoped lang="scss">

View File

@@ -3,7 +3,7 @@
<router-view v-slot="{ Component, route }">
<transition :name="appStore.transitionName" mode="out-in" appear>
<keep-alive :include="(tabsStore.cacheList as string[])">
<component :is="Component" :key="route.path" />
<component :is="Component" :key="route.path + String(tabsStore.reloadFlag)" />
</keep-alive>
</transition>
</router-view>

View File

@@ -20,23 +20,34 @@
<span @contextmenu="(e) => handleContextMenu(e, item.path)">
{{ item.meta?.title }}
</span>
<template #content>
<a-doption @click="reload">
<template #icon>
<icon-refresh class="reload-icon" :class="{ 'reload-icon--spin': loading }" />
</template>
<template #default>重新加载</template>
</a-doption>
<a-doption @click="tabsStore.closeCurrent(currentContextPath)">
<template #icon>
<icon-close />
</template>
<template #default>关闭当前</template>
</a-doption>
<a-doption @click="tabsStore.closeLeft(currentContextPath)">
<template #icon>
<icon-to-left />
</template>
<template #default>关闭左侧</template>
</a-doption>
<a-doption @click="tabsStore.closeRight(currentContextPath)">
<template #icon>
<icon-close />
<icon-to-right />
</template>
<template #default>关闭右侧</template>
</a-doption>
<a-doption @click="tabsStore.closeOther(currentContextPath)">
<template #icon>
<icon-eraser />
<icon-close />
</template>
<template #default>关闭其他</template>
</a-doption>
@@ -58,21 +69,33 @@
</template>
</a-button>
<template #content>
<a-doption @click="reload">
<template #icon>
<icon-refresh class="reload-icon" :class="{ 'reload-icon--spin': loading }" />
</template>
<template #default>重新加载</template>
</a-doption>
<a-doption @click="tabsStore.closeCurrent(route.path)">
<template #icon>
<icon-close />
</template>
<template #default>关闭当前</template>
</a-doption>
<a-doption @click="tabsStore.closeRight(route.path)">
<a-doption @click="tabsStore.closeLeft(currentContextPath)">
<template #icon>
<icon-close />
<icon-to-left />
</template>
<template #default>关闭左侧</template>
</a-doption>
<a-doption @click="tabsStore.closeRight(currentContextPath)">
<template #icon>
<icon-to-right />
</template>
<template #default>关闭右侧</template>
</a-doption>
<a-doption @click="tabsStore.closeOther(route.path)">
<template #icon>
<icon-eraser />
<icon-close />
</template>
<template #default>关闭其他</template>
</a-doption>
@@ -131,6 +154,17 @@ const handleContextMenu = (e: MouseEvent, path: string) => {
e.preventDefault()
currentContextPath.value = path
}
const loading = ref(false)
// 重新加载
const reload = () => {
if (loading.value) return
loading.value = true
tabsStore.reloadPage()
setTimeout(() => {
loading.value = false
}, 600)
}
</script>
<style scoped lang="scss">
@@ -164,4 +198,13 @@ const handleContextMenu = (e: MouseEvent, path: string) => {
background-color: var(--color-bg-1);
position: relative;
}
.reload-icon {
cursor: pointer;
&--spin {
animation-name: arco-loading-circle;
animation-duration: 0.6s;
}
}
</style>

View File

@@ -9,8 +9,6 @@ import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import App from './App.vue'
import router from './router'
import '@/router/permission'
// 使用动画库
import 'animate.css/animate.min.css'
@@ -25,6 +23,8 @@ import 'virtual:svg-icons-register'
// 自定义指令
import directives from './directives'
// 状态管理
import pinia from '@/stores'
// 对特定组件进行默认配置

144
src/router/guard.ts Normal file
View File

@@ -0,0 +1,144 @@
import { Button, Message, Notification, Space } from '@arco-design/web-vue'
import NProgress from 'nprogress'
import type { Router } from 'vue-router'
import { useRouteStore, useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import 'nprogress/nprogress.css'
NProgress.configure({
easing: 'ease', // 动画方式
speed: 500, // 递增进度条的速度
showSpinner: false, // 是否显示圆圈加载
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 初始化时的最小百分比
})
// 版本更新
let versionTag: string | null = null // 版本标识
// 更新
const onUpdateSystem = (id: string) => {
Notification.remove(id)
window.location.reload()
}
// 关闭更新弹窗
const onCloseUpdateSystem = (id: string) => {
Notification.remove(id)
}
// 提示用户更新弹窗
const handleNotification = () => {
const id = `updateModel`
Notification.info({
id,
title: '新版本更新',
content: '当前系统检测到有新的版本,请及时更新',
duration: 0,
closable: true,
position: 'bottomRight',
footer: () => {
return h(Space, {}, () => [h(Button, {
type: 'primary',
onClick: () => onUpdateSystem(id),
}, '更新'), h(Button, { type: 'secondary', onClick: () => onCloseUpdateSystem(id) }, '关闭')])
},
})
}
/**
* 获取首页的 ETag 或 Last-Modified 值,作为当前版本标识
* @returns {Promise<string|null>} 返回 ETag 或 Last-Modified 值
*/
const getVersionTag = async () => {
const response = await fetch('/', {
cache: 'no-cache',
})
return response.headers.get('etag') || response.headers.get('last-modified')
}
/**
* 比较当前的 ETag 或 Last-Modified 值与最新获取的值
*/
const compareTag = async () => {
const newVersionTag = await getVersionTag()
if (versionTag === null) {
versionTag = newVersionTag
} else if (versionTag !== newVersionTag) {
// 如果 ETag 或 Last-Modified 发生变化,则认为有更新
// 提示用户更新
handleNotification()
}
}
/** 免登录白名单 */
const whiteList = ['/login', '/social/callback', '/pwdExpired']
/** 是否已经生成过路由表 */
let hasRouteFlag = false
export const resetHasRouteFlag = () => {
hasRouteFlag = false
}
/** 初始化路由守卫 */
export const setupRouterGuard = (router: Router) => {
router.beforeEach(async (to, from, next) => {
NProgress.start()
const userStore = useUserStore()
const routeStore = useRouteStore()
// 判断该用户是否登录
if (getToken()) {
if (to.path === '/login') {
// 如果已经登录,并准备进入 Login 页面,则重定向到主页
next()
} else {
if (!hasRouteFlag) {
try {
await userStore.getInfo()
if (userStore.userInfo.pwdExpired && to.path !== '/pwdExpired') {
Message.warning('密码已过期,请修改密码')
next('/pwdExpired')
}
const accessRoutes = await routeStore.generateRoutes()
accessRoutes.forEach((route) => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
hasRouteFlag = true
// 确保添加路由已完成
// 设置 replace: true, 因此导航将不会留下历史记录
next({ ...to, replace: true })
} catch (error: any) {
// 过程中发生任何错误,都直接重置 Token并重定向到登录页面
await userStore.logoutCallBack()
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
}
} else {
// 如果没有 Token
if (whiteList.includes(to.path)) {
// 如果在免登录的白名单中,则直接进入
next()
} else {
// 其他没有访问权限的页面将被重定向到登录页面
next('/login')
}
}
// 生产环境开启检测版本更新
const isProd = import.meta.env.PROD
if (isProd) {
await compareTag()
}
})
router.onError(() => {
NProgress.done()
})
router.afterEach(() => {
NProgress.done()
})
}

View File

@@ -1,97 +1,16 @@
import { type RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { useRouteStore } from '@/stores'
/** 默认布局 */
const Layout = () => import('@/layout/index.vue')
/** 静态路由 */
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/redirect',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/default/redirect/index.vue'),
},
],
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true },
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),
meta: { hidden: true },
},
{
path: '/403',
component: () => import('@/views/default/error/403.vue'),
meta: { hidden: true },
},
{
path: '/',
name: 'Dashboard',
component: Layout,
redirect: '/dashboard/workplace',
meta: { title: '仪表盘', icon: 'dashboard', hidden: false },
children: [
{
path: '/dashboard/workplace',
name: 'Workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: { title: '工作台', icon: 'desktop', hidden: false, affix: true },
},
{
path: '/dashboard/analysis',
name: 'Analysis',
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: { title: '分析页', icon: 'insert-chart', hidden: false },
},
],
},
{
path: '/social/callback',
component: () => import('@/views/login/social/index.vue'),
meta: { hidden: true },
},
{
path: '/pwdExpired',
component: () => import('@/views/login/pwdExpired/index.vue'),
meta: { hidden: true },
},
{
path: '/setting',
name: 'Setting',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/setting/profile',
name: 'SettingProfile',
component: () => import('@/views/setting/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false },
},
{
path: '/setting/message',
name: 'SettingMessage',
component: () => import('@/views/setting/message/index.vue'),
meta: { title: '消息中心', showInTabs: false },
},
],
},
]
import { constantRoutes, systemRoutes } from '@/router/route'
import { setupRouterGuard } from '@/router/guard'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes,
routes: [...constantRoutes, ...systemRoutes],
scrollBehavior: () => ({ left: 0, top: 0 }),
})
setupRouterGuard(router)
/**
* @description 重置路由
* @description 注意:所有动态路由路由必须带有 name 属性,否则可能会不能完全重置干净

View File

@@ -1,131 +0,0 @@
import { Button, Message, Notification, Space } from '@arco-design/web-vue'
import NProgress from 'nprogress'
import router from '@/router'
import { useRouteStore, useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
// 版本更新
let versionTag: string | null = null // 版本标识
// 更新
const onUpdateSystem = (id: string) => {
Notification.remove(id)
window.location.reload()
}
// 关闭更新弹窗
const onCloseUpdateSystem = (id: string) => {
Notification.remove(id)
}
// 提示用户更新弹窗
const handleNotification = () => {
const id = `updateModel`
Notification.info({
id,
title: '新版本更新',
content: '当前系统检测到有新的版本,请及时更新',
duration: 0,
closable: true,
position: 'bottomRight',
footer: () => {
return h(Space, {}, () => [h(Button, { type: 'primary', onClick: () => onUpdateSystem(id) }, '更新'), h(Button, { type: 'secondary', onClick: () => onCloseUpdateSystem(id) }, '关闭')])
},
})
}
/**
* 获取首页的 ETag 或 Last-Modified 值,作为当前版本标识
* @returns {Promise<string|null>} 返回 ETag 或 Last-Modified 值
*/
const getVersionTag = async () => {
const response = await fetch('/', {
cache: 'no-cache',
})
return response.headers.get('etag') || response.headers.get('last-modified')
}
/**
* 比较当前的 ETag 或 Last-Modified 值与最新获取的值
*/
const compareTag = async () => {
const newVersionTag = await getVersionTag()
if (versionTag === null) {
versionTag = newVersionTag
} else if (versionTag !== newVersionTag) {
// 如果 ETag 或 Last-Modified 发生变化,则认为有更新
// 提示用户更新
handleNotification()
}
}
/** 免登录白名单 */
const whiteList = ['/login', '/social/callback', '/pwdExpired']
/** 是否已经生成过路由表 */
let hasRouteFlag = false
export const resetHasRouteFlag = () => {
hasRouteFlag = false
}
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const routeStore = useRouteStore()
NProgress.start()
// 判断该用户是否登录
if (getToken()) {
if (to.path === '/login') {
// 如果已经登录,并准备进入 Login 页面,则重定向到主页
next()
NProgress.done()
} else {
if (!hasRouteFlag) {
try {
await userStore.getInfo()
if (userStore.userInfo.pwdExpired && to.path !== '/pwdExpired') {
Message.warning('密码已过期,请修改密码')
next('/pwdExpired')
NProgress.done()
}
const accessRoutes = await routeStore.generateRoutes()
accessRoutes.forEach((route) => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
hasRouteFlag = true
// 确保添加路由已完成
// 设置 replace: true, 因此导航将不会留下历史记录
next({ ...to, replace: true })
NProgress.done()
} catch (error: any) {
// 过程中发生任何错误,都直接重置 Token并重定向到登录页面
await userStore.logoutCallBack()
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
next()
NProgress.done()
}
}
} else {
// 如果没有 Token
if (whiteList.includes(to.path)) {
// 如果在免登录的白名单中,则直接进入
next()
NProgress.done()
} else {
// 其他没有访问权限的页面将被重定向到登录页面
next('/login')
NProgress.done()
}
}
// 生产环境开启检测版本更新
const isProd = import.meta.env.PROD
if (isProd) {
await compareTag()
}
})

132
src/router/route.ts Normal file
View File

@@ -0,0 +1,132 @@
import type { RouteRecordRaw } from 'vue-router'
/** 默认布局 */
const Layout = () => import('@/layout/index.vue')
/** 系统路由 */
export const systemRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true },
},
{
path: '/',
name: 'Dashboard',
component: Layout,
redirect: '/dashboard/workplace',
meta: { title: '仪表盘', icon: 'dashboard', hidden: false },
children: [
{
path: '/dashboard/workplace',
name: 'Workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: { title: '工作台', icon: 'desktop', hidden: false, affix: true },
},
{
path: '/dashboard/analysis',
name: 'Analysis',
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: { title: '分析页', icon: 'insert-chart', hidden: false },
},
],
},
{
path: '/social/callback',
component: () => import('@/views/login/social/index.vue'),
meta: { hidden: true },
},
{
path: '/pwdExpired',
component: () => import('@/views/login/pwdExpired/index.vue'),
meta: { hidden: true },
},
{
path: '/setting',
name: 'Setting',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/setting/profile',
name: 'SettingProfile',
component: () => import('@/views/setting/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false },
},
{
path: '/setting/message',
name: 'SettingMessage',
component: () => import('@/views/setting/message/index.vue'),
meta: { title: '消息中心', showInTabs: false },
},
],
},
{
path: '/about',
name: 'About',
component: Layout,
meta: { title: '关于项目', icon: 'apps', hidden: false, sort: 999 },
children: [
{
path: '/about/document/continew',
component: () => import('@/views/about/document/continew/index.vue'),
meta: { title: '在线文档', icon: 'continew', hidden: false, keepAlive: true },
},
{
path: '/about/document/api',
component: () => import('@/views/about/document/api/index.vue'),
meta: { title: '接口文档', icon: 'continew', hidden: false, keepAlive: true },
},
{
path: '/about/document/arco-design-vue',
component: () => import('@/views/about/document/arco-design-vue/index.vue'),
meta: { title: 'Arco Design文档', icon: 'arco', hidden: false, keepAlive: true },
},
{
path: '/about/source',
name: 'AboutSource',
meta: { title: '开源地址', icon: 'github', hidden: false },
children: [
{
path: 'https://gitee.com/continew/continew-admin',
meta: { title: 'Gitee', icon: 'gitee', hidden: false },
},
{
path: 'https://gitcode.com/continew/continew-admin',
meta: { title: 'GitCode', icon: 'gitcode', hidden: false },
},
{
path: 'https://github.com/continew-org/continew-admin',
meta: { title: 'GitHub', icon: 'github', hidden: false },
},
],
},
],
},
]
// 固定路由(默认路由)
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/redirect',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/default/redirect/index.vue'),
},
],
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),
meta: { hidden: true },
},
{
path: '/403',
component: () => import('@/views/default/error/403.vue'),
meta: { hidden: true },
},
]

View File

@@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { mapTree, toTreeArray } from 'xe-utils'
import { cloneDeep, omit } from 'lodash-es'
import { constantRoutes } from '@/router'
import { constantRoutes, systemRoutes } from '@/router/route'
import ParentView from '@/components/ParentView/index.vue'
import { type RouteItem, getUserRoute } from '@/apis'
import { transformPathToName } from '@/utils'
@@ -44,7 +44,6 @@ const transformComponentView = (component: string) => {
*/
const formatAsyncRoutes = (menus: RouteItem[]) => {
if (!menus.length) return []
menus.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序
const pathMap = new Map()
const routes = mapTree(menus, (item) => {
pathMap.set(item.id, item.path)
@@ -103,7 +102,9 @@ const storeSetup = () => {
// 合并路由
const setRoutes = (data: RouteRecordRaw[]) => {
routes.value = constantRoutes.concat(data)
// 合并路由并排序
routes.value = [...constantRoutes, ...systemRoutes].concat(data)
.sort((a, b) => (a.meta?.sort ?? 0) - (b.meta?.sort ?? 0))
asyncRoutes.value = data
}

View File

@@ -83,6 +83,17 @@ const storeSetup = () => {
})
}
// 关闭左侧
const closeLeft = (path: string) => {
const index = tabList.value.findIndex((i) => i.path === path)
if (index < 0) return
const arr = tabList.value.filter((i, n) => n < index)
arr.forEach((item) => {
deleteTabItem(item.path)
item?.name && deleteCacheItem(item.name)
})
}
// 关闭右侧
const closeRight = (path: string) => {
const index = tabList.value.findIndex((i) => i.path === path)
@@ -113,6 +124,18 @@ const storeSetup = () => {
reset()
}
// Tabs页签右侧刷新按钮-页面重新加载
const reloadFlag = ref(true)
const reloadPage = () => {
const route = router.currentRoute.value
deleteCacheItem(route.name as string) // 修复点击刷新图标,无法重新触发生命周期钩子函数问题
reloadFlag.value = false
nextTick(() => {
reloadFlag.value = true
addCacheItem(route)
})
}
return {
tabList,
cacheList,
@@ -124,10 +147,13 @@ const storeSetup = () => {
clearCacheList,
closeCurrent,
closeOther,
closeLeft,
closeRight,
closeAll,
reset,
init,
reloadFlag,
reloadPage,
}
}

View File

@@ -14,7 +14,7 @@ import {
socialLogin as socialLoginApi,
} from '@/apis'
import { clearToken, getToken, setToken } from '@/utils/auth'
import { resetHasRouteFlag } from '@/router/permission'
import { resetHasRouteFlag } from '@/router/guard'
const storeSetup = () => {
const userInfo = reactive<UserInfo>({

View File

@@ -28,6 +28,7 @@ declare module 'vue' {
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
GiOption: typeof import('./../components/GiOption/index.vue')['default']
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
GiOverFlowTags: typeof import('./../components/GiOverFlowTags/index.vue')['default']

View File

@@ -0,0 +1,9 @@
<template>
<GiIframe src="https://api.continew.top/doc.html"></GiIframe>
</template>
<script lang='ts' setup>
defineOptions({ name: 'AboutDocumentApi' })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,9 @@
<template>
<GiIframe src="https://arco.design/vue/component/button"></GiIframe>
</template>
<script lang='ts' setup>
defineOptions({ name: 'AboutDocumentArcoDesignVue' })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,9 @@
<template>
<GiIframe src="https://continew.top"></GiIframe>
</template>
<script lang='ts' setup>
defineOptions({ name: 'AboutDocumentContiNew' })
</script>
<style scoped lang="scss"></style>

View File

@@ -125,7 +125,7 @@ import { type FieldConfigResp, type GeneratorConfigResp, getGenConfig, listField
import type { LabelValueState } from '@/types/global'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
const emit = defineEmits<{
@@ -146,7 +146,7 @@ const options: Options = {
grid: { cols: 2 },
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
isOverride: false,
})
const formColumns: Columns = reactive([

View File

@@ -13,9 +13,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.tableName" placeholder="请输入表名称" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.tableName" placeholder="搜索表名称" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>

View File

@@ -12,7 +12,6 @@ import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisTimeslot as getData } from '@/apis/common'
import handleIcon from '@/assets/icons/slider.svg'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
// 提示框
const tooltipItemsHtmlString = (items) => {
return items

View File

@@ -13,7 +13,6 @@ import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisBrowser as getData } from '@/apis/common'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const xAxis = ref<string[]>([])
const chartData = ref([])
const { chartOption } = useChart((isDark: EChartsOption) => {

View File

@@ -42,7 +42,6 @@ import { computed } from 'vue'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const appStore = useAppStore()
const isDark = computed(() => appStore.theme === 'dark')

View File

@@ -42,7 +42,6 @@ import { computed } from 'vue'
import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const appStore = useAppStore()
const isDark = computed(() => appStore.theme === 'dark')

View File

@@ -43,7 +43,6 @@ import { useChart } from '@/hooks'
import { useAppStore } from '@/stores'
import { type DashboardChartCommonResp, getDashboardOverviewIp as getData } from '@/apis'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const appStore = useAppStore()
const isDark = computed(() => appStore.theme === 'dark')

View File

@@ -45,7 +45,6 @@ import { type DashboardChartCommonResp, getDashboardOverviewPv as getData } from
const appStore = useAppStore()
const isDark = computed(() => appStore.theme === 'dark')
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const count = ref(0)
const today = ref(0)
const growth = ref(0)

View File

@@ -30,8 +30,6 @@ import type { EChartsOption } from 'echarts'
import { getAnalysisGeo as getData } from '@/apis/common/dashboard'
import { useChart } from '@/hooks'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const chartRef = useTemplateRef('chartRef')
const chartData = ref([])
const totalValue = ref(0)

View File

@@ -13,8 +13,6 @@ import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisModule as getData } from '@/apis/common'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const yAxis = ref<string[]>([])
const chartData = ref([])
const { chartOption } = useChart((isDark: EChartsOption) => {

View File

@@ -13,7 +13,6 @@ import type { EChartsOption } from 'echarts'
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisOs as getData } from '@/apis/common'
const Chart = defineAsyncComponent(() => import('@/components/Chart/index.vue'))
const xAxis = ref<string[]>([])
const chartData = ref([])
const { chartOption } = useChart((isDark: EChartsOption) => {

View File

@@ -35,7 +35,7 @@ import Geo from './components/Geo.vue'
import Os from './components/Os.vue'
import Browser from './components/Browser.vue'
import Module from './components/Module.vue'
import AccessTimeslot from '@/views/dashboard/analysis/components/AccessTimeslot.vue'
import AccessTimeslot from './components/AccessTimeslot.vue'
defineOptions({ name: 'Analysis' })
</script>

View File

@@ -33,7 +33,15 @@
<template #title>
<a-space>
<img :src="item.logo" width="35px" height="25px" alt="logo" />
<a-typography-text bold>{{ item.alias }}</a-typography-text>
<a-typography-paragraph
:ellipsis="{
rows: 1,
showTooltip: true,
css: true,
}"
>
{{ item.alias }}
</a-typography-paragraph>
</a-space>
</template>
<template #description>

View File

@@ -32,7 +32,7 @@ const links = [
{ text: '角色管理', icon: 'user-group', path: '/system/role' },
{ text: '菜单管理', icon: 'menu', path: '/system/menu' },
{ text: '文件管理', icon: 'file', path: '/system/file' },
{ text: '代码生成', icon: 'code', path: '/tool/generator' },
{ text: '代码生成', icon: 'code', path: '/code/generator' },
{ text: '系统日志', icon: 'history', path: '/monitor/log' },
]
</script>

View File

@@ -1,7 +1,12 @@
<template>
<a-form
ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin"
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="username" hide-label>
<a-input v-model="form.username" placeholder="请输入用户名" allow-clear />
@@ -9,7 +14,7 @@
<a-form-item field="password" hide-label>
<a-input-password v-model="form.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-form-item v-if="isCaptchaEnabled" field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<div class="captcha-container" @click="getCaptcha">
<img :src="captchaImgBase64" alt="验证码" class="captcha" />
@@ -46,6 +51,10 @@ const loginConfig = useStorage('login-config', {
// username: debug ? 'admin' : '', // 演示默认值
// password: debug ? 'admin123' : '', // 演示默认值
})
// 是否启用验证码
const isCaptchaEnabled = ref(true)
// 验证码图片
const captchaImgBase64 = ref()
const formRef = ref<FormInstance>()
const form = reactive({
@@ -58,7 +67,7 @@ const form = reactive({
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
captcha: [{ required: true, message: '请输入验证码' }],
captcha: [{ required: isCaptchaEnabled.value, message: '请输入验证码' }],
}
// 验证码过期定时器
@@ -83,13 +92,13 @@ onBeforeUnmount(() => {
}
})
const captchaImgBase64 = ref()
// 获取验证码
const getCaptcha = () => {
getImageCaptcha().then((res) => {
const { uuid, img, expireTime } = res.data
form.uuid = uuid
const { uuid, img, expireTime, isEnabled } = res.data
isCaptchaEnabled.value = isEnabled
captchaImgBase64.value = img
form.uuid = uuid
form.expired = false
startTimer(expireTime)
})

View File

@@ -95,6 +95,8 @@ const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('phone')
if (isInvalid) return
// 重置行为参数
VerifyRef.value.instance.refresh()
VerifyRef.value.show()
}

View File

@@ -6,8 +6,12 @@
</a-space>
</a-row>
<a-tabs v-model:active-key="activeKey" type="card-gutter" size="large" @change="change">
<a-tab-pane key="1" title="登录日志" />
<a-tab-pane key="2" title="操作日志" />
<a-tab-pane key="1">
<template #title><icon-lock /> 登录日志</template>
</a-tab-pane>
<a-tab-pane key="2">
<template #title><icon-find-replace /> 操作日志</template>
</a-tab-pane>
</a-tabs>
<keep-alive>
<component :is="PaneMap[activeKey]" />

View File

@@ -11,10 +11,10 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.createUserString" placeholder="请输入登录用户" allow-clear @change="search">
<a-input v-model="queryForm.createUserString" placeholder="搜索登录用户" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input v-model="queryForm.ip" placeholder="请输入登录 IP 或地点" allow-clear @change="search">
<a-input v-model="queryForm.ip" placeholder="搜索登录 IP 或地点" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<DateRangePicker v-model="queryForm.createTime" @change="search" />

View File

@@ -12,10 +12,10 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.createUserString" placeholder="请输入操作人" allow-clear @change="search">
<a-input v-model="queryForm.createUserString" placeholder="搜索操作人" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input v-model="queryForm.ip" placeholder="请输入操作 IP 或地点" allow-clear @change="search">
<a-input v-model="queryForm.ip" placeholder="搜索操作 IP 或地点" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<DateRangePicker v-model="queryForm.createTime" @change="search" />

View File

@@ -12,9 +12,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.nickname" placeholder="请输入用户名/昵称" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.nickname" placeholder="搜索用户名/昵称" allow-clear @search="search" />
<DateRangePicker v-model="queryForm.loginTime" @change="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>

View File

@@ -18,7 +18,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addApp, getApp, updateApp } from '@/apis/open/app'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -37,7 +37,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
status: 1,
})

View File

@@ -13,9 +13,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.description" placeholder="请输入名称/描述" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>

View File

@@ -180,10 +180,9 @@
import { type ColProps, type FormInstance, Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addJob, listGroup, updateJob } from '@/apis/schedule/job'
import { useForm } from '@/hooks'
import { useDict } from '@/hooks/app'
import CronGeneratorModal from '@/components/GenCron/CronModel/index.vue'
import type { LabelValueState } from '@/types/global'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -221,7 +220,7 @@ const rules: FormInstance['rules'] = {
parallelNum: [{ required: true, message: '请输入并行数' }],
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
triggerType: 2,
triggerInterval: 60,
taskType: 1,

View File

@@ -20,7 +20,7 @@
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-input-search v-model="queryForm.jobName" placeholder="搜索任务名称" allow-clear @search="search" />
<a-select v-model="queryForm.jobStatus" placeholder="请选择任务状态" :options="job_status_enum" allow-clear style="width: 150px" @change="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>

View File

@@ -19,7 +19,7 @@
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-input-search v-model="queryForm.jobName" placeholder="搜索任务名称" allow-clear @search="search" />
<a-select
v-model="queryForm.taskBatchStatus"
placeholder="请选择状态"

View File

@@ -29,7 +29,7 @@ import { type BehaviorCaptchaReq, getEmailCaptcha, updateUserEmail, updateUserPa
import { encryptByRsa } from '@/utils/encrypt'
import { useUserStore } from '@/stores'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import * as Regexp from '@/utils/regexp'
import modalErrorWrapper from '@/utils/modal-error-wrapper'
import router from '@/router'
@@ -49,7 +49,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
phone: '',
email: '',
captcha: '',

View File

@@ -18,8 +18,8 @@ import { useWindowSize } from '@vueuse/core'
import { Message } from '@arco-design/web-vue'
import { updateUserBaseInfo } from '@/apis/system'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useUserStore } from '@/stores'
import { useResetReactive } from '@/hooks'
const { width } = useWindowSize()
const userStore = useUserStore()
@@ -33,7 +33,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
nickname: userInfo.value.nickname,
gender: userInfo.value.gender,
})

View File

@@ -1,6 +1,14 @@
<template>
<a-form ref="formRef" :model="form" :rules="rules" size="large" layout="vertical" :disabled="!isUpdate" class="form">
<a-list class="list-layout" :bordered="false" :loading="loading">
<a-spin :loading="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
size="large"
layout="vertical"
:disabled="!isUpdate"
class="form"
>
<a-form-item class="image-item" field="SITE_LOGO" hide-label>
{{ siteConfig.SITE_LOGO.name }}
<template #extra>
@@ -65,52 +73,51 @@
</template>
</a-form-item>
<a-form-item class="input-item" field="SITE_TITLE" :label="siteConfig.SITE_TITLE.name">
<a-input v-model.trim="form.SITE_TITLE" placeholder="请输入系统标题" :max-length="18" show-word-limit />
<a-input v-model.trim="form.SITE_TITLE" class="input-width" placeholder="请输入系统标题" :max-length="18" show-word-limit />
</a-form-item>
<a-form-item class="input-item" field="SITE_DESCRIPTION" :label="siteConfig.SITE_DESCRIPTION.name">
<a-textarea
v-model.trim="form.SITE_DESCRIPTION"
class="input-width"
placeholder="请输入系统描述"
:auto-size="{ minRows: 1, maxRows: 3 }"
/>
</a-form-item>
<a-form-item class="input-item" field="SITE_COPYRIGHT" :label="siteConfig.SITE_COPYRIGHT.name">
<a-input v-model.trim="form.SITE_COPYRIGHT" placeholder="请输入版权信息" />
<a-input v-model.trim="form.SITE_COPYRIGHT" class="input-width" placeholder="请输入版权信息" />
</a-form-item>
<a-form-item class="input-item" field="SITE_BEIAN" :label="siteConfig.SITE_BEIAN.name">
<a-input v-model.trim="form.SITE_BEIAN" placeholder="请输入备案号" :max-length="30" show-word-limit style="width: 100%;" />
<a-form-item field="SITE_BEIAN" :label="siteConfig.SITE_BEIAN.name">
<a-input v-model.trim="form.SITE_BEIAN" class="input-width" placeholder="请输入备案号" :max-length="30" show-word-limit />
</a-form-item>
<div style="margin-top: 20px">
<a-space>
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon>
<icon-edit />
</template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon>
<icon-undo />
</template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon>
<icon-save />
</template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon>
<icon-refresh />
</template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon>
<icon-undo />
</template>取消
</a-button>
</a-space>
</div>
</a-list>
</a-form>
<a-space style="margin-bottom: 16px">
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon>
<icon-edit />
</template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon>
<icon-undo />
</template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon>
<icon-save />
</template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon>
<icon-refresh />
</template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon>
<icon-undo />
</template>取消
</a-button>
</a-space>
</a-form>
</a-spin>
</template>
<script setup lang="ts">
@@ -123,14 +130,14 @@ import {
updateOption,
} from '@/apis/system'
import { useAppStore } from '@/stores'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { fileToBase64 } from '@/utils'
defineOptions({ name: 'BasicSetting' })
const loading = ref<boolean>(false)
const formRef = ref<FormInstance>()
const { form } = useForm({
const [form] = useResetReactive({
SITE_FAVICON: '',
SITE_LOGO: '',
SITE_TITLE: '',
@@ -306,18 +313,23 @@ onMounted(() => {
line-height: 46px;
}
.input-width {
width: 500px;
}
.arco-form .image-item,
.input-item {
margin-bottom: 5px;
}
:deep(.arco-form-item-layout-vertical > .arco-form-item-label-col) {
margin-bottom: 0;
}
:deep(.arco-list-medium .arco-list-content-wrapper .arco-list-content > .arco-list-item) {
padding: 13px;
border-bottom: 1px solid var(--color-fill-3);
}
:deep(.arco-form-item-wrapper-col) {
width: 100%;
max-width: 500px;
// responsive
.mobile {
.input-width {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<a-spin :loading="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
label-align="left"
:layout="width >= 500 ? 'horizontal' : 'vertical'"
:disabled="!isUpdate"
scroll-to-first-error
>
<a-form-item
field="LOGIN_CAPTCHA_ENABLED"
:label="loginConfig.LOGIN_CAPTCHA_ENABLED.name"
>
<a-switch
v-model="form.LOGIN_CAPTCHA_ENABLED"
type="round"
:checked-value="1"
:unchecked-value="0"
>
<template #checked></template>
<template #unchecked></template>
</a-switch>
</a-form-item>
<a-space style="margin-bottom: 16px">
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon><icon-edit /></template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon><icon-undo /></template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon><icon-save /></template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon><icon-refresh /></template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon><icon-undo /></template>取消
</a-button>
</a-space>
</a-form>
</a-spin>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type FormInstance, Message, Modal } from '@arco-design/web-vue'
import { type LoginConfig, type OptionResp, listOption, resetOptionValue, updateOption } from '@/apis/system'
import { useResetReactive } from '@/hooks'
defineOptions({ name: 'LoginSetting' })
const { width } = useWindowSize()
const loading = ref<boolean>(false)
const formRef = ref<FormInstance>()
const [form] = useResetReactive({
LOGIN_CAPTCHA_ENABLED: 1,
})
const rules: FormInstance['rules'] = {
LOGIN_CAPTCHA_ENABLED: [{ required: true, message: '请选择' }],
}
const loginConfig = ref<LoginConfig>({
LOGIN_CAPTCHA_ENABLED: {},
})
// 重置
const reset = () => {
formRef.value?.resetFields()
form.LOGIN_CAPTCHA_ENABLED = loginConfig.value.LOGIN_CAPTCHA_ENABLED.value || 0
}
const isUpdate = ref(false)
// 修改
const onUpdate = () => {
isUpdate.value = true
}
// 取消
const handleCancel = () => {
reset()
isUpdate.value = false
}
const queryForm = {
category: 'LOGIN',
}
// 查询列表数据
const getDataList = async () => {
try {
loading.value = true
const { data } = await listOption(queryForm)
loginConfig.value = data.reduce((obj: LoginConfig, option: OptionResp) => {
obj[option.code] = { ...option, value: Number.parseInt(option.value) }
return obj
}, {})
handleCancel()
} finally {
loading.value = false
}
}
// 保存
const handleSave = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
await updateOption(
Object.entries(form).map(([key, value]) => {
return { id: loginConfig.value[key].id, code: key, value }
}),
)
await getDataList()
Message.success('保存成功')
}
// 恢复默认
const handleResetValue = async () => {
await resetOptionValue(queryForm)
Message.success('恢复成功')
await getDataList()
}
const onResetValue = () => {
Modal.warning({
title: '警告',
content: '确认恢复登录配置为默认值吗?',
hideCancel: false,
maskClosable: false,
onOk: handleResetValue,
})
}
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss">
:deep(.arco-form-item.arco-form-item-has-help) {
margin-bottom: 5px;
}
.input-width {
width: 200px;
}
</style>

View File

@@ -1,59 +1,68 @@
<template>
<a-space wrap :size="30">
<a-spin :loading="loading">
<a-form
ref="formRef" :model="form" :rules="rules" auto-label-width label-align="left"
:layout="width >= 500 ? 'horizontal' : 'vertical'" :disabled="!isUpdate" scroll-to-first-error
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
label-align="left"
:layout="width >= 500 ? 'horizontal' : 'vertical'"
:disabled="!isUpdate"
scroll-to-first-error
>
<a-list :bordered="false" :loading="loading">
<a-form-item field="MAIL_PROTOCOL" :label="mailConfig.MAIL_PROTOCOL.name" hide-asterisk>
<a-select v-model.trim="form.MAIL_PROTOCOL">
<a-option label="SMTP" value="smtp" />
</a-select>
</a-form-item>
<a-form-item field="MAIL_HOST" :label="mailConfig.MAIL_HOST.name" hide-asterisk>
<a-input v-model.trim="form.MAIL_HOST" />
</a-form-item>
<a-form-item field="MAIL_PORT" :label="mailConfig.MAIL_PORT.name" hide-asterisk>
<a-input-number v-model="form.MAIL_PORT" :min="0" />
</a-form-item>
<a-form-item field="MAIL_USERNAME" :label="mailConfig.MAIL_USERNAME.name" hide-asterisk>
<a-input v-model.trim="form.MAIL_USERNAME" />
</a-form-item>
<a-form-item field="MAIL_PASSWORD" :label="mailConfig.MAIL_PASSWORD?.name" hide-asterisk>
<a-input-password v-model.trim="form.MAIL_PASSWORD" />
</a-form-item>
<a-form-item field="MAIL_SSL_ENABLED" :label="mailConfig.MAIL_SSL_ENABLED?.name" hide-asterisk>
<a-radio-group v-model:model-value="form.MAIL_SSL_ENABLED">
<a-radio value="1">启用</a-radio>
<a-radio value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="form.MAIL_SSL_ENABLED === '1'" field="MAIL_SSL_PORT" :label="mailConfig.MAIL_SSL_PORT.name"
hide-asterisk
<a-form-item field="MAIL_PROTOCOL" :label="mailConfig.MAIL_PROTOCOL.name" hide-asterisk>
<a-select v-model.trim="form.MAIL_PROTOCOL">
<a-option label="SMTP" value="smtp" />
</a-select>
</a-form-item>
<a-form-item field="MAIL_HOST" :label="mailConfig.MAIL_HOST.name" hide-asterisk>
<a-input v-model.trim="form.MAIL_HOST" class="input-width" />
</a-form-item>
<a-form-item field="MAIL_PORT" :label="mailConfig.MAIL_PORT.name" hide-asterisk>
<a-input-number v-model="form.MAIL_PORT" class="input-width" :min="0" />
</a-form-item>
<a-form-item field="MAIL_USERNAME" :label="mailConfig.MAIL_USERNAME.name" hide-asterisk>
<a-input v-model.trim="form.MAIL_USERNAME" class="input-width" />
</a-form-item>
<a-form-item field="MAIL_PASSWORD" :label="mailConfig.MAIL_PASSWORD?.name" hide-asterisk>
<a-input-password v-model.trim="form.MAIL_PASSWORD" class="input-width" />
</a-form-item>
<a-form-item field="MAIL_SSL_ENABLED" :label="mailConfig.MAIL_SSL_ENABLED?.name" hide-asterisk>
<a-switch
v-model="form.MAIL_SSL_ENABLED"
type="round"
:checked-value="1"
:unchecked-value="0"
>
<a-input-number v-model="form.MAIL_SSL_PORT" :min="0" />
</a-form-item>
<a-space>
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon><icon-edit /></template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon><icon-undo /></template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon><icon-save /></template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon><icon-refresh /></template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon><icon-undo /></template>取消
</a-button>
</a-space>
</a-list>
<template #checked>启用</template>
<template #unchecked>禁用</template>
</a-switch>
</a-form-item>
<a-form-item
v-if="form.MAIL_SSL_ENABLED === '1'" field="MAIL_SSL_PORT" :label="mailConfig.MAIL_SSL_PORT.name"
hide-asterisk
>
<a-input-number v-model="form.MAIL_SSL_PORT" class="input-width" :min="0" />
</a-form-item>
<a-space style="margin-bottom: 16px">
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon><icon-edit /></template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon><icon-undo /></template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon><icon-save /></template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon><icon-refresh /></template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon><icon-undo /></template>取消
</a-button>
</a-space>
</a-form>
</a-space>
</a-spin>
</template>
<script setup lang="ts">
@@ -66,13 +75,13 @@ import {
resetOptionValue,
updateOption,
} from '@/apis/system'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
defineOptions({ name: 'MailSetting' })
const { width } = useWindowSize()
const loading = ref<boolean>(false)
const formRef = ref<FormInstance>()
const { form } = useForm({
const [form] = useResetReactive({
MAIL_PROTOCOL: '',
MAIL_HOST: '',
MAIL_PORT: 0,
@@ -176,4 +185,8 @@ onMounted(() => {
:deep(.arco-form-item.arco-form-item-has-help) {
margin-bottom: 5px;
}
.input-width, :deep(.arco-select-view-single) {
width: 220px;
}
</style>

View File

@@ -1,121 +1,125 @@
<template>
<a-space wrap :size="30">
<a-spin :loading="loading">
<a-form
ref="formRef" :model="form" :rules="rules" auto-label-width label-align="left"
:layout="width >= 500 ? 'horizontal' : 'vertical'" :disabled="!isUpdate" scroll-to-first-error
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
label-align="left"
:layout="width >= 500 ? 'horizontal' : 'vertical'"
:disabled="!isUpdate"
scroll-to-first-error
>
<a-list :bordered="false" :loading="loading">
<a-form-item
field="PASSWORD_ERROR_LOCK_COUNT" :label="securityConfig.PASSWORD_ERROR_LOCK_COUNT.name"
:help="securityConfig.PASSWORD_ERROR_LOCK_COUNT.description" hide-asterisk
<a-form-item
field="PASSWORD_ERROR_LOCK_COUNT" :label="securityConfig.PASSWORD_ERROR_LOCK_COUNT.name"
:help="securityConfig.PASSWORD_ERROR_LOCK_COUNT.description" hide-asterisk
>
<a-input-number
v-model="form.PASSWORD_ERROR_LOCK_COUNT" class="input-width" :default-value="0" :precision="0"
:min="0" :max="10"
>
<a-input-number
v-model="form.PASSWORD_ERROR_LOCK_COUNT" class="input-width" :default-value="0" :precision="0"
:min="0" :max="10"
>
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_ERROR_LOCK_MINUTES" :label="securityConfig.PASSWORD_ERROR_LOCK_MINUTES.name"
:help="securityConfig.PASSWORD_ERROR_LOCK_MINUTES.description" hide-asterisk
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_ERROR_LOCK_MINUTES" :label="securityConfig.PASSWORD_ERROR_LOCK_MINUTES.name"
:help="securityConfig.PASSWORD_ERROR_LOCK_MINUTES.description" hide-asterisk
>
<a-input-number
v-model="form.PASSWORD_ERROR_LOCK_MINUTES" class="input-width" :precision="0" :min="1"
:max="1440"
>
<a-input-number
v-model="form.PASSWORD_ERROR_LOCK_MINUTES" class="input-width" :precision="0" :min="1"
:max="1440"
>
<template #append>分钟</template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_EXPIRATION_DAYS" :label="securityConfig.PASSWORD_EXPIRATION_DAYS.name"
:help="securityConfig.PASSWORD_EXPIRATION_DAYS.description" hide-asterisk
<template #append>分钟</template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_EXPIRATION_DAYS" :label="securityConfig.PASSWORD_EXPIRATION_DAYS.name"
:help="securityConfig.PASSWORD_EXPIRATION_DAYS.description" hide-asterisk
>
<a-input-number
v-model="form.PASSWORD_EXPIRATION_DAYS" class="input-width" :precision="0" :min="0"
:max="999"
>
<a-input-number
v-model="form.PASSWORD_EXPIRATION_DAYS" class="input-width" :precision="0" :min="0"
:max="999"
>
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
:label="securityConfig.PASSWORD_EXPIRATION_WARNING_DAYS.name"
field="PASSWORD_EXPIRATION_WARNING_DAYS" :help="securityConfig.PASSWORD_EXPIRATION_WARNING_DAYS.description"
hide-asterisk
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
:label="securityConfig.PASSWORD_EXPIRATION_WARNING_DAYS.name"
field="PASSWORD_EXPIRATION_WARNING_DAYS" :help="securityConfig.PASSWORD_EXPIRATION_WARNING_DAYS.description"
hide-asterisk
>
<a-input-number
v-model="form.PASSWORD_EXPIRATION_WARNING_DAYS" class="input-width" :precision="0" :min="0"
:max="998"
>
<a-input-number
v-model="form.PASSWORD_EXPIRATION_WARNING_DAYS" class="input-width" :precision="0" :min="0"
:max="998"
>
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_REPETITION_TIMES" :label="securityConfig.PASSWORD_REPETITION_TIMES.name"
:help="securityConfig.PASSWORD_REPETITION_TIMES.description" hide-asterisk
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_REPETITION_TIMES" :label="securityConfig.PASSWORD_REPETITION_TIMES.name"
:help="securityConfig.PASSWORD_REPETITION_TIMES.description" hide-asterisk
>
<a-input-number
v-model="form.PASSWORD_REPETITION_TIMES" class="input-width" :precision="0" :min="3"
:max="32"
>
<a-input-number
v-model="form.PASSWORD_REPETITION_TIMES" class="input-width" :precision="0" :min="3"
:max="32"
>
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_MIN_LENGTH" :label="securityConfig.PASSWORD_MIN_LENGTH.name"
:help="securityConfig.PASSWORD_MIN_LENGTH.description" hide-asterisk
>
<a-input-number v-model="form.PASSWORD_MIN_LENGTH" class="input-width" :precision="0" :min="8" :max="32" />
</a-form-item>
<a-form-item
field="PASSWORD_ALLOW_CONTAIN_USERNAME"
:label="securityConfig.PASSWORD_ALLOW_CONTAIN_USERNAME.name"
>
<a-switch v-model="form.PASSWORD_ALLOW_CONTAIN_USERNAME" type="round" :checked-value="1" :unchecked-value="0">
<template #checked></template>
<template #unchecked></template>
</a-switch>
</a-form-item>
<a-form-item field="PASSWORD_REQUIRE_SYMBOLS" :label="securityConfig.PASSWORD_REQUIRE_SYMBOLS.name">
<a-switch v-model="form.PASSWORD_REQUIRE_SYMBOLS" type="round" :checked-value="1" :unchecked-value="0">
<template #checked></template>
<template #unchecked></template>
</a-switch>
</a-form-item>
<a-space>
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon><icon-edit /></template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon><icon-undo /></template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon><icon-save /></template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon><icon-refresh /></template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon><icon-undo /></template>取消
</a-button>
</a-space>
</a-list>
<template #append></template>
</a-input-number>
</a-form-item>
<a-form-item
field="PASSWORD_MIN_LENGTH" :label="securityConfig.PASSWORD_MIN_LENGTH.name"
:help="securityConfig.PASSWORD_MIN_LENGTH.description" hide-asterisk
>
<a-input-number v-model="form.PASSWORD_MIN_LENGTH" class="input-width" :precision="0" :min="8" :max="32" />
</a-form-item>
<a-form-item
field="PASSWORD_ALLOW_CONTAIN_USERNAME"
:label="securityConfig.PASSWORD_ALLOW_CONTAIN_USERNAME.name"
>
<a-switch v-model="form.PASSWORD_ALLOW_CONTAIN_USERNAME" type="round" :checked-value="1" :unchecked-value="0">
<template #checked></template>
<template #unchecked></template>
</a-switch>
</a-form-item>
<a-form-item field="PASSWORD_REQUIRE_SYMBOLS" :label="securityConfig.PASSWORD_REQUIRE_SYMBOLS.name">
<a-switch v-model="form.PASSWORD_REQUIRE_SYMBOLS" type="round" :checked-value="1" :unchecked-value="0">
<template #checked></template>
<template #unchecked></template>
</a-switch>
</a-form-item>
<a-space style="margin-bottom: 16px">
<a-button v-if="!isUpdate" v-permission="['system:config:update']" type="primary" @click="onUpdate">
<template #icon><icon-edit /></template>修改
</a-button>
<a-button v-if="!isUpdate" v-permission="['system:config:reset']" @click="onResetValue">
<template #icon><icon-undo /></template>恢复默认
</a-button>
<a-button v-if="isUpdate" type="primary" @click="handleSave">
<template #icon><icon-save /></template>保存
</a-button>
<a-button v-if="isUpdate" @click="reset">
<template #icon><icon-refresh /></template>重置
</a-button>
<a-button v-if="isUpdate" @click="handleCancel">
<template #icon><icon-undo /></template>取消
</a-button>
</a-space>
</a-form>
</a-space>
</a-spin>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type FormInstance, Message, Modal } from '@arco-design/web-vue'
import { type OptionResp, type SecurityConfig, listOption, resetOptionValue, updateOption } from '@/apis/system'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
defineOptions({ name: 'SecuritySetting' })
const { width } = useWindowSize()
const loading = ref<boolean>(false)
const formRef = ref<FormInstance>()
const { form } = useForm({
const [form] = useResetReactive({
PASSWORD_ERROR_LOCK_COUNT: 0,
PASSWORD_ERROR_LOCK_MINUTES: 0,
PASSWORD_EXPIRATION_DAYS: 0,
@@ -230,11 +234,11 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.input-width {
width: 196px;
}
:deep(.arco-form-item.arco-form-item-has-help) {
margin-bottom: 5px;
}
.input-width {
width: 200px;
}
</style>

View File

@@ -1,36 +1,46 @@
<template>
<div class="gi_page">
<a-card class="general-card" title="系统配置">
<a-tabs v-model:active-key="activeKey" position="right" @change="change">
<a-tab-pane key="1" title="基础配置">
<BasicSetting />
</a-tab-pane>
<a-tab-pane key="2" title="邮件配置">
<MailSetting />
</a-tab-pane>
<a-tab-pane key="3" title="安全配置">
<SecuritySetting />
</a-tab-pane>
</a-tabs>
<!-- <keep-alive>
<component :is="PanMap[activeKey]" />
</keep-alive> -->
</a-card>
<div class="table-page">
<a-row justify="space-between" align="center" class="header page_header">
<a-space wrap>
<div class="title">系统配置</div>
</a-space>
</a-row>
<a-tabs v-model:active-key="activeKey" type="card-gutter" size="large" @change="change">
<a-tab-pane key="1">
<template #title><icon-settings /> 基础配置</template>
</a-tab-pane>
<a-tab-pane key="2">
<template #title><icon-safe /> 安全配置</template>
</a-tab-pane>
<a-tab-pane key="3">
<template #title><icon-email /> 邮件配置</template>
</a-tab-pane>
<a-tab-pane key="4">
<template #title><icon-lock /> 登录配置</template>
</a-tab-pane>
</a-tabs>
<keep-alive>
<component :is="PanMap[activeKey]" />
</keep-alive>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import BasicSetting from './components/BasicSetting.vue'
import MailSetting from './components/MailSetting.vue'
import SecuritySetting from './components/SecuritySetting.vue'
import MailSetting from './components/MailSetting.vue'
import LoginSetting from './components/LoginSetting.vue'
defineOptions({ name: 'SystemConfig' })
// const PanMap: Record<string, Component> = {
// 1: BasicSetting,
// 2: MailSetting,
// 3: SecuritySetting
// }
const PanMap: Record<string, Component> = {
1: BasicSetting,
2: SecuritySetting,
3: MailSetting,
4: LoginSetting,
}
const route = useRoute()
const router = useRouter()
const activeKey = ref('1')
@@ -49,12 +59,34 @@ const change = (key: string | number) => {
}
</script>
<style scoped lang="less">
:deep(.arco-tabs-content) {
padding-top: 5px;
<style scoped lang="scss">
.table-page {
overflow-y: auto;
}
:deep(.arco-tabs .arco-tabs-nav-type-card-gutter .arco-tabs-tab-active) {
box-shadow: inset 0 2px 0 rgb(var(--primary-6)), inset -1px 0 0 var(--color-border-2),
inset 1px 0 0 var(--color-border-2);
position: relative;
}
:deep(.arco-tabs-tab) {
background-color: var(--color-fill-2);
:deep(.arco-tabs-nav-type-card-gutter .arco-tabs-tab) {
border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0;
}
:deep(.arco-tabs-type-card-gutter > .arco-tabs-content) {
border: none;
}
:deep(.arco-tabs-nav::before) {
left: -20px;
right: -20px;
}
:deep(.arco-tabs) {
overflow: visible;
}
:deep(.arco-tabs-nav) {
overflow: visible;
}
</style>

View File

@@ -19,7 +19,7 @@ import { useWindowSize } from '@vueuse/core'
import { mapTree } from 'xe-utils'
import { type DeptResp, addDept, getDept, updateDept } from '@/apis/system/dept'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
interface Props {
depts: DeptResp[]
@@ -55,7 +55,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
sort: 999,
status: 1,
})

View File

@@ -17,7 +17,7 @@
<IconRight v-else />
</template>
<template #toolbar-left>
<a-input v-model="name" placeholder="请输入名称" allow-clear>
<a-input v-model="name" placeholder="搜索名称" allow-clear>
<template #prefix><icon-search /></template>
</a-input>
<a-button @click="reset">

View File

@@ -26,7 +26,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addDictItem, getDictItem, updateDictItem } from '@/apis/system/dict'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -46,7 +46,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
color: 'blue',
sort: 999,
status: 1,

View File

@@ -22,9 +22,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.description" placeholder="请输入标签/描述" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.description" placeholder="搜索标签/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>

View File

@@ -18,7 +18,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addDict, getDict, updateDict } from '@/apis/system/dict'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -37,7 +37,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({})
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
{ label: '名称', field: 'name', type: 'input', rules: [{ required: true, message: '请输入名称' }] },

View File

@@ -1,7 +1,7 @@
<template>
<div class="container">
<div class="search">
<a-input v-model="searchKey" placeholder="请输入关键词" allow-clear>
<a-input v-model="searchKey" placeholder="搜索名称/编码" allow-clear>
<template #prefix><icon-search /></template>
</a-input>
<a-button v-permission="['system:dict:add']" type="primary" @click="onAdd">

View File

@@ -18,7 +18,7 @@
<a-input-group>
<a-input
v-model="queryForm.name" placeholder="请输入文件名" allow-clear style="width: 200px"
v-model="queryForm.name" placeholder="搜索文件名" allow-clear style="width: 200px"
@change="search"
/>
<a-button type="primary" @click="search">

View File

@@ -119,7 +119,7 @@ import { type FormInstance, Message, type TreeNodeData } from '@arco-design/web-
import { useWindowSize } from '@vueuse/core'
import { mapTree } from 'xe-utils'
import { type MenuResp, addMenu, getMenu, updateMenu } from '@/apis/system/menu'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { filterTree, transformPathToName } from '@/utils'
interface Props {
@@ -141,7 +141,7 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改菜单' : '新增菜单'))
const formRef = ref<FormInstance>()
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
type: 1,
sort: 999,
isExternal: false,

View File

@@ -17,7 +17,7 @@
<IconRight v-else />
</template>
<template #toolbar-left>
<a-input v-model="title" placeholder="请输入菜单标题" allow-clear>
<a-input v-model="title" placeholder="搜索菜单标题" allow-clear>
<template #prefix><icon-search /></template>
</a-input>
<a-button @click="reset">

View File

@@ -64,7 +64,7 @@ import { listUserDict } from '@/apis'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import type { LabelValueState } from '@/types/global'
import { useTabsStore } from '@/stores'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
@@ -85,7 +85,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
title: '',
type: '',
effectiveTime: '',

View File

@@ -41,7 +41,7 @@
import AiEditor from './components/index.vue'
import { getNotice } from '@/apis/system/notice'
import { useTabsStore } from '@/stores'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
const route = useRoute()
const router = useRouter()
@@ -49,7 +49,7 @@ const tabsStore = useTabsStore()
const { id } = route.query
const containerRef = ref<HTMLElement | null>()
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
title: '',
createUserString: '',
effectiveTime: '',

View File

@@ -13,9 +13,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.title" placeholder="请输入标题" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
<a-select
v-model="queryForm.type"
:options="notice_type"

View File

@@ -45,7 +45,7 @@
<a-tree
ref="menuTreeRef"
v-model:checked-keys="form.menuIds"
class="w-full"
class="w-full menu-tree"
:data="menuList"
:default-expand-all="isMenuExpanded"
:check-strictly="!form.menuCheckStrictly"
@@ -105,7 +105,7 @@
import { type FormInstance, Message, type TreeNodeData } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addRole } from '@/apis/system/role'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDept, useDict, useMenu } from '@/hooks/app'
const emit = defineEmits<{
@@ -127,7 +127,7 @@ const rules: FormInstance['rules'] = {
dataScope: [{ required: true, message: '请选择数据权限' }],
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
menuCheckStrictly: true,
deptCheckStrictly: true,
sort: 999,
@@ -283,7 +283,17 @@ fieldset legend {
:deep(.arco-form-item-extra) {
width: 100%;
}
:deep(.arco-modal-footer){
margin-top: -20px;
}
.menu-tree{
:deep(.arco-tree-node-is-leaf) {
display: inline-flex;
}
:deep(.arco-tree-node-indent-block){
width: 10px;
}
}
</style>

View File

@@ -15,7 +15,7 @@
<a-input v-model.trim="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="编码" field="code">
<a-input v-model.trim="form.code" placeholder="请输入编码" :disabled="isUpdate" />
<a-input v-model.trim="form.code" placeholder="请输入编码" :disabled="true" />
</a-form-item>
<a-form-item label="排序" field="sort">
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
@@ -41,6 +41,7 @@
<template #extra>
<a-tree
ref="menuTreeRef"
class="menu-tree"
:data="menuList"
:default-expand-all="isMenuExpanded"
:check-strictly="!form.menuCheckStrictly"
@@ -84,7 +85,7 @@
import { type FormInstance, Message, type TreeNodeData } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { getRole, updateRole } from '@/apis/system/role'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDept, useDict, useMenu } from '@/hooks/app'
const emit = defineEmits<{
@@ -106,7 +107,7 @@ const rules: FormInstance['rules'] = {
dataScope: [{ required: true, message: '请选择数据权限' }],
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
menuCheckStrictly: true,
deptCheckStrictly: true,
sort: 999,
@@ -235,4 +236,12 @@ fieldset legend {
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.menu-tree{
:deep(.arco-tree-node-is-leaf) {
display: inline-flex;
}
:deep(.arco-tree-node-indent-block){
width: 10px;
}
}
</style>

View File

@@ -13,9 +13,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.description" placeholder="请输入名称/编码/描述" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/编码/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>

View File

@@ -95,7 +95,7 @@
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addStorage, getStorage, updateStorage } from '@/apis/system/storage'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
import { encryptByRsa } from '@/utils/encrypt'
import { isIPv4 } from '@/utils/validate'
@@ -123,7 +123,7 @@ const rules: FormInstance['rules'] = {
bucketName: [{ required: true, message: '请输入桶名称' }],
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
type: 2,
isDefault: false,
sort: 999,

View File

@@ -13,9 +13,7 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.description" placeholder="请输入名称/编码/描述" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/编码/描述" allow-clear @search="search" />
<a-select
v-model="queryForm.status"
:options="DisEnableStatusList"

View File

@@ -19,7 +19,7 @@ import { addUser, getUser, updateUser } from '@/apis/system/user'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import type { Gender, Status } from '@/types/global'
import { GenderList } from '@/constant/common'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useDept, useRole } from '@/hooks/app'
import { encryptByRsa } from '@/utils/encrypt'
@@ -42,7 +42,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
gender: 1 as Gender,
status: 1 as Status,
})

View File

@@ -94,7 +94,7 @@ import {
importUser,
parseImportUser,
} from '@/apis/system/user'
import { useDownload, useForm } from '@/hooks'
import { useDownload, useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -106,7 +106,7 @@ const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const { form, resetForm } = useForm({
const [form, resetForm] = useResetReactive({
errorPolicy: 1,
duplicateUser: 1,
duplicateEmail: 1,

View File

@@ -18,7 +18,7 @@
import { Message } from '@arco-design/web-vue'
import { resetUserPwd } from '@/apis/system'
import { type Columns, GiForm } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { encryptByRsa } from '@/utils/encrypt'
const emit = defineEmits<{
@@ -34,7 +34,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({})
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
{ label: '密码', field: 'newPassword', type: 'input-password', rules: [{ required: true, message: '请输入密码' }] },

View File

@@ -18,7 +18,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { getUser, updateUserRole } from '@/apis/system'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useResetReactive } from '@/hooks'
import { useRole } from '@/hooks/app'
const emit = defineEmits<{
@@ -36,7 +36,7 @@ const options: Options = {
btns: { hide: true },
}
const { form, resetForm } = useForm({})
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
{

View File

@@ -1,7 +1,7 @@
<template>
<div class="container">
<div class="search">
<a-input v-model="searchKey" placeholder="请输入部门名称" allow-clear>
<a-input v-model="searchKey" placeholder="搜索部门名称" allow-clear>
<template #prefix><icon-search /></template>
</a-input>
</div>

View File

@@ -105,7 +105,7 @@ import { type UserResp, deleteUser, exportUser, listUser } from '@/apis/system/u
import type { Columns, Options } from '@/components/GiForm'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { DisEnableStatusList } from '@/constant/common'
import { useDownload, useForm, useTable } from '@/hooks'
import { useDownload, useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
@@ -116,7 +116,7 @@ const options: Options = reactive({
grid: { cols: { xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 } },
fold: { enable: true, index: 1, defaultCollapsed: true },
})
const { form: queryForm, resetForm } = useForm({
const [queryForm, resetForm] = useResetReactive({
sort: ['t1.id,desc'],
})
const queryFormColumns: Columns = reactive([
@@ -127,7 +127,7 @@ const queryFormColumns: Columns = reactive([
hideLabel: true,
},
props: {
placeholder: '用户名/昵称/描述',
placeholder: '搜索用户名/昵称/描述',
},
},
{