Compare commits
15 Commits
example
...
1a259a0f0f
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a259a0f0f | |||
| 1d4201b50b | |||
| 0e2e512eda | |||
| a10efd3b7e | |||
| 12f3ad79fb | |||
| 65640c8b71 | |||
| b3fb87d850 | |||
| c6d97dba21 | |||
| 9da847fb6f | |||
| c472a94395 | |||
| 91a261c1ef | |||
| bb232bf884 | |||
| f8dc639e05 | |||
| 46081c365c | |||
| 4a9cf6c3da |
12
.env
12
.env
@ -2,9 +2,9 @@
|
||||
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
|
||||
VITE_BASE_URL=/
|
||||
|
||||
VITE_APP_TITLE=SoybeanAdmin
|
||||
VITE_APP_TITLE=Dolphin
|
||||
|
||||
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
|
||||
VITE_APP_DESC='A fresh and elegant admin management system'
|
||||
|
||||
# the prefix of the icon name
|
||||
VITE_ICON_PREFIX=icon
|
||||
@ -29,16 +29,16 @@ VITE_HTTP_PROXY=Y
|
||||
VITE_ROUTER_HISTORY_MODE=history
|
||||
|
||||
# success code of backend service, when the code is received, the request is successful
|
||||
VITE_SERVICE_SUCCESS_CODE=0000
|
||||
VITE_SERVICE_SUCCESS_CODE=00000000
|
||||
|
||||
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
|
||||
VITE_SERVICE_LOGOUT_CODES=8888,8889
|
||||
VITE_SERVICE_LOGOUT_CODES=00010001,00010002
|
||||
|
||||
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
|
||||
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
||||
VITE_SERVICE_MODAL_LOGOUT_CODES=00010004
|
||||
|
||||
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
|
||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333
|
||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=00010003
|
||||
|
||||
# when the route mode is static, the defined super role
|
||||
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# backend service base url, test environment
|
||||
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
||||
VITE_SERVICE_BASE_URL=http://localhost:8080
|
||||
|
||||
# other backend service base url, test environment
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
VITE_OTHER_SERVICE_BASE_URL=`{
|
||||
"demo": "http://localhost:9528"
|
||||
}`
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -5,7 +5,6 @@
|
||||
"antfu.unocss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"lokalise.i18n-ally",
|
||||
"mhutchie.git-graph",
|
||||
"mikestead.dotenv",
|
||||
|
||||
@ -139,7 +139,7 @@ Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) docume
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): SoybeanAdmin based version of React.
|
||||
- [skyroc-admin](https://github.com/Ohh-889/skyroc-admin): SoybeanAdmin's React version implementation.
|
||||
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): A Mock Api management system that helps front-end developers quickly implement interface mocks.
|
||||
- [T-Shell](https://github.com/TheBlindM/T-Shell): A terminal emulator and SSH client with configurable command prompts.
|
||||
- [pea](https://github.com/haitang1894/pea) : Adopting SpringBoot3.2 + JDK21, MyBatis-Plus, SpringSecurity security framework, etc., suitable for the simple permission system developed by [soybean-admin](https://gitee.com/honghuangdc/soybean-admin).
|
||||
@ -151,6 +151,8 @@ Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) docume
|
||||
- [ba](https://github.com/xiatianYa/Ba-Server): Backend service docking with soybean admin based on goFrame framework, adapted to dynamic routing, and interface authentication permissions.
|
||||
- [soybean-admin-go](https://github.com/WgoW/soybean-admin-go):A Go backend service developed based on the Gin and GORM frameworks, integrated with the example branch of Soybean Admin. It supports dynamic routing and API permission authentication.
|
||||
|
||||
More ecosystem please refer to [Ecosystem](https://docs.soybeanjs.cn/awesome) document.
|
||||
|
||||
|
||||
## How to Contribute
|
||||
|
||||
@ -194,7 +196,7 @@ Here are the most active contributors from the past year. Thank you all for your
|
||||
|
||||
<div>
|
||||
<p>QQ Group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-5.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>WeChat Group</p>
|
||||
|
||||
@ -165,7 +165,7 @@ pnpm build
|
||||
|
||||
## 周边生态
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): 基于SoybeanAdmin的React版本.
|
||||
- [skyroc-admin](https://github.com/Ohh-889/skyroc-admin): SoybeanAdmin的React版本实现.
|
||||
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): 一个 Mock Api 管理系统,帮助前端开发伙伴快速实现接口的 mock。
|
||||
- [T-Shell](https://github.com/TheBlindM/T-Shell): 是一个可配置命令提示的终端模拟器和 SSH 客户端。
|
||||
- [pea](https://github.com/haitang1894/pea) : 采用SpringBoot3.2 + JDK21、MyBatis-Plus、SpringSecurity安全框架等,适配 [soybean-admin](https://gitee.com/honghuangdc/soybean-admin) 开发的简单权限系统。
|
||||
@ -177,6 +177,8 @@ pnpm build
|
||||
- [ba](https://github.com/xiatianYa/Ba-Server): 基于goFrame框架开发的后端服务对接soybean-admin,适配动态路由,接口鉴权限。
|
||||
- [soybean-admin-go](https://github.com/WgoW/soybean-admin-go):基于gin+gorm框架开发的go语言后端服务对接soybean-admin的example分支,适配动态路由,接口鉴权限。
|
||||
|
||||
更多周边生态请翻阅 [周边生态](https://docs.soybeanjs.cn/zh/awesome) 文档。
|
||||
|
||||
|
||||
## 如何贡献
|
||||
|
||||
@ -222,7 +224,7 @@ pnpm build
|
||||
|
||||
<div>
|
||||
<p>QQ交流群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-5.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>微信群</p>
|
||||
|
||||
32
package.json
32
package.json
@ -62,45 +62,45 @@
|
||||
"json5": "2.2.3",
|
||||
"naive-ui": "2.43.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.3",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"vue": "3.5.22",
|
||||
"pinia": "3.0.4",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"vue": "3.5.24",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.1.12",
|
||||
"vue-router": "4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.402",
|
||||
"@iconify/json": "2.2.407",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.1",
|
||||
"@types/node": "24.9.2",
|
||||
"@soybeanjs/eslint-config": "1.7.3",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.5.4",
|
||||
"@unocss/preset-icons": "66.5.4",
|
||||
"@unocss/preset-uno": "66.5.4",
|
||||
"@unocss/transformer-directives": "66.5.4",
|
||||
"@unocss/transformer-variant-group": "66.5.4",
|
||||
"@unocss/vite": "66.5.4",
|
||||
"@unocss/eslint-config": "66.5.6",
|
||||
"@unocss/preset-icons": "66.5.6",
|
||||
"@unocss/preset-uno": "66.5.6",
|
||||
"@unocss/transformer-directives": "66.5.6",
|
||||
"@unocss/transformer-variant-group": "66.5.6",
|
||||
"@unocss/vite": "66.5.6",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "5.1.1",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.39.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-plugin-vue": "10.5.1",
|
||||
"kolorist": "1.8.0",
|
||||
"sass": "1.93.3",
|
||||
"sass": "1.94.0",
|
||||
"simple-git-hooks": "2.13.1",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.9.3",
|
||||
"unplugin-icons": "22.5.0",
|
||||
"unplugin-vue-components": "30.0.0",
|
||||
"vite": "7.1.12",
|
||||
"vite": "7.2.2",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "8.0.3",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.2"
|
||||
"vue-tsc": "3.1.4"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.13.1",
|
||||
"axios": "1.13.2",
|
||||
"axios-retry": "4.5.0",
|
||||
"qs": "6.14.0"
|
||||
},
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.25",
|
||||
"bumpp": "10.3.1",
|
||||
"c12": "3.3.1",
|
||||
"c12": "3.3.2",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
|
||||
1441
pnpm-lock.yaml
generated
1441
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ defineOptions({
|
||||
interface Props {
|
||||
itemAlign?: NaiveUI.Align;
|
||||
disabledDelete?: boolean;
|
||||
disabledAdd?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@ -42,7 +43,7 @@ function refresh() {
|
||||
<NSpace :align="itemAlign" wrap justify="end" class="lt-sm:w-200px">
|
||||
<slot name="prefix"></slot>
|
||||
<slot name="default">
|
||||
<NButton size="small" ghost type="primary" @click="add">
|
||||
<NButton size="small" ghost type="primary" :disabled="disabledAdd" @click="add">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
|
||||
8
src/constants/sys/core/dictionary.ts
Normal file
8
src/constants/sys/core/dictionary.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { transformRecordToOption } from '@/utils/common';
|
||||
|
||||
export const dictionaryTypeRecord: Record<Api.Sys.Core.DictionaryType, App.I18n.I18nKey> = {
|
||||
enum: 'page.sys.core.dictionary.options.type.enum',
|
||||
tree: 'page.sys.core.dictionary.options.type.tree'
|
||||
};
|
||||
|
||||
export const dictionaryTypeOptions = transformRecordToOption(dictionaryTypeRecord);
|
||||
@ -230,17 +230,17 @@ export function useTableOperate<TableData>(
|
||||
}
|
||||
|
||||
export function defaultTransform<ApiData>(
|
||||
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
||||
response: FlatResponseData<any, Api.Common.PageResponse<ApiData>>
|
||||
): PaginationData<ApiData> {
|
||||
const { data, error } = response;
|
||||
|
||||
if (!error) {
|
||||
const { records, current, size, total } = data;
|
||||
const { records, pageNumber, pageSize, total } = data;
|
||||
|
||||
return {
|
||||
data: records,
|
||||
pageNum: current,
|
||||
pageSize: size,
|
||||
pageNum: pageNumber,
|
||||
pageSize,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ function handleDropdown(key: DropdownKey) {
|
||||
<div>
|
||||
<ButtonIcon>
|
||||
<SvgIcon icon="ph:user-circle" class="text-icon-large" />
|
||||
<span class="text-16px font-medium">{{ authStore.userInfo.userName }}</span>
|
||||
<span class="text-16px font-medium">{{ authStore.userInfo.username }}</span>
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
</NDropdown>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { defu } from 'defu';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { themeSettings } from '@/theme/settings';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
@ -31,6 +33,8 @@ type ThemePreset = Pick<
|
||||
desc: string;
|
||||
i18nkey?: string;
|
||||
version: string;
|
||||
/** Optional NaiveUI theme overrides */
|
||||
naiveui?: App.Theme.NaiveUIThemeOverride;
|
||||
};
|
||||
|
||||
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
|
||||
@ -76,7 +80,9 @@ const getPresetDesc = (preset: ThemePreset): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
|
||||
const applyPreset = (preset: ThemePreset): void => {
|
||||
const mergedPreset = defu(preset, themeSettings);
|
||||
const { themeScheme, grayscale, colourWeakness, layout, watermark, naiveui, ...rest } = mergedPreset;
|
||||
themeStore.setThemeScheme(themeScheme);
|
||||
themeStore.setGrayscale(grayscale);
|
||||
themeStore.setColourWeakness(colourWeakness);
|
||||
@ -96,6 +102,9 @@ const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark
|
||||
tokens: { ...rest.tokens }
|
||||
});
|
||||
|
||||
// Apply NaiveUI theme overrides if present
|
||||
themeStore.setNaiveThemeOverrides(naiveui);
|
||||
|
||||
window.$message?.success($t('theme.appearance.preset.applySuccess'));
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const local: App.I18n.Schema = {
|
||||
system: {
|
||||
title: 'SoybeanAdmin',
|
||||
title: 'Dolphin',
|
||||
updateTitle: 'System Version Update Notification',
|
||||
updateContent: 'A new version of the system has been detected. Do you want to refresh the page immediately?',
|
||||
updateConfirm: 'Refresh immediately',
|
||||
@ -225,7 +225,12 @@ const local: App.I18n.Schema = {
|
||||
404: 'Page Not Found',
|
||||
500: 'Server Error',
|
||||
'iframe-page': 'Iframe',
|
||||
home: 'Home'
|
||||
home: 'Home',
|
||||
sys: 'System',
|
||||
sys_core: 'Configuration',
|
||||
sys_core_dictionary: 'Dictionary',
|
||||
sys_rbac: 'Organization',
|
||||
sys_rbac_region: 'Region'
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
@ -302,6 +307,36 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
|
||||
},
|
||||
creativity: 'Creativity'
|
||||
},
|
||||
sys: {
|
||||
core: {
|
||||
dictionary: {
|
||||
title: 'Dictionary',
|
||||
fields: {
|
||||
id: 'ID',
|
||||
name: 'Name',
|
||||
code: 'Code',
|
||||
type: 'string',
|
||||
description: 'Description',
|
||||
createTime: 'Create Time',
|
||||
updateTime: 'Update Time'
|
||||
},
|
||||
options: {
|
||||
type: {
|
||||
enum: 'Enum',
|
||||
tree: 'Tree'
|
||||
}
|
||||
},
|
||||
item: {
|
||||
title: 'Item'
|
||||
}
|
||||
}
|
||||
},
|
||||
rbac: {
|
||||
region: {
|
||||
title: 'Region'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
form: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const local: App.I18n.Schema = {
|
||||
system: {
|
||||
title: 'Soybean 管理系统',
|
||||
title: 'Dolphin 管理系统',
|
||||
updateTitle: '系统版本更新通知',
|
||||
updateContent: '检测到系统有新版本发布,是否立即刷新页面?',
|
||||
updateConfirm: '立即刷新',
|
||||
@ -222,7 +222,12 @@ const local: App.I18n.Schema = {
|
||||
404: '页面不存在',
|
||||
500: '服务器错误',
|
||||
'iframe-page': '外链页面',
|
||||
home: '首页'
|
||||
home: '首页',
|
||||
sys: '系统管理',
|
||||
sys_core: '系统配置',
|
||||
sys_core_dictionary: '数据字典',
|
||||
sys_rbac: '组织架构',
|
||||
sys_rbac_region: '行政区划'
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
@ -299,6 +304,36 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
|
||||
},
|
||||
creativity: '创意'
|
||||
},
|
||||
sys: {
|
||||
core: {
|
||||
dictionary: {
|
||||
title: '数据字典',
|
||||
fields: {
|
||||
id: 'ID',
|
||||
name: '名称',
|
||||
code: '代码',
|
||||
type: '类型',
|
||||
description: '描述',
|
||||
createTime: '创建时间',
|
||||
updateTime: '修改时间'
|
||||
},
|
||||
options: {
|
||||
type: {
|
||||
enum: '枚举',
|
||||
tree: '数型'
|
||||
}
|
||||
},
|
||||
item: {
|
||||
title: '字典项'
|
||||
}
|
||||
}
|
||||
},
|
||||
rbac: {
|
||||
region: {
|
||||
title: '行政区划'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
form: {
|
||||
|
||||
@ -21,4 +21,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
||||
login: () => import("@/views/_builtin/login/index.vue"),
|
||||
home: () => import("@/views/home/index.vue"),
|
||||
sys_core_dictionary: () => import("@/views/sys/core/dictionary/index.vue"),
|
||||
sys_rbac_region: () => import("@/views/sys/rbac/region/index.vue"),
|
||||
};
|
||||
|
||||
@ -47,7 +47,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'home',
|
||||
i18nKey: 'route.home',
|
||||
icon: 'mdi:monitor-dashboard',
|
||||
order: 1
|
||||
order: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -74,5 +74,60 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sys',
|
||||
path: '/sys',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'sys',
|
||||
i18nKey: 'route.sys',
|
||||
icon: 'mdi:laptop-windows',
|
||||
order: 20
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'sys_core',
|
||||
path: '/sys/core',
|
||||
meta: {
|
||||
title: 'sys_core',
|
||||
i18nKey: 'route.sys_core',
|
||||
icon: 'ic:round-construction'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'sys_core_dictionary',
|
||||
path: '/sys/core/dictionary',
|
||||
component: 'view.sys_core_dictionary',
|
||||
meta: {
|
||||
title: 'sys_core_dictionary',
|
||||
i18nKey: 'route.sys_core_dictionary',
|
||||
icon: 'ic:round-list-alt'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'sys_rbac',
|
||||
path: '/sys/rbac',
|
||||
meta: {
|
||||
title: 'sys_rbac',
|
||||
i18nKey: 'route.sys_rbac',
|
||||
icon: 'ic:outline-account-tree'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'sys_rbac_region',
|
||||
path: '/sys/rbac/region',
|
||||
component: 'view.sys_rbac_region',
|
||||
meta: {
|
||||
title: 'sys_rbac_region',
|
||||
i18nKey: 'route.sys_rbac_region',
|
||||
icon: 'mdi:home-city-outline'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@ -168,7 +168,12 @@ const routeMap: RouteMap = {
|
||||
"500": "/500",
|
||||
"home": "/home",
|
||||
"iframe-page": "/iframe-page/:url",
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
|
||||
"sys": "/sys",
|
||||
"sys_core": "/sys/core",
|
||||
"sys_core_dictionary": "/sys/core/dictionary",
|
||||
"sys_rbac": "/sys/rbac",
|
||||
"sys_rbac_region": "/sys/rbac/region"
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -35,7 +35,7 @@ export function createRouteGuard(router: Router) {
|
||||
const needLogin = !to.meta.constant;
|
||||
const routeRoles = to.meta.roles || [];
|
||||
|
||||
const hasRole = authStore.userInfo.roles.some(role => routeRoles.includes(role));
|
||||
const hasRole = authStore.userInfo.authorities.some(role => routeRoles.includes(role));
|
||||
const hasAuth = authStore.isStaticSuper || !routeRoles.length || hasRole;
|
||||
|
||||
// if it is login route when logged in, then switch to the root page
|
||||
|
||||
@ -3,15 +3,15 @@ import { request } from '../request';
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param userName User name
|
||||
* @param username User name
|
||||
* @param password Password
|
||||
*/
|
||||
export function fetchLogin(userName: string, password: string) {
|
||||
export function fetchLogin(username: string, password: string) {
|
||||
return request<Api.Auth.LoginToken>({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data: {
|
||||
userName,
|
||||
username,
|
||||
password
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
||||
export * from './sys/core/dictionary';
|
||||
|
||||
45
src/service/api/sys/core/dictionary.ts
Normal file
45
src/service/api/sys/core/dictionary.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { request } from '../../../request';
|
||||
|
||||
export function fetchPageDictionary(pageRequest: Api.Sys.Core.DictionaryQueryPageRequest) {
|
||||
return request<Api.Common.PageResponse<Api.Sys.Core.Dictionary>>({
|
||||
url: '/dictionary/page',
|
||||
method: 'post',
|
||||
data: pageRequest
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDictionaryAdd(dictionaryOp: Api.Sys.Core.DictionaryOp) {
|
||||
return request({
|
||||
url: '/dictionary/add',
|
||||
method: 'post',
|
||||
data: dictionaryOp
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDictionaryEdit(dictionaryOp: Api.Sys.Core.DictionaryOp) {
|
||||
return request({
|
||||
url: '/dictionary/edit',
|
||||
method: 'post',
|
||||
data: dictionaryOp
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDictionaryDelete(id: string) {
|
||||
return request({
|
||||
url: '/dictionary/delete',
|
||||
method: 'post',
|
||||
data: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDictionaryDeleteBatch(ids: string[]) {
|
||||
return request({
|
||||
url: '/dictionary/deleteBatch',
|
||||
method: 'post',
|
||||
data: {
|
||||
ids
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -48,7 +48,7 @@ export const request = createFlatRequest(
|
||||
handleLogout();
|
||||
window.removeEventListener('beforeunload', handleLogout);
|
||||
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.message);
|
||||
}
|
||||
|
||||
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||
@ -60,15 +60,15 @@ export const request = createFlatRequest(
|
||||
|
||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.message)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.message];
|
||||
|
||||
// prevent the user from refreshing the page
|
||||
window.addEventListener('beforeunload', handleLogout);
|
||||
|
||||
window.$dialog?.error({
|
||||
title: $t('common.error'),
|
||||
content: response.data.msg,
|
||||
content: response.data.message,
|
||||
positiveText: $t('common.confirm'),
|
||||
maskClosable: false,
|
||||
closeOnEsc: false,
|
||||
@ -106,7 +106,7 @@ export const request = createFlatRequest(
|
||||
|
||||
// get backend error message and code
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.msg || message;
|
||||
message = error.response?.data?.message || message;
|
||||
backendErrorCode = String(error.response?.data?.code || '');
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ async function handleRefreshToken() {
|
||||
const rToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(rToken);
|
||||
if (!error) {
|
||||
localStg.set('token', data.token);
|
||||
localStg.set('token', data?.accessToken);
|
||||
localStg.set('refreshToken', data.refreshToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -22,9 +22,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const token = ref(getToken());
|
||||
|
||||
const userInfo: Api.Auth.UserInfo = reactive({
|
||||
userId: '',
|
||||
userName: '',
|
||||
roles: [],
|
||||
id: '',
|
||||
username: '',
|
||||
authorities: [],
|
||||
buttons: []
|
||||
});
|
||||
|
||||
@ -32,7 +32,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const isStaticSuper = computed(() => {
|
||||
const { VITE_AUTH_ROUTE_MODE, VITE_STATIC_SUPER_ROLE } = import.meta.env;
|
||||
|
||||
return VITE_AUTH_ROUTE_MODE === 'static' && userInfo.roles.includes(VITE_STATIC_SUPER_ROLE);
|
||||
return VITE_AUTH_ROUTE_MODE === 'static' && userInfo.authorities.includes(VITE_STATIC_SUPER_ROLE);
|
||||
});
|
||||
|
||||
/** Is login */
|
||||
@ -56,12 +56,12 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
|
||||
function recordUserId() {
|
||||
if (!userInfo.userId) {
|
||||
if (!userInfo.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store current user ID locally for next login comparison
|
||||
localStg.set('lastLoginUserId', userInfo.userId);
|
||||
localStg.set('lastLoginUserId', userInfo.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,14 +70,14 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
* @returns {boolean} Whether to clear all tabs
|
||||
*/
|
||||
function checkTabClear(): boolean {
|
||||
if (!userInfo.userId) {
|
||||
if (!userInfo.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastLoginUserId = localStg.get('lastLoginUserId');
|
||||
|
||||
// Clear all tabs if current user is different from previous user
|
||||
if (!lastLoginUserId || lastLoginUserId !== userInfo.userId) {
|
||||
if (!lastLoginUserId || lastLoginUserId !== userInfo.id) {
|
||||
localStg.remove('globalTabs');
|
||||
tabStore.clearTabs();
|
||||
|
||||
@ -117,7 +117,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
window.$notification?.success({
|
||||
title: $t('page.login.common.loginSuccess'),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.username }),
|
||||
duration: 4500
|
||||
});
|
||||
}
|
||||
@ -130,14 +130,14 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
async function loginByToken(loginToken: Api.Auth.LoginToken) {
|
||||
// 1. stored in the localStorage, the later requests need it in headers
|
||||
localStg.set('token', loginToken.token);
|
||||
localStg.set('token', loginToken.accessToken);
|
||||
localStg.set('refreshToken', loginToken.refreshToken);
|
||||
|
||||
// 2. get user info
|
||||
const pass = await getUserInfo();
|
||||
|
||||
if (pass) {
|
||||
token.value = loginToken.token;
|
||||
token.value = loginToken.accessToken;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
/** Init auth route */
|
||||
async function initAuthRoute() {
|
||||
// check if user info is initialized
|
||||
if (!authStore.userInfo.userId) {
|
||||
if (!authStore.userInfo.id) {
|
||||
await authStore.initUserInfo();
|
||||
}
|
||||
|
||||
@ -197,7 +197,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
if (authStore.isStaticSuper) {
|
||||
addAuthRoutes(staticAuthRoutes);
|
||||
} else {
|
||||
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles);
|
||||
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.authorities);
|
||||
|
||||
addAuthRoutes(filteredAuthRoutes);
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
/** Theme settings */
|
||||
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
|
||||
|
||||
/** Optional NaiveUI theme overrides from preset */
|
||||
const naiveThemeOverrides: Ref<App.Theme.NaiveUIThemeOverride | undefined> = ref(undefined);
|
||||
|
||||
/** Watermark time instance with controls */
|
||||
const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
|
||||
|
||||
@ -53,7 +56,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
});
|
||||
|
||||
/** Naive theme */
|
||||
const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value));
|
||||
const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value, naiveThemeOverrides.value));
|
||||
|
||||
/**
|
||||
* Settings json
|
||||
@ -73,8 +76,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
const watermarkContent = computed(() => {
|
||||
const { watermark } = settings.value;
|
||||
|
||||
if (watermark.enableUserName && authStore.userInfo.userName) {
|
||||
return authStore.userInfo.userName;
|
||||
if (watermark.enableUserName && authStore.userInfo.username) {
|
||||
return authStore.userInfo.username;
|
||||
}
|
||||
|
||||
if (watermark.enableTime) {
|
||||
@ -198,6 +201,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set NaiveUI theme overrides
|
||||
*
|
||||
* @param overrides NaiveUI theme overrides or undefined to clear
|
||||
*/
|
||||
function setNaiveThemeOverrides(overrides?: App.Theme.NaiveUIThemeOverride) {
|
||||
naiveThemeOverrides.value = overrides;
|
||||
}
|
||||
|
||||
/** Only run timer when watermark is visible and time display is enabled */
|
||||
function updateWatermarkTimer() {
|
||||
const { watermark } = settings.value;
|
||||
@ -284,6 +296,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
updateThemeColors,
|
||||
setThemeLayout,
|
||||
setWatermarkEnableUserName,
|
||||
setWatermarkEnableTime
|
||||
setWatermarkEnableTime,
|
||||
setNaiveThemeOverrides
|
||||
};
|
||||
});
|
||||
|
||||
@ -236,11 +236,15 @@ function getNaiveThemeColors(colors: App.Theme.ThemeColor, recommended = false)
|
||||
/**
|
||||
* Get naive theme
|
||||
*
|
||||
* @param settings Theme settings object.
|
||||
* @param settings.recommendColor Whether to use recommended color palette.
|
||||
* @param settings.themeRadius Border radius to use in the theme (in px).
|
||||
* @param colors Theme colors
|
||||
* @param settings Theme settings object
|
||||
* @param overrides Optional manual overrides from preset
|
||||
*/
|
||||
export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.ThemeSetting) {
|
||||
export function getNaiveTheme(
|
||||
colors: App.Theme.ThemeColor,
|
||||
settings: App.Theme.ThemeSetting,
|
||||
overrides?: GlobalThemeOverrides
|
||||
) {
|
||||
const { primary: colorLoading } = colors;
|
||||
|
||||
const theme: GlobalThemeOverrides = {
|
||||
@ -256,5 +260,7 @@ export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.
|
||||
}
|
||||
};
|
||||
|
||||
return theme;
|
||||
// If there are overrides, merge them with priority
|
||||
// overrides has higher priority than auto-generated theme
|
||||
return overrides ? defu(overrides, theme) : theme;
|
||||
}
|
||||
|
||||
@ -2,10 +2,8 @@
|
||||
"name": "Azir's Preset",
|
||||
"desc": "It is a cold and elegant preset that Azir likes",
|
||||
"i18nkey": "theme.appearance.preset.azir",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"themeScheme": "light",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": true,
|
||||
"themeColor": "#78a878",
|
||||
"otherColor": {
|
||||
@ -14,57 +12,7 @@
|
||||
"warning": "#d4bb9d",
|
||||
"error": "#c49a9a"
|
||||
},
|
||||
"themeRadius": 6,
|
||||
"isInfoFollowPrimary": true,
|
||||
"layout": {
|
||||
"mode": "vertical-mix",
|
||||
"scrollMode": "wrapper"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "zoom-fade"
|
||||
},
|
||||
"header": {
|
||||
"height": 64,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": true
|
||||
},
|
||||
"globalSearch": {
|
||||
"visible": true
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 48,
|
||||
"mode": "chrome"
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": false,
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 90,
|
||||
"mixCollapsedWidth": 64,
|
||||
"mixChildMenuWidth": 200
|
||||
},
|
||||
"footer": {
|
||||
"visible": true,
|
||||
"fixed": true,
|
||||
"height": 56,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": true,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm:ss"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
@ -86,5 +34,19 @@
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"naiveui": {
|
||||
"Alert": {
|
||||
"borderRadiusMedium": "12px",
|
||||
"fontWeightStrong": "600",
|
||||
"paddingMedium": "0 20px"
|
||||
},
|
||||
"Card": {
|
||||
"borderRadius": "16px",
|
||||
"paddingMedium": "24px"
|
||||
},
|
||||
"Input": {
|
||||
"borderRadius": "10px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,33 +2,12 @@
|
||||
"name": "Compact Preset",
|
||||
"desc": "Compact layout preset for small screens",
|
||||
"i18nkey": "theme.appearance.preset.compact",
|
||||
"version": "1.0.0",
|
||||
"themeScheme": "light",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": false,
|
||||
"themeColor": "#646cff",
|
||||
"otherColor": {
|
||||
"info": "#2080f0",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"error": "#f5222d"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"themeRadius": 6,
|
||||
"isInfoFollowPrimary": true,
|
||||
"layout": {
|
||||
"mode": "vertical",
|
||||
"scrollMode": "content"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "fade-slide"
|
||||
},
|
||||
"header": {
|
||||
"height": 48,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
"visible": false
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": false
|
||||
@ -41,9 +20,9 @@
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 36,
|
||||
"mode": "button"
|
||||
"mode": "button",
|
||||
"closeTabByMiddleClick": false
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": false,
|
||||
"width": 180,
|
||||
@ -53,38 +32,6 @@
|
||||
"mixChildMenuWidth": 180
|
||||
},
|
||||
"footer": {
|
||||
"visible": false,
|
||||
"fixed": false,
|
||||
"height": 40,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": false,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
"container": "rgb(255, 255, 255)",
|
||||
"layout": "rgb(247, 250, 252)",
|
||||
"inverted": "rgb(0, 20, 40)",
|
||||
"base-text": "rgb(31, 31, 31)"
|
||||
},
|
||||
"boxShadow": {
|
||||
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"colors": {
|
||||
"container": "rgb(28, 28, 28)",
|
||||
"layout": "rgb(18, 18, 18)",
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
"visible": false
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
"name": "Dark Preset",
|
||||
"desc": "Dark theme preset for night time usage",
|
||||
"i18nkey": "theme.appearance.preset.dark",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"themeScheme": "dark",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": false,
|
||||
"themeColor": "#409eff",
|
||||
"themeColor": "#646cff",
|
||||
"otherColor": {
|
||||
"info": "#2080f0",
|
||||
"success": "#52c41a",
|
||||
@ -41,11 +41,12 @@
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 44,
|
||||
"mode": "chrome"
|
||||
"mode": "chrome",
|
||||
"closeTabByMiddleClick": false
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": true,
|
||||
"inverted": false,
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 90,
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 44,
|
||||
"mode": "chrome"
|
||||
"mode": "chrome",
|
||||
"closeTabByMiddleClick": false
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
|
||||
@ -4,7 +4,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
grayscale: false,
|
||||
colourWeakness: false,
|
||||
recommendColor: false,
|
||||
themeColor: '#646cff',
|
||||
themeColor: '#ec4899',
|
||||
themeRadius: 6,
|
||||
otherColor: {
|
||||
info: '#2080f0',
|
||||
@ -31,7 +31,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
visible: true
|
||||
},
|
||||
globalSearch: {
|
||||
visible: true
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
@ -51,7 +51,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
mixChildMenuWidth: 200
|
||||
},
|
||||
footer: {
|
||||
visible: true,
|
||||
visible: false,
|
||||
fixed: false,
|
||||
height: 48,
|
||||
right: true
|
||||
|
||||
8
src/typings/api/auth.d.ts
vendored
8
src/typings/api/auth.d.ts
vendored
@ -6,14 +6,14 @@ declare namespace Api {
|
||||
*/
|
||||
namespace Auth {
|
||||
interface LoginToken {
|
||||
token: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
userName: string;
|
||||
roles: string[];
|
||||
id: string;
|
||||
username: string;
|
||||
authorities: string[];
|
||||
buttons: string[];
|
||||
}
|
||||
}
|
||||
|
||||
51
src/typings/api/common.d.ts
vendored
51
src/typings/api/common.d.ts
vendored
@ -5,46 +5,23 @@
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Common {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** current page number */
|
||||
current: number;
|
||||
/** page size */
|
||||
size: number;
|
||||
/** total count */
|
||||
total: number;
|
||||
/** 分页请求 */
|
||||
interface PageRequest {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
/** 带查询参数的分页请求 */
|
||||
interface QueryPageRequest<T> extends PageRequest {
|
||||
query: T;
|
||||
}
|
||||
|
||||
/** 分页响应 */
|
||||
interface PageResponse<T> {
|
||||
total: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
records: T[];
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2';
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
id: number;
|
||||
/** record creator */
|
||||
createBy: string;
|
||||
/** record create time */
|
||||
createTime: string;
|
||||
/** record updater */
|
||||
updateBy: string;
|
||||
/** record update time */
|
||||
updateTime: string;
|
||||
/** record status */
|
||||
status: EnableStatus | null;
|
||||
} & T;
|
||||
}
|
||||
}
|
||||
|
||||
40
src/typings/api/sys/core.d.ts
vendored
Normal file
40
src/typings/api/sys/core.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
declare namespace Api {
|
||||
namespace Sys {
|
||||
namespace Core {
|
||||
// ******************** sys_core_dictionary ********************
|
||||
type DictionaryType = 'enum' | 'tree';
|
||||
interface Dictionary {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: DictionaryType;
|
||||
description: string | null;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
interface DictionaryQuery {
|
||||
name: string | null;
|
||||
code: string | null;
|
||||
type: string | null;
|
||||
}
|
||||
type DictionaryQueryPageRequest = Api.Common.QueryPageRequest<DictionaryQuery>;
|
||||
interface DictionaryOp {
|
||||
id: string | null;
|
||||
name: string;
|
||||
code: string;
|
||||
type: DictionaryType;
|
||||
description: string | null;
|
||||
}
|
||||
interface DictionaryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
sort: number;
|
||||
description: string | null;
|
||||
createTime: string | null;
|
||||
updateTime: string | null;
|
||||
children: DictionaryItem[];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/typings/app.d.ts
vendored
37
src/typings/app.d.ts
vendored
@ -4,6 +4,9 @@ declare namespace App {
|
||||
namespace Theme {
|
||||
type ColorPaletteNumber = import('@sa/color').ColorPaletteNumber;
|
||||
|
||||
/** NaiveUI theme overrides that can be specified in preset */
|
||||
type NaiveUIThemeOverride = import('naive-ui').GlobalThemeOverrides;
|
||||
|
||||
/** Theme setting */
|
||||
interface ThemeSetting {
|
||||
/** Theme scheme */
|
||||
@ -546,6 +549,36 @@ declare namespace App {
|
||||
};
|
||||
creativity: string;
|
||||
};
|
||||
sys: {
|
||||
core: {
|
||||
dictionary: {
|
||||
title: string;
|
||||
fields: {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
description: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
options: {
|
||||
type: {
|
||||
enum: string;
|
||||
tree: string;
|
||||
};
|
||||
};
|
||||
item: {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
rbac: {
|
||||
region: {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
form: {
|
||||
required: string;
|
||||
@ -628,9 +661,11 @@ declare namespace App {
|
||||
/** The backend service response code */
|
||||
code: string;
|
||||
/** The backend service response message */
|
||||
msg: string;
|
||||
message: string;
|
||||
/** The backend service response data */
|
||||
data: T;
|
||||
/** The backend service response timestamp */
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
/** The demo backend service response data */
|
||||
|
||||
28
src/typings/components.d.ts
vendored
28
src/typings/components.d.ts
vendored
@ -21,14 +21,21 @@ declare module 'vue' {
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconLocalLogo: typeof import('~icons/local/logo')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
@ -41,7 +48,10 @@ declare module 'vue' {
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@ -50,6 +60,7 @@ declare module 'vue' {
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
@ -62,7 +73,10 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
@ -98,14 +112,21 @@ declare global {
|
||||
const FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
const IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
const IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
const IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
const IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
const IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
const IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
const IconLocalLogo: typeof import('~icons/local/logo')['default']
|
||||
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
const IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
const IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
@ -118,7 +139,10 @@ declare global {
|
||||
const NButton: typeof import('naive-ui')['NButton']
|
||||
const NCard: typeof import('naive-ui')['NCard']
|
||||
const NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
const NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
const NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
const NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
const NDivider: typeof import('naive-ui')['NDivider']
|
||||
const NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@ -127,6 +151,7 @@ declare global {
|
||||
const NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
const NForm: typeof import('naive-ui')['NForm']
|
||||
const NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
const NGi: typeof import('naive-ui')['NGi']
|
||||
const NGrid: typeof import('naive-ui')['NGrid']
|
||||
const NInput: typeof import('naive-ui')['NInput']
|
||||
@ -139,7 +164,10 @@ declare global {
|
||||
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
const NModal: typeof import('naive-ui')['NModal']
|
||||
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
const NPopover: typeof import('naive-ui')['NPopover']
|
||||
const NRadio: typeof import('naive-ui')['NRadio']
|
||||
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
const NSelect: typeof import('naive-ui')['NSelect']
|
||||
const NSpace: typeof import('naive-ui')['NSpace']
|
||||
|
||||
8
src/typings/elegant-router.d.ts
vendored
8
src/typings/elegant-router.d.ts
vendored
@ -23,6 +23,11 @@ declare module "@elegant-router/types" {
|
||||
"home": "/home";
|
||||
"iframe-page": "/iframe-page/:url";
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
|
||||
"sys": "/sys";
|
||||
"sys_core": "/sys/core";
|
||||
"sys_core_dictionary": "/sys/core/dictionary";
|
||||
"sys_rbac": "/sys/rbac";
|
||||
"sys_rbac_region": "/sys/rbac/region";
|
||||
};
|
||||
|
||||
/**
|
||||
@ -60,6 +65,7 @@ declare module "@elegant-router/types" {
|
||||
| "home"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "sys"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -82,6 +88,8 @@ declare module "@elegant-router/types" {
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "home"
|
||||
| "sys_core_dictionary"
|
||||
| "sys_rbac_region"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
// import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
@ -20,7 +20,7 @@ interface FormModel {
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
userName: 'Soybean',
|
||||
userName: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
@ -39,8 +39,10 @@ async function handleSubmit() {
|
||||
await authStore.login(model.userName, model.password);
|
||||
}
|
||||
|
||||
/*
|
||||
type AccountKey = 'super' | 'admin' | 'user';
|
||||
|
||||
|
||||
interface Account {
|
||||
key: AccountKey;
|
||||
label: string;
|
||||
@ -48,6 +50,7 @@ interface Account {
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
@ -68,10 +71,13 @@ const accounts = computed<Account[]>(() => [
|
||||
password: '123456'
|
||||
}
|
||||
]);
|
||||
*/
|
||||
|
||||
/*
|
||||
async function handleAccountLogin(account: Account) {
|
||||
await authStore.login(account.userName, account.password);
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -97,6 +103,7 @@ async function handleAccountLogin(account: Account) {
|
||||
<NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<!--
|
||||
<div class="flex-y-center justify-between gap-12px">
|
||||
<NButton class="flex-1" block @click="toggleLoginModule('code-login')">
|
||||
{{ $t(loginModuleRecord['code-login']) }}
|
||||
@ -111,6 +118,7 @@ async function handleAccountLogin(account: Account) {
|
||||
{{ item.label }}
|
||||
</NButton>
|
||||
</div>
|
||||
-->
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
@ -15,7 +15,7 @@ const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NAlert :title="$t('common.warning')" type="warning">
|
||||
<NAlert :title="$t('common.tip')" type="warning">
|
||||
{{ $t('page.home.branchDesc') }}
|
||||
</NAlert>
|
||||
<HeaderBanner />
|
||||
|
||||
@ -48,7 +48,7 @@ const statisticData = computed<StatisticData[]>(() => [
|
||||
</div>
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">
|
||||
{{ $t('page.home.greeting', { userName: authStore.userInfo.userName }) }}
|
||||
{{ $t('page.home.greeting', { userName: authStore.userInfo.username }) }}
|
||||
</h3>
|
||||
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
|
||||
</div>
|
||||
|
||||
167
src/views/sys/core/dictionary/index.vue
Normal file
167
src/views/sys/core/dictionary/index.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { dictionaryTypeRecord } from '@/constants/sys/core/dictionary';
|
||||
import { fetchDictionaryDelete, fetchDictionaryDeleteBatch, fetchPageDictionary } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import DictionarySearch from '@/views/sys/core/dictionary/modules/dictionary-search.vue';
|
||||
import DictionaryOperateDrawer from '@/views/sys/core/dictionary/modules/dictionary-operate-drawer.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const pageRequest: Api.Sys.Core.DictionaryQueryPageRequest = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
query: {
|
||||
name: null,
|
||||
code: null,
|
||||
type: null
|
||||
}
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useNaivePaginatedTable({
|
||||
api: () => fetchPageDictionary(pageRequest),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
pageRequest.pageNumber = params.page || 1;
|
||||
pageRequest.pageSize = params.pageSize || 10;
|
||||
},
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: $t('common.index'),
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (_, index) => index + 1
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: $t('page.sys.core.dictionary.fields.name'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
title: $t('page.sys.core.dictionary.fields.code'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: $t('page.sys.core.dictionary.fields.type'),
|
||||
width: 100,
|
||||
render: row => {
|
||||
if (row.type === null) {
|
||||
return null;
|
||||
}
|
||||
return <NTag type="success">{$t(dictionaryTypeRecord[row.type])}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
title: $t('page.sys.core.dictionary.fields.createTime'),
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'updateTime',
|
||||
title: $t('page.sys.core.dictionary.fields.updateTime'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
render: row => (
|
||||
<div class="flex-center gap-8px">
|
||||
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
|
||||
{$t('common.edit')}
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
|
||||
{{
|
||||
default: () => $t('common.confirmDelete'),
|
||||
trigger: () => (
|
||||
<NButton type="error" ghost size="small">
|
||||
{$t('common.delete')}
|
||||
</NButton>
|
||||
)
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||
useTableOperate(data, 'id', getData);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
fetchDictionaryDeleteBatch(checkedRowKeys.value).then(() => {
|
||||
onBatchDeleted();
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(id: string) {
|
||||
fetchDictionaryDelete(id).then(() => {
|
||||
onDeleted();
|
||||
});
|
||||
}
|
||||
|
||||
function edit(id: string) {
|
||||
handleEdit(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<DictionarySearch v-model:model="pageRequest" @search="getDataByPage" />
|
||||
<NCard
|
||||
:title="$t('page.sys.core.dictionary.title')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="card-wrapper sm:flex-1-hidden"
|
||||
>
|
||||
<template #header-extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="702"
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.id"
|
||||
:pagination="mobilePagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<DictionaryOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,233 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { dictionaryTypeOptions } from '@/constants/sys/core/dictionary';
|
||||
import { fetchDictionaryAdd, fetchDictionaryEdit } from '@/service/api';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'DictionaryOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: NaiveUI.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.Sys.Core.Dictionary | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: $t('common.add'),
|
||||
edit: $t('common.edit')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Pick<Api.Sys.Core.Dictionary, 'name' | 'code' | 'type' | 'description'>;
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'enum',
|
||||
description: null
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Exclude<keyof Model, 'description'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
name: defaultRequiredRule,
|
||||
code: defaultRequiredRule,
|
||||
type: defaultRequiredRule
|
||||
};
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model.value, jsonClone(props.rowData));
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const isEdit = props.operateType === 'edit';
|
||||
const opFunc = isEdit ? fetchDictionaryEdit : fetchDictionaryAdd;
|
||||
const opData: Api.Sys.Core.DictionaryOp = {
|
||||
id: props.rowData?.id || null,
|
||||
...model.value
|
||||
};
|
||||
opFunc(opData).then(() => {
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddDictionaryItem() {}
|
||||
|
||||
const dictionaryItemData: Api.Sys.Core.DictionaryItem[] = [
|
||||
{
|
||||
id: '07',
|
||||
name: '07akioni',
|
||||
code: '07akioni',
|
||||
sort: 0,
|
||||
description: null,
|
||||
createTime: null,
|
||||
updateTime: null,
|
||||
children: [
|
||||
{
|
||||
id: '08',
|
||||
name: '08akioni',
|
||||
code: '08akioni',
|
||||
sort: 0,
|
||||
description: null,
|
||||
createTime: null,
|
||||
updateTime: null,
|
||||
children: [
|
||||
{
|
||||
id: '09',
|
||||
name: '09akioni',
|
||||
code: '09akioni',
|
||||
children: [],
|
||||
sort: 0,
|
||||
description: null,
|
||||
createTime: null,
|
||||
updateTime: null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: '11akioni',
|
||||
code: '11akioni',
|
||||
children: [],
|
||||
sort: 0,
|
||||
description: null,
|
||||
createTime: null,
|
||||
updateTime: null
|
||||
}
|
||||
];
|
||||
|
||||
const newDictionaryItemData = ref<Api.Sys.Core.DictionaryItem[]>([]);
|
||||
|
||||
const dictionaryItemColumns: DataTableColumns<Api.Sys.Core.DictionaryItem> = [
|
||||
{
|
||||
type: 'selection'
|
||||
},
|
||||
{
|
||||
title: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'index',
|
||||
key: 'index'
|
||||
}
|
||||
];
|
||||
|
||||
function rowKey(row: Api.Sys.Core.DictionaryItem) {
|
||||
return row.id;
|
||||
}
|
||||
|
||||
function handleUpdateChecked() {
|
||||
const dictionaryType = model.value.type;
|
||||
if (!dictionaryType) {
|
||||
return;
|
||||
}
|
||||
newDictionaryItemData.value = cloneDeep(dictionaryItemData);
|
||||
if (dictionaryType === 'enum') {
|
||||
for (const newDictionaryItemDatum of newDictionaryItemData.value) {
|
||||
newDictionaryItemDatum.children = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
handleUpdateChecked();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :default-width="600" resizable>
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.sys.core.dictionary.fields.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.sys.core.dictionary.fields.name')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.sys.core.dictionary.fields.code')" path="code">
|
||||
<NInput v-model:value="model.code" :placeholder="$t('page.sys.core.dictionary.fields.code')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.sys.core.dictionary.fields.type')" path="type">
|
||||
<NRadioGroup v-model:value="model.type" @change="handleUpdateChecked">
|
||||
<NRadio
|
||||
v-for="item in dictionaryTypeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="$t(item.label)"
|
||||
/>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.sys.core.dictionary.fields.description')" path="description">
|
||||
<NInput
|
||||
v-model:value="model.description"
|
||||
:placeholder="$t('page.sys.core.dictionary.fields.description')"
|
||||
type="textarea"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NCard :title="$t('page.sys.core.dictionary.item.title')" hoverable embedded>
|
||||
<template #header-extra>
|
||||
<NSpace wrap justify="end" class="lt-sm:w-200px">
|
||||
<NButton size="small" ghost type="primary" @click="handleAddDictionaryItem">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.add') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NCard>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NDataTable :columns="dictionaryItemColumns" :data="newDictionaryItemData" :row-key="rowKey" />
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
89
src/views/sys/core/dictionary/modules/dictionary-search.vue
Normal file
89
src/views/sys/core/dictionary/modules/dictionary-search.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { toRaw } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { dictionaryTypeOptions } from '@/constants/sys/core/dictionary';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'DictionarySearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.Sys.Core.DictionaryQueryPageRequest>('model', { required: true });
|
||||
|
||||
const defaultModel = jsonClone(toRaw(model.value));
|
||||
|
||||
function resetModel() {
|
||||
Object.assign(model.value, defaultModel);
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||
<NCollapse :default-expanded-names="['role-search']">
|
||||
<NCollapseItem :title="$t('common.search')" name="role-search">
|
||||
<NForm :model="model" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:6"
|
||||
:label="$t('page.sys.core.dictionary.fields.name')"
|
||||
path="name"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NInput v-model:value="model.query.name" :placeholder="$t('page.sys.core.dictionary.fields.name')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:6"
|
||||
:label="$t('page.sys.core.dictionary.fields.code')"
|
||||
path="code"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NInput v-model:value="model.query.code" :placeholder="$t('page.sys.core.dictionary.fields.code')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:6"
|
||||
:label="$t('page.sys.core.dictionary.fields.type')"
|
||||
path="type"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="model.query.type"
|
||||
:placeholder="$t('page.sys.core.dictionary.fields.type')"
|
||||
:options="translateOptions(dictionaryTypeOptions)"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="resetModel">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
<NButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
16
src/views/sys/rbac/region/index.vue
Normal file
16
src/views/sys/rbac/region/index.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<NCard
|
||||
:title="$t('page.sys.rbac.region.title')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="card-wrapper sm:flex-1-hidden"
|
||||
></NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user