20 Commits

Author SHA1 Message Date
34ab7d5da2 fix(projects): 修复菜单默认图标问题 2025-09-11 11:45:52 +08:00
513dc31eaa feat(styles): 优化左侧树形结构样式 2025-09-11 10:28:43 +08:00
dc2fbbd556 fix(projects): 修复退出登录未清空消息列表问题 2025-09-11 10:21:11 +08:00
56fd5434ca optimize(projects): 字典状态使用枚举值 2025-09-11 10:16:20 +08:00
ad207255bb fix(projects): 修复菜单弹窗打开未清空默认值问题 2025-09-11 10:14:45 +08:00
3146c039f0 feat(projects): 用户列表新增头像展示 2025-09-11 10:09:35 +08:00
AN
d5bbc37dec fix(projects): 修复新增部门时不显示上级部门问题 2025-09-10 11:48:13 +08:00
AN
2f794c4b73 fix(projects): 修改代码生成功能模块名为驼峰时,路由错误问题 2025-09-10 10:40:23 +08:00
AN
378aa869bf style(components): 修改json预览组件样式问题 2025-09-08 13:51:28 +08:00
AN
4a4244b5c4 style(styles): 修复字体样式导致下划线不可见问题 2025-09-05 17:20:55 +08:00
AN
ecad1c3e78 optimize(components): 补充国际化 2025-09-05 14:31:23 +08:00
9ef0bd416e fix(utils): 修复请求工具响应解密问题 2025-09-04 14:57:12 +08:00
25ee32074a feat(projects): 路由兼容 activeMenu 选项 2025-09-02 15:10:59 +08:00
8412a8db16 feat(projects): 重构登录页面样式 2025-09-02 14:49:58 +08:00
e230b0da81 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	build/config/proxy.ts
#	package.json
#	pnpm-lock.yaml
2025-09-01 15:06:44 +08:00
12b25e0d58 fix(types): fix proxy types 2025-08-28 00:26:29 +08:00
e33f944a74 chore(deps): update deps 2025-08-28 00:26:12 +08:00
7f2f3bd088 feat(utils): 新增本地 Excel 导出工具类 2025-08-18 16:14:29 +08:00
5ef1c5de98 fix(hooks): 修复下载 hooks 错误未处理 2025-08-18 15:24:25 +08:00
90a14e338a fix(components): 修复字典标签会修改字典数据值问题 2025-08-18 14:10:12 +08:00
100 changed files with 2117 additions and 13189 deletions

View File

@ -15,6 +15,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -31,7 +31,8 @@
"vue.server.hybridMode": true,
"files.exclude": { "/docs": true },
"search.exclude": {
"/docs": true
"/docs": true,
"**/dist/**": true
},
"cSpell.words": ["Axios", "tinymce"]
}

View File

@ -1,4 +1,4 @@
import type { HttpProxy, ProxyOptions } from 'vite';
import type { ProxyOptions } from 'vite';
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
import { consola } from 'consola';
import { createServiceConfig } from '../../src/utils/service';
@ -34,7 +34,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
target: item.baseURL,
changeOrigin: true,
ws: item.ws,
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
configure: (_proxy, options) => {
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
if (!enableLog) return;

View File

@ -10,6 +10,7 @@ import org.apache.velocity.VelocityContext;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.generator.constant.GenConstants;
import org.dromara.generator.domain.GenTable;
@ -58,7 +59,7 @@ public class VelocityUtils {
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName());
velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
velocityContext.put("businessName", genTable.getBusinessName());
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
@ -124,11 +125,12 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
if (DataBaseHelper.isOracle()) {
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm");
} else if (DataBaseHelper.isPostgerSql()) {
} else if (dataBaseType.isPostgreSql()) {
templates.add("vm/sql/postgres/sql.vm");
} else if (DataBaseHelper.isSqlServer()) {
} else if (dataBaseType.isSqlServer()) {
templates.add("vm/sql/sqlserver/sql.vm");
} else {
templates.add("vm/sql/sql.vm");
@ -163,7 +165,7 @@ public class VelocityUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String soybeanPath = "soy";
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
if (template.contains("domain.java.vm")) {
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
}
@ -186,17 +188,17 @@ public class VelocityUtils {
} else if (template.contains("sql.vm")) {
fileName = businessName + "Menu.sql";
} else if (template.contains("index.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("search.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
}
return fileName;
}

View File

@ -41,21 +41,18 @@
"scripts": {
"build": "vite build --mode prod",
"build:dev": "vite build --mode dev",
"build:tauri": "pnpm tauri build",
"build:test": "vite build --mode test",
"cleanup": "sa cleanup",
"commit": "sa git-commit",
"commit:zh": "sa git-commit -l=zh-cn",
"dev": "vite --mode dev",
"dev:prod": "vite --mode prod",
"dev:tauri": "pnpm tauri dev",
"dev:test": "vite --mode test",
"gen-route": "sa gen-route",
"lint": "eslint . --fix",
"prepare": "simple-git-hooks",
"preview": "vite preview",
"release": "sa release",
"tauri-icon": "pnpm tauri icon ./public/logo.png",
"typecheck": "vue-tsc --noEmit --skipLibCheck",
"update-pkg": "sa update-pkg"
},
@ -68,13 +65,12 @@
"@sa/materials": "workspace:*",
"@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*",
"@tauri-apps/api": "2.5.0",
"@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.5.0",
"@vueuse/core": "13.8.0",
"clipboard": "2.0.11",
"dayjs": "1.11.13",
"dayjs": "1.11.14",
"defu": "6.1.4",
"echarts": "5.6.0",
"echarts": "6.0.0",
"highlight.js": "^11.11.1",
"jsencrypt": "^3.3.2",
"json5": "2.2.3",
@ -84,47 +80,47 @@
"pinia": "3.0.3",
"streamsaver": "^2.0.6",
"tailwind-merge": "3.3.1",
"vue": "3.5.17",
"vue": "3.5.20",
"vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.9",
"vue-router": "4.5.1"
"vue-i18n": "11.1.11",
"vue-router": "4.5.1",
"xlsx": "0.18.5"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.357",
"@iconify/json": "2.2.378",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1",
"@tauri-apps/cli": "2.5.0",
"@types/node": "24.0.13",
"@types/node": "24.3.0",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.3",
"@unocss/preset-uno": "66.3.3",
"@unocss/transformer-directives": "66.3.3",
"@unocss/transformer-variant-group": "66.3.3",
"@unocss/vite": "66.3.3",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.1",
"@unocss/eslint-config": "66.4.2",
"@unocss/preset-icons": "66.4.2",
"@unocss/preset-uno": "66.4.2",
"@unocss/transformer-directives": "66.4.2",
"@unocss/transformer-variant-group": "66.4.2",
"@unocss/vite": "66.4.2",
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.1.0",
"consola": "3.4.2",
"eslint": "9.31.0",
"eslint-plugin-vue": "10.3.0",
"eslint": "9.34.0",
"eslint-plugin-vue": "10.4.0",
"kolorist": "1.8.0",
"sass": "1.89.2",
"simple-git-hooks": "2.13.0",
"tsx": "4.20.3",
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.8.0",
"vite": "7.0.4",
"sass": "1.91.0",
"simple-git-hooks": "2.13.1",
"tsx": "4.20.5",
"typescript": "5.9.2",
"unplugin-icons": "22.2.0",
"unplugin-vue-components": "29.0.0",
"vite": "7.1.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7",
"vite-plugin-vue-devtools": "8.0.1",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.1"
"vue-tsc": "3.0.6"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@ -11,7 +11,7 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.10.0",
"axios": "1.11.0",
"axios-retry": "4.5.0",
"qs": "6.14.0"
},

View File

@ -14,14 +14,14 @@
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0",
"c12": "3.0.4",
"bumpp": "10.2.3",
"c12": "3.2.0",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "18.0.1",
"npm-check-updates": "18.0.3",
"rimraf": "6.0.1"
}
}

2555
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4580
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2", features = [] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,7 +0,0 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"migrated": {
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,8 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,57 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"frontendDist": "../dist",
"devUrl": "http://localhost:9527"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "RuoYi-Plus-Soybean",
"targets": "all",
"externalBin": [],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"deb": {
"depends": []
}
}
},
"productName": "RuoYi-Plus-Soybean",
"mainBinaryName": "RuoYi-Plus-Soybean",
"version": "1.0.0",
"identifier": "org.dromara.admin",
"plugins": {},
"app": {
"windows": [
{
"fullscreen": false,
"height": 768,
"resizable": true,
"title": "RuoYi-Plus-Soybean",
"width": 1366,
"useHttpsScheme": true
}
],
"security": {
"csp": null
}
}
}

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import type { TagProps } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { useDict } from '@/hooks/business/dict';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
@ -28,7 +29,7 @@ const { transformDictData } = useDict(props.dictCode, props.immediate);
const dictTagData = computed<Api.System.DictData[]>(() => {
if (props.dictData) {
const dictData = props.dictData;
const dictData = jsonClone(props.dictData);
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
}

View File

@ -2,6 +2,7 @@
import { computed } from 'vue';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import { twMerge } from 'tailwind-merge';
hljs.registerLanguage('json', json);
@ -10,15 +11,19 @@ defineOptions({
});
interface Props {
class?: string;
code?: string;
showLineNumbers?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
class: '',
code: '',
showLineNumbers: false
});
const DEFAULT_CLASS = 'max-h-500px';
/** 格式化JSON数据 */
const jsonData = computed<string>(() => {
if (!props.code) return '';
@ -33,9 +38,9 @@ const jsonData = computed<string>(() => {
</script>
<template>
<div class="json-preview">
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" />
</div>
<NScrollbar :class="twMerge(DEFAULT_CLASS, props.class)">
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" :word-wrap="true" />
</NScrollbar>
</template>
<style lang="scss">
@ -44,18 +49,4 @@ html[class='dark'] {
background-color: #7c7777;
}
}
.json-preview {
width: 100%;
max-height: 500px;
overflow-y: auto;
@include scrollbar();
.empty-data {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
font-size: 14px;
}
}
</style>

View File

@ -1,61 +1,524 @@
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<script lang="ts" setup>
import { computed } from 'vue';
import { getPaletteColorByNumber } from '@sa/color';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({ name: 'WaveBg' });
interface Props {
/** Theme color */
themeColor: string;
const themeStore = useThemeStore();
function toggleThemeScheme() {
if (themeStore.darkMode) {
themeStore.setThemeScheme('light');
return;
}
themeStore.setThemeScheme('dark');
}
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script>
<template>
<div class="absolute-lt z-1 size-full overflow-hidden">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
</div>
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
<svg height="896" width="967.8852157128662">
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
<div class="wave-bg">
<!-- 几何装饰元素 -->
<div class="geometric-decorations">
<!-- 基础几何形状 -->
<div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
<div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
<div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
<div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
<!-- 背景泡泡 -->
<div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
<!-- 太阳/月亮 -->
<div
class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
@click="toggleThemeScheme"
></div>
<!-- 装饰点 -->
<div class="geo-element dot dot-top-left animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-top-right animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-center-right animate-bounce-in animate-delay-0s"></div>
<!-- 叠加方块组 -->
<div class="squares-group">
<i class="geo-element square square-blue animate-fade-in-left-rotated-blue animate-delay-0.2s"></i>
<i class="geo-element square square-pink animate-fade-in-left-rotated-pink animate-delay-0.4s"></i>
<i class="geo-element square square-purple animate-fade-in-left-no-rotation animate-delay-0.6s"></i>
</div>
</div>
</div>
</template>
<style scoped></style>
<style lang="scss" scoped>
// 颜色变量定义
$primary-light-7: rgb(var(--primary-50-color));
$primary-light-8: rgb(var(--primary-100-color));
$primary-light-9: rgb(var(--primary-200-color));
$primary-base: rgb(var(--primary-color));
$main-bg: rgb(var(--primary-50-color));
// 混合颜色函数
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
.wave-bg {
.geometric-decorations {
.geo-element {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
// 动画 mixin
@mixin fadeAnimation($direction: '', $rotation: 0deg) {
from {
opacity: 0;
@if $direction == 'up' {
transform: translateY(30px) rotate($rotation);
} @else if $direction == 'down' {
transform: translateY(-30px) rotate($rotation);
} @else if $direction == 'left' {
transform: translateX(-30px) rotate($rotation);
} @else if $direction == 'right' {
transform: translateX(30px) rotate($rotation);
}
}
to {
opacity: 1;
@if $direction == 'up' or $direction == 'down' {
transform: translateY(0) rotate($rotation);
} @else {
transform: translateX(0) rotate($rotation);
}
}
}
// 动画定义
@keyframes fadeInUp {
@include fadeAnimation('up');
}
@keyframes fadeInDown {
@include fadeAnimation('down');
}
@keyframes fadeInLeft {
@include fadeAnimation('left');
}
@keyframes fadeInLeftRotated {
@include fadeAnimation('left', -25deg);
}
@keyframes fadeInRight {
@include fadeAnimation('right');
}
@keyframes fadeInRightRotated {
@include fadeAnimation('right', 45deg);
}
@keyframes fadeInLeftRotatedBlue {
@include fadeAnimation('left', -10deg);
}
@keyframes fadeInLeftRotatedPink {
@include fadeAnimation('left', 10deg);
}
@keyframes fadeInLeftNoRotation {
@include fadeAnimation('left');
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes lineGrow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 动画类
.animate-fade-in-up {
animation-name: fadeInUp;
}
.animate-fade-in-down {
animation-name: fadeInDown;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
}
.animate-fade-in-right {
animation-name: fadeInRight;
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 1.2s;
}
.animate-bounce-in {
animation-name: bounceIn;
animation-duration: 0.6s;
}
.animate-fade-in-left-rotated-blue {
animation-name: fadeInLeftRotatedBlue;
}
.animate-fade-in-left-rotated-pink {
animation-name: fadeInLeftRotatedPink;
}
.animate-fade-in-left-no-rotation {
animation-name: fadeInLeftNoRotation;
}
// 基础几何形状
.circle-outline {
top: 10%;
left: 25%;
width: 42px;
height: 42px;
border: 2px solid $primary-light-8;
border-radius: 50%;
}
.square-rotated {
top: 50%;
left: 16%;
width: 60px;
height: 60px;
background-color: $bg-mix-light-8;
&.animate-fade-in-left {
animation-name: fadeInLeftRotated;
}
}
.circle-small {
bottom: 26%;
left: 30%;
width: 18px;
height: 18px;
background-color: $primary-light-8;
border-radius: 50%;
}
// 太阳/月亮效果
.circle-top-right {
top: 3%;
right: 3%;
z-index: 100;
width: 50px;
height: 50px;
cursor: pointer;
background: $bg-mix-light-7;
border-radius: 50%;
transition: all 0.3s;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: '';
background: linear-gradient(to right, #fcbb04, #fffc00);
border-radius: 50%;
opacity: 0;
transition: all 0.5s;
transform: translate(-50%, -50%);
}
&:hover {
box-shadow: 0 0 36px #fffc00;
&::after {
opacity: 1;
}
}
}
.square-bottom-right {
right: 10%;
bottom: 10%;
width: 50px;
height: 50px;
background-color: $primary-light-8;
&.animate-fade-in-right {
animation-name: fadeInRightRotated;
}
}
// 背景泡泡
.bg-bubble {
top: -120px;
right: -120px;
width: 360px;
height: 360px;
background-color: $bg-mix-light-8;
border-radius: 50%;
}
// 装饰点
.dot {
width: 14px;
height: 14px;
background-color: $primary-light-7;
border-radius: 50%;
&.dot-top-left {
top: 140px;
left: 100px;
}
&.dot-top-right {
top: 140px;
right: 120px;
}
&.dot-center-right {
top: 46%;
right: 22%;
background-color: $primary-light-8;
}
}
// 叠加方块组
.squares-group {
position: absolute;
bottom: 18px;
left: 20px;
width: 140px;
height: 140px;
pointer-events: none;
.square {
position: absolute;
display: block;
border-radius: 8px;
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
&.square-blue {
top: 12px;
left: 30px;
z-index: 2;
width: 50px;
height: 50px;
background-color: rgb(from $primary-base r g b / 30%);
}
&.square-pink {
top: 30px;
left: 48px;
z-index: 1;
width: 70px;
height: 70px;
background-color: rgb(from $primary-base r g b / 15%);
}
&.square-purple {
top: 66px;
left: 86px;
z-index: 3;
width: 32px;
height: 32px;
background-color: rgb(from $primary-base r g b / 45%);
}
}
// 装饰线条
&::after {
position: absolute;
top: 86px;
left: 72px;
width: 80px;
height: 1px;
content: '';
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
opacity: 0;
transform: rotate(50deg);
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 1.2s;
}
}
}
@media only screen and (max-width: 1600px) {
width: 60vw;
.text-wrap {
bottom: 40px;
}
}
@media only screen and (max-width: 1280px) {
width: auto;
height: auto;
padding: 0;
// 隐藏背景和其他内容,只保留 logo
background: transparent;
.left-img,
.text-wrap,
.geometric-decorations {
display: none;
}
.logo {
position: fixed;
top: 15px;
left: 25px;
z-index: 1000;
}
}
}
// 暗色主题
.dark .wave-bg {
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
@media only screen and (max-width: 1280px) {
background: transparent;
}
.geometric-decorations {
// 月亮效果
.circle-top-right {
background-color: $bg-mix-light-8;
box-shadow: 0 0 25px #333 inset;
transition: all 0.3s ease-in-out 0.1s;
rotate: -48deg;
&::before {
position: absolute;
top: 0;
left: 15px;
width: 50px;
height: 50px;
content: '';
background-color: $bg-mix-light-9;
border-radius: 50%;
transition: all 0.3s ease-in-out;
}
&:hover {
background-color: transparent;
box-shadow: 0 40px 25px #ddd inset;
&::before {
left: 18px;
}
&::after {
opacity: 0;
}
}
}
.bg-bubble {
background-color: $bg-mix-light-9;
}
// 其他元素颜色调整
.square-rotated {
background-color: $bg-mix-light-9;
}
.circle-small,
.dot {
background-color: $primary-light-8;
}
.square-bottom-right {
background-color: $primary-light-9;
}
.dot.dot-top-right {
background-color: $primary-light-8;
}
}
// 方块组暗色调整
.squares-group {
.square {
box-shadow: none;
&.square-blue {
background-color: rgb(from $primary-base r g b / 18%);
}
&.square-pink {
background-color: rgb(from $primary-base r g b / 10%);
}
&.square-purple {
background-color: rgb(from $primary-base r g b / 20%);
}
}
&::after {
background: linear-gradient(90deg, $primary-light-8, transparent);
}
}
}
</style>

View File

@ -4,5 +4,6 @@ export enum SetupStoreId {
Auth = 'auth-store',
Route = 'route-store',
Tab = 'tab-store',
Notice = 'notice-store'
Notice = 'notice-store',
Dict = 'dict-store'
}

View File

@ -113,6 +113,10 @@ export function useDownload() {
const response = await fetch(fullUrl, requestOptions);
if (response.status !== 200) {
throw new Error(errorCodeRecord.default);
}
await handleResponse(response);
const rawHeader = response.headers.get('Download-Filename');

View File

@ -33,7 +33,7 @@ const toGitee = () => {
</NBadge>
</NButton>
</template>
消息
{{ $t('page.home.message') }}
</NTooltip>
</template>
<NCard

View File

@ -328,6 +328,8 @@ const local: App.I18n.Schema = {
page: {
login: {
common: {
title: 'Modern enterprise-level multi-tenant management system',
subTitle: 'Provides developers with a complete enterprise management solution',
loginOrRegister: 'Login / Register',
register: 'Register',
userNamePlaceholder: 'Please enter user name',

View File

@ -328,6 +328,8 @@ const local: App.I18n.Schema = {
page: {
login: {
common: {
title: '现代化的企业级多租户管理系统',
subTitle: '为开发者提供了完整的企业管理解决方案',
loginOrRegister: '登录 / 注册',
register: '注册',
userNamePlaceholder: '请输入用户名',

View File

@ -8,7 +8,7 @@ import { decrypt, encrypt } from '@/utils/jsencrypt';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';
const encryptHeader = 'encrypt-key';
const encryptHeader = import.meta.env.VITE_HEADER_FLAG || 'encrypt-key';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@ -48,6 +48,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
isBackendSuccess(response) {
// when the backend response code is "0000"(default), it means the request is success
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
if (import.meta.env.VITE_APP_ENCRYPT === 'Y' && response.headers[encryptHeader]) {
const keyStr = response.headers[encryptHeader];
const data = String(response.data);
const base64Str = decrypt(keyStr);
const aesKey = decryptBase64(base64Str.toString());
const decryptData = decryptWithAes(data, aesKey);
response.data = JSON.parse(decryptData);
}
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
@ -125,23 +133,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
return null;
},
transformBackendResponse(response) {
if (import.meta.env.VITE_APP_ENCRYPT === 'Y') {
// 加密后的 AES 秘钥
const keyStr = response.headers[encryptHeader];
// 加密
if (keyStr && keyStr !== '') {
const data = String(response.data);
// 请求体 AES 解密
const base64Str = decrypt(keyStr);
// base64 解码 得到请求头的 AES 秘钥
const aesKey = decryptBase64(base64Str.toString());
// aesKey 解码 data
const decryptData = decryptWithAes(data, aesKey);
// 将结果 (得到的是 JSON 字符串) 转为 JSON
response.data = JSON.parse(decryptData);
}
}
// 二进制数据则直接返回
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
return response.data;

View File

@ -8,6 +8,7 @@ import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum';
import { useRouteStore } from '../route';
import { useTabStore } from '../tab';
import useNoticeStore from '../notice';
import { clearAuthStorage, getToken } from './shared';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
@ -15,6 +16,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const authStore = useAuthStore();
const routeStore = useRouteStore();
const tabStore = useTabStore();
const noticeStore = useNoticeStore();
const { toLogin, redirectFromLogin } = useRouterPush(false);
const { loading: loginLoading, startLoading, endLoading } = useLoading();
@ -48,6 +50,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
await toLogin();
}
noticeStore.clearNotice();
tabStore.cacheTabs();
routeStore.resetStore();
}

View File

@ -1,8 +1,9 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { $t } from '@/locales';
import { SetupStoreId } from '@/enum';
export const useDictStore = defineStore('dict', () => {
export const useDictStore = defineStore(SetupStoreId.Dict, () => {
const dictData = ref<{ [key: string]: Api.System.DictData[] }>({});
const getDict = (key: string) => {

View File

@ -127,7 +127,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
}
// @ts-expect-error no hidden field
route.meta.hideInMenu = route.hidden;
if (route.meta.hideInMenu && parent) {
if (route.meta.hideInMenu && parent && !route.meta.activeMenu) {
// @ts-expect-error parent.name is activeMenu type
route.meta.activeMenu = parent.name;
}

View File

@ -6,6 +6,7 @@ html,
body,
#app {
height: 100%;
font-family: inherit;
}
html {

View File

@ -44,7 +44,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixedHeaderAndTab: true,
sider: {
inverted: false,
width: 220,
width: 230,
collapsedWidth: 64,
mixWidth: 90,
mixCollapsedWidth: 64,

View File

@ -487,6 +487,8 @@ declare namespace App {
};
login: {
common: {
title: string;
subTitle: string;
loginOrRegister: string;
register: string;
userNamePlaceholder: string;

View File

@ -114,6 +114,7 @@ declare namespace Env {
readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
readonly VITE_APP_CLIENT_ID?: string;
readonly VITE_APP_ENCRYPT?: CommonType.YesOrNo;
readonly VITE_HEADER_FLAG?: string;
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;

71
src/utils/export.ts Normal file
View File

@ -0,0 +1,71 @@
import { utils, writeFile } from 'xlsx';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
export interface ExportExcelProps<T> {
columns: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<T>>[];
data: NaiveUI.TableDataWithIndex<T>[];
filename: string;
ignoreKeys?: (keyof NaiveUI.TableDataWithIndex<T> | NaiveUI.CustomColumnKey)[];
dicts?: Record<keyof NaiveUI.TableDataWithIndex<T>, string>;
}
export function exportExcel<T>({
columns,
data,
filename,
dicts,
ignoreKeys = ['index', 'operate']
}: ExportExcelProps<T>) {
const exportColumns = columns.filter(col => isTableColumnHasKey(col) && !ignoreKeys?.includes(col.key));
const excelList = data.map(item => exportColumns.map(col => getTableValue(col, item, dicts)));
const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.title) || null);
excelList.unshift(titleList);
const workBook = utils.book_new();
const workSheet = utils.aoa_to_sheet(excelList);
workSheet['!cols'] = exportColumns.map(item => ({
width: Math.round(Number(item.width) / 10 || 20)
}));
utils.book_append_sheet(workBook, workSheet, filename);
writeFile(workBook, `${filename}.xlsx`);
}
function getTableValue<T>(
col: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<T>>,
item: NaiveUI.TableDataWithIndex<T>,
dicts?: Record<keyof NaiveUI.TableDataWithIndex<T>, string>
) {
if (!isTableColumnHasKey(col)) {
return null;
}
const { key } = col;
if (key === 'operate') {
return null;
}
if (isNotNull(dicts?.[key]) && isNotNull(item[key])) {
return $t(item[key] as App.I18n.I18nKey);
}
return item[key];
}
function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}
function isTableColumnHasTitle<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> & {
title: string;
} {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).title);
}

View File

@ -39,45 +39,51 @@ const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
</script>
<template>
<div class="relative min-h-screen w-full flex flex-wrap">
<div class="hidden min-h-screen w-50% bg-primary-100 lg:block dark:bg-primary-800">
<div class="size-full flex-center">
<img class="w-60% sm:w-80%" :src="loginBackground" />
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<div class="box-border size-full flex">
<div class="relative box-border hidden h-full w-65vw overflow-hidden bg-primary-50 xl:block dark:bg-primary-900">
<div class="relative z-100 flex items-center pl-30px pt-30px">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
</div>
<div class="w-full flex-col-center px-24px py-32px lg:w-50%">
<div class="mx-auto max-w-464px w-full">
<header class="flex-y-center justify-between">
<div class="flex-y-center gap-16px">
<SystemLogo class="text-30px text-primary sm:text-42px" />
<h3 class="text-24px text-primary font-500 sm:text-32px">{{ $t('system.title') }}</h3>
</div>
<div class="flex-y-center">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-24px">
<div>
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
<div class="absolute inset-x-0 inset-b-10.5% inset-t-0 z-10 m-auto w-40%">
<img class="size-full" :src="loginBackground" />
</div>
<div class="absolute bottom-80px w-full text-center">
<h1 class="text-24px font-400">{{ $t('page.login.common.title') }}</h1>
<p class="mt-8px text-14px color-gray-500">{{ $t('page.login.common.subTitle') }}</p>
</div>
<WaveBg />
</div>
<header class="relative h-full flex-1 xl:m-auto sm:!w-full">
<div class="relative z-100 block flex items-center pl-30px pt-30px xl:hidden">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
<div class="position-fixed right-30px top-24px z-100 flex items-center justify-end">
<div class="ml-15px inline-block flex cursor-pointer select-none p-5px">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@change-lang="appStore.changeLocale"
/>
</div>
</div>
<main class="absolute inset-0 m-auto h-630px max-w-450px w-full overflow-hidden rounded-5px bg-cover px-24px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</main>
</header>
</div>
</template>

View File

@ -40,6 +40,10 @@ async function handleSubmit() {
</script>
<template>
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">
{{ $t('page.login.codeLogin.title') }}
</div>
<div class="pb-18px text-16px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
@ -52,15 +56,32 @@ async function handleSubmit() {
</NButton>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
<NSpace vertical :size="20" class="w-full">
<NButton type="primary" size="large" block @click="handleSubmit">
{{ $t('page.login.codeLogin.title') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</template>
<style scoped></style>
<style scoped>
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
:deep(.n-base-selection-label) {
padding: 0 6px !important;
}
:deep(.n-button) {
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
</style>

View File

@ -124,8 +124,8 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">登录到您的账户</div>
<div class="pb-24px text-18px text-#858585">欢迎回来请输入您的账户信息</div>
<div class="mb-5px text-32px text-black font-600 dark:text-white">登录到您的账户</div>
<div class="pb-18px text-16px text-#858585">欢迎回来请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
@ -156,16 +156,16 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-52px">
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="16" class="mb-8px">
<div class="mx-6px mb-10px flex-y-center justify-between">
<NSpace vertical :size="12" class="mb-8px">
<div class="mx-6px mb-8px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
@ -199,7 +199,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
</NButton>
</div>
<div class="mt-32px w-full text-center text-18px text-#858585">
<div class="mt-24px w-full text-center text-18px text-#858585">
您还没有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
@ -216,13 +216,13 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
img {
height: 52px;
height: 42px;
}
}
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -237,7 +237,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -104,8 +104,8 @@ handleFetchCaptchaCode();
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-24px text-18px text-#858585">欢迎注册请输入您的账户信息</div>
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-18px text-16px text-#858585">欢迎注册请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
@ -147,14 +147,14 @@ handleFetchCaptchaCode();
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full pt-6px">
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<div class="mt-32px w-full text-center text-18px text-#858585">
<div class="mt-24px w-full text-center text-18px text-#858585">
您已有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('pwd-login')">
{{ $t('common.login') }}
@ -177,7 +177,7 @@ handleFetchCaptchaCode();
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -187,7 +187,7 @@ handleFetchCaptchaCode();
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -46,10 +46,10 @@ async function handleSubmit() {
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">
{{ $t('page.login.resetPwd.title') }}
</div>
<div class="pb-24px text-18px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<div class="pb-18px text-16px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
@ -73,7 +73,7 @@ async function handleSubmit() {
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NSpace vertical :size="20" class="w-full">
<NButton type="primary" size="large" block @click="handleSubmit">
{{ $t('page.login.resetPwd.title') }}
</NButton>
@ -88,7 +88,7 @@ async function handleSubmit() {
<style scoped>
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -98,7 +98,7 @@ async function handleSubmit() {
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -40,6 +40,7 @@ const deptData = ref<Api.System.Dept[]>([]);
const userOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
const placeholder = ref<string>($t('page.system.dept.placeholder.defaultLeaderPlaceHolder'));
const disabled = ref<boolean>(false);
const expandedKeys = ref<CommonType.IdType[]>([]);
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
@ -55,7 +56,7 @@ const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
parentId: '',
parentId: props.rowData?.deptId || '',
deptName: '',
deptCategory: '',
orderNum: null,
@ -80,7 +81,6 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
model.parentId = props.rowData?.deptId || 0;
}
if (props.operateType === 'edit' && props.rowData) {
@ -144,6 +144,7 @@ async function getDeptData() {
if (data) {
deptData.value = handleTree(data, { idField: 'deptId' });
expandedKeys.value = [deptData.value[0].deptId];
}
endDeptLoading();
}
@ -186,15 +187,15 @@ watch(visible, () => {
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem v-if="model.parentId != 0" :label="$t('page.system.dept.parentId')" path="parentId">
<NFormItem v-if="model.parentId !== 0" :label="$t('page.system.dept.parentId')" path="parentId">
<NTreeSelect
v-model:value="model.parentId"
v-model:expanded-keys="expandedKeys"
:loading="deptLoading"
clearable
:options="deptData"
label-field="deptName"
key-field="deptId"
default-expand-all
:placeholder="$t('page.system.dept.form.parentId.required')"
/>
</NFormItem>

View File

@ -38,6 +38,8 @@ const visible = defineModel<boolean>('visible', {
default: false
});
const defaultIcon = import.meta.env.VITE_MENU_ICON;
const iconType = ref<Api.System.IconType>('1');
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule, createNumberRequiredRule } = useFormRules();
@ -69,7 +71,7 @@ function createDefaultModel(): Model {
visible: '0',
status: '0',
perms: '',
icon: undefined,
icon: defaultIcon,
remark: ''
};
}
@ -118,6 +120,7 @@ const localIconOptions = localIcons.map<SelectOption>(item => ({
function handleInitModel() {
queryList.value = [];
iconType.value = '1';
Object.assign(model, createDefaultModel());
if (props.operateType === 'edit' && props.rowData) {
@ -208,7 +211,7 @@ async function handleSubmit() {
visible: menuVisible,
status,
perms,
icon,
icon: icon || defaultIcon,
component: processComponent(component),
remark
};

View File

@ -323,11 +323,11 @@ function handleResetSearch() {
}
:deep(.n-tree-node) {
height: 33px;
height: 30px;
}
:deep(.n-tree-node-switcher) {
height: 33px;
height: 30px;
}
:deep(.n-tree-node-switcher__icon) {

View File

@ -1,6 +1,6 @@
<script setup lang="tsx">
import { computed, ref } from 'vue';
import { NButton, NDivider } from 'naive-ui';
import { NAvatar, NButton, NDivider, NEllipsis } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { jsonClone } from '@sa/utils';
import { fetchBatchDeleteUser, fetchGetDeptTree, fetchGetUserList, fetchUpdateUserStatus } from '@/service/api/system';
@ -12,6 +12,7 @@ import { useDownload } from '@/hooks/business/download';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import StatusSwitch from '@/components/custom/status-switch.vue';
import DictTag from '@/components/custom/dict-tag.vue';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserImportModal from './modules/user-import-modal.vue';
import UserPasswordDrawer from './modules/user-password-drawer.vue';
@ -65,41 +66,64 @@ const {
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
width: 48
},
{
key: 'userName',
title: $t('page.system.user.userName'),
align: 'center',
minWidth: 120,
ellipsis: true
align: 'left',
width: 200,
ellipsis: true,
render: row => {
return (
<div class="flex items-center justify-center gap-2">
<NAvatar src={row.avatar} class="bg-primary">
{row.avatar ? undefined : row.nickName.charAt(0)}
</NAvatar>
<div class="max-w-160px flex flex-col">
<NEllipsis>{row.userName}</NEllipsis>
<NEllipsis>{row.nickName}</NEllipsis>
</div>
</div>
);
}
},
{
key: 'nickName',
title: $t('page.system.user.nickName'),
key: 'sex',
title: $t('page.system.user.sex'),
align: 'center',
minWidth: 120,
ellipsis: true
width: 80,
ellipsis: true,
render(row) {
return <DictTag value={row.sex} dictCode="sys_user_sex" />;
}
},
{
key: 'deptName',
title: $t('page.system.user.deptName'),
align: 'center',
minWidth: 120,
width: 120,
ellipsis: true
},
{
key: 'email',
title: $t('page.system.user.email'),
align: 'center',
width: 120,
ellipsis: true
},
{
key: 'phonenumber',
title: $t('page.system.user.phonenumber'),
align: 'center',
minWidth: 120,
width: 120,
ellipsis: true
},
{
key: 'status',
title: $t('page.system.user.status'),
align: 'center',
minWidth: 80,
width: 80,
render(row) {
return (
<StatusSwitch
@ -115,7 +139,7 @@ const {
key: 'createTime',
title: $t('page.system.user.createTime'),
align: 'center',
minWidth: 120
width: 120
},
{
key: 'operate',
@ -341,7 +365,7 @@ function handleResetSearch() {
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:scroll-x="1200"
:loading="loading"
remote
:row-key="row => row.userId"
@ -391,11 +415,11 @@ function handleResetSearch() {
}
:deep(.n-tree-node) {
height: 25px;
height: 30px;
}
:deep(.n-tree-node-switcher) {
height: 25px;
height: 30px;
}
:deep(.n-tree-node-switcher__icon) {

View File

@ -356,7 +356,7 @@ const columns: NaiveUI.TableColumn<Api.Tool.GenTableColumn>[] = [
<NFormItemGi span="24 s:12" path="moduleName">
<template #label>
<div class="flex-center">
<FormTip content="可理解为子系统名,例如 system" />
<FormTip content="可理解为子系统名,例如 systemflow-instance。避免驼峰命名" />
<span class="pl-3px">生成模块名</span>
</div>
</template>