mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2026-01-12 05:01:39 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 206b92c2a2 | |||
| 7c509fa737 | |||
| abacb267aa | |||
|
|
43dd512b8a | ||
| 51a2168822 | |||
|
|
4cd892e288 | ||
|
|
3f871e102a | ||
| 21913350e7 | |||
| c2463fc450 | |||
| 99f8edb729 | |||
| 7fe3ffe9da | |||
| 7fa42975cf | |||
| b82ca81b79 | |||
| 7402de5695 | |||
| b030921189 | |||
| 6c45483fae | |||
|
|
be4356fa04 | ||
| 930227ea0c | |||
| 61ef692c83 | |||
| e8941adde4 | |||
| 246d638a8f | |||
|
|
068d959d03 |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -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)
|
||||
|
||||
### ✨ 新特性
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ImageCaptchaResp {
|
||||
uuid: string
|
||||
img: string
|
||||
expireTime: number
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
/** 仪表盘公告类型 */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/** 绑定三方账号信息 */
|
||||
|
||||
1
src/assets/icons/api-management.svg
Normal file
1
src/assets/icons/api-management.svg
Normal 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 |
5
src/assets/icons/arco.svg
Normal file
5
src/assets/icons/arco.svg
Normal 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 |
1
src/assets/icons/code-release-managment.svg
Normal file
1
src/assets/icons/code-release-managment.svg
Normal 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 |
9
src/assets/icons/continew.svg
Normal file
9
src/assets/icons/continew.svg
Normal 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 |
26
src/assets/icons/gitcode.svg
Normal file
26
src/assets/icons/gitcode.svg
Normal 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 |
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
37
src/components/GiIframe/index.vue
Normal file
37
src/components/GiIframe/index.vue
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
15
src/hooks/modules/useResetReactive.ts
Normal file
15
src/hooks/modules/useResetReactive.ts
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
144
src/router/guard.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
@@ -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 属性,否则可能会不能完全重置干净
|
||||
|
||||
@@ -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
132
src/router/route.ts
Normal 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 },
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>({
|
||||
|
||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -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']
|
||||
|
||||
9
src/views/about/document/api/index.vue
Normal file
9
src/views/about/document/api/index.vue
Normal 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>
|
||||
9
src/views/about/document/arco-design-vue/index.vue
Normal file
9
src/views/about/document/arco-design-vue/index.vue
Normal 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>
|
||||
9
src/views/about/document/continew/index.vue
Normal file
9
src/views/about/document/continew/index.vue
Normal 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>
|
||||
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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]" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="请选择状态"
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
148
src/views/system/config/components/LoginSetting.vue
Normal file
148
src/views/system/config/components/LoginSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '请输入名称' }] },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '请输入密码' }] },
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '搜索用户名/昵称/描述',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user