Compare commits

...

15 Commits

Author SHA1 Message Date
1a259a0f0f feat(projects): support theme perset to override component library presets.
(cherry picked from commit 605173a1cc336b609cf08715e4ce69bfa5ad8b50)
2025-12-03 19:21:44 +08:00
1d4201b50b chore(other): remove Prettier's recommendation.
(cherry picked from commit 73e9a0fe0b3e4c87fc8ffcb304506c34de6db508)
2025-12-03 19:21:21 +08:00
0e2e512eda feat-wip(components): 数据字典相关页面代码提交 2025-12-02 00:25:16 +08:00
a10efd3b7e feat-wip(components): 相关页面及路由代码提交 2025-12-01 21:29:29 +08:00
12f3ad79fb feat-wip(components): 相关页面及路由代码提交" 2025-12-01 14:31:42 +08:00
65640c8b71 git commit -m feat(projects): 对接后端登录接口 2025-11-30 11:04:30 +08:00
b3fb87d850 feat(styles): 修改默认主题 2025-11-29 23:47:57 +08:00
c6d97dba21 optimize(projects): simplify some theme preset configurations. 2025-11-29 21:47:39 +08:00
9da847fb6f feat(projects): support theme presets to only set partial content. 2025-11-29 21:47:39 +08:00
c472a94395 docs(projects): add link to ecosystem document. 2025-11-24 20:29:56 +08:00
91a261c1ef style(projects): modify homepage prompt title to tip. 2025-11-24 19:41:43 +08:00
bb232bf884 fix(docs): update project name in ecosystem section of README 2025-11-17 12:18:17 +08:00
f8dc639e05 chore(deps): update deps 2025-11-16 08:55:51 +08:00
46081c365c feat(docs): update QQ group image in README 2025-11-12 22:14:16 +08:00
4a9cf6c3da fix(types): add missing property in theme presets 2025-11-08 20:18:08 +08:00
47 changed files with 1639 additions and 1014 deletions

12
.env
View File

@ -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

View File

@ -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"
}`

View File

@ -5,7 +5,6 @@
"antfu.unocss",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",

View File

@ -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>

View File

@ -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>

View File

@ -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",

View File

@ -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"
},

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>

View 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);

View File

@ -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
};
}

View File

@ -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>

View File

@ -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>

View File

@ -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: {

View File

@ -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: {

View File

@ -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"),
};

View File

@ -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'
}
}
]
}
]
}
];

View File

@ -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"
};
/**

View File

@ -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

View File

@ -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
}
});

View File

@ -1,2 +1,3 @@
export * from './auth';
export * from './route';
export * from './sys/core/dictionary';

View 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
}
});
}

View File

@ -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 || '');
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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
};
});

View File

@ -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;
}

View File

@ -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"
}
}
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -41,7 +41,8 @@
"visible": true,
"cache": true,
"height": 44,
"mode": "chrome"
"mode": "chrome",
"closeTabByMiddleClick": false
},
"fixedHeaderAndTab": true,
"sider": {

View File

@ -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

View File

@ -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[];
}
}

View File

@ -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
View 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
View File

@ -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 */

View File

@ -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']

View File

@ -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"
>;
/**

View File

@ -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>

View File

@ -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 />

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>