Merge branch 'master' into tauri

# Conflicts:
#	package.json
#	pnpm-lock.yaml
This commit is contained in:
xlsea
2025-07-24 17:49:12 +08:00
49 changed files with 1671 additions and 1048 deletions

View File

@ -16,7 +16,7 @@
}, },
"playwright": { "playwright": {
"command": "npx", "command": "npx",
"args": ["@playwright/mcp@latest"] "args": ["@playwright/mcp@0.0.29"]
}, },
"mcp-server-time": { "mcp-server-time": {
"command": "uvx", "command": "uvx",
@ -26,7 +26,7 @@
"command": "npx", "command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"], "args": ["-y", "mcp-shrimp-task-manager"],
"env": { "env": {
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data", "DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en", "TEMPLATES_USE": "en",
"ENABLE_GUI": "false" "ENABLE_GUI": "false"
} }

View File

@ -11,6 +11,9 @@
"i18n-ally.editor.preferEditor": true, "i18n-ally.editor.preferEditor": true,
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"], "i18n-ally.localesPaths": ["src/locales/langs"],
"i18n-ally.parsers.typescript.compilerOptions": {
"moduleResolution": "node"
},
"prettier.enable": false, "prettier.enable": false,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"unocss.root": ["./"], "unocss.root": ["./"],

View File

@ -1,5 +1,93 @@
# 更新日志 # 更新日志
## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24)
###    🐞 Bug 修复
- 修复 api.d.ts.vm 代码生成模板bug &nbsp;-&nbsp; by **zygalaxy** [<samp>(4e8c8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e8c8715)
- **projects**:
- 修复刷新时跳转至登录页问题 &nbsp;-&nbsp; by **AN** [<samp>(2587f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2587f8cb)
- 修复登录过期不弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(e485f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e485f680)
- 修复菜单结构变动后路由无法进入问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f4038)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f4038a2d)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**: 优化搜索框FormItem &nbsp;-&nbsp; by **AN** [<samp>(a1336)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a1336d15)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**: update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(e89b8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e89b86ce)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**: 搜索FormItem占比调整 &nbsp;-&nbsp; by **AN** [<samp>(cc29e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cc29ea85)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;
[zygalaxy](mailto:zygalaxy@qq.com)
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **hooks**:
- 重构下载方法,支持流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(65067)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/650673e2)
- **projects**:
- 角色分配用户新增部门与时间查询条件 &nbsp;-&nbsp; by @m-xlsea [<samp>(ad48d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
- 修改操作后列表查询方式 &nbsp;-&nbsp; by @m-xlsea [<samp>(d8542)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d85424ee)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **hooks**:
- 解决 streamsaver 访问不到 Github 资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(566b2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/566b2c2d)
- **other**:
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **packages**:
- 修复 cleanup 会删除富文本编辑器资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(9ca7c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
- **projects**:
- 修复字典数据重复获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(3628c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3628c249)
- 修改强退在线设备接口 &nbsp;-&nbsp; by **AN** [<samp>(dbcf8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
- 修复代码生成逻辑判断问题 &nbsp;-&nbsp; by **AN** [<samp>(6fc7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A &nbsp;-&nbsp; by @m-xlsea [<samp>(ad938)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad9386eb)
- 修复未清空文件列表,上传回显问题 &nbsp;-&nbsp; by **AN** [<samp>(229e0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/229e0044)
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 &nbsp;-&nbsp; by @xiaobao0505 in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41191d54)
- 修复角色列表操作栏展示不全问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(62f2c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
- 修复用户导入结果信息未渲染标签问题 &nbsp;-&nbsp; by **AN** [<samp>(efc95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/efc953c0)
- 修复角色用户分配未调用接口问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ff874)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ff87415d)
- **styles**:
- 修复登录页平板界面滚动问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(90145)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90145fa5)
- **utils**:
- 修复isNull和IsNotNull判断方法潜在问题 &nbsp;-&nbsp; by **AN** [<samp>(90d32)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90d32ee2)
### &nbsp;&nbsp;&nbsp;💅 重构
- **projects**: 调整租户套餐菜单接口 &nbsp;-&nbsp; by **AN** [<samp>(b9999)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b9999935)
### &nbsp;&nbsp;&nbsp;📖 文档
- **other**: 修改文档内容 &nbsp;-&nbsp; by @m-xlsea [<samp>(3ae99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3ae9922d)
- **projects**: 优化 cursor 规则及 mcp &nbsp;-&nbsp; by @m-xlsea [<samp>(a3199)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a31994dc)
- **readme**: 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cbc)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update NodeJS and pnpm version requirements in package.json and documentation &nbsp;-&nbsp; by **Junior25306** [<samp>(a5c4b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(5cb1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(aeb63)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb63690)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(89c71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89c716e1)
- **packages**:
- update Vite version to 7 in package.json and documentation. &nbsp;-&nbsp; by **Azir** [<samp>(03dd6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03dd64c5)
- **projects**:
- update pnpm-lock.yaml &nbsp;-&nbsp; by @m-xlsea [<samp>(7c6ca)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
- **vscode**:
- remove unused vue.server.hybridMode setting from .vscode/settings.json &nbsp;-&nbsp; by @soybeanjs [<samp>(13319)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/133196f3)
### &nbsp;&nbsp;&nbsp;❤️ 贡献值
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![xiaobao0505](https://github.com/xiaobao0505.png?size=48)](https://github.com/xiaobao0505)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[Junior25306](mailto:dayu429@qq.com)
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01) ## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
### &nbsp;&nbsp;&nbsp;🚀 新功能 ### &nbsp;&nbsp;&nbsp;🚀 新功能
@ -90,7 +178,7 @@
### &nbsp;&nbsp;&nbsp;❤️ 贡献者 ### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![wenyuanw](https://github.com/chen-ziwen.png?size=48)](https://github.com/chen-ziwen)&nbsp;&nbsp; [![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![chen-ziwen](https://github.com/chen-ziwen.png?size=48)](https://github.com/chen-ziwen)&nbsp;&nbsp;
[![wangzhongqi0917](https://gitee.com/wangzhongqi0917.png?width=48)](https://gitee.com/wangzhongqi0917)&nbsp;&nbsp;[![qq1822213252](https://gitee.com/qq1822213252.png?width=48)](https://gitee.com/qq1822213252)&nbsp;&nbsp;[![tangzc](https://gitee.com/tangzc.png?width=48)](https://gitee.com/tangzc),&nbsp;[metabytes](https://gitee.com/metabytes) [![wangzhongqi0917](https://gitee.com/wangzhongqi0917.png?width=48)](https://gitee.com/wangzhongqi0917)&nbsp;&nbsp;[![qq1822213252](https://gitee.com/qq1822213252.png?width=48)](https://gitee.com/qq1822213252)&nbsp;&nbsp;[![tangzc](https://gitee.com/tangzc.png?width=48)](https://gitee.com/tangzc),&nbsp;[metabytes](https://gitee.com/metabytes)

View File

@ -22,13 +22,12 @@
# 📢 重要通知 # 📢 重要通知
1.1.0 版本已发布,但仍然建议: 1.1.2 版本已经正式发布,工作流版本迎来首个版本(请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
- 在生产环境使用前进行充分测试 - 在生产环境使用前进行充分测试
- 关注项目更新,及时获取最新版本 - 关注项目更新,及时获取最新版本
- 积极反馈问题,帮助我们快速迭代 - 积极反馈问题,帮助我们快速迭代
**后续规划** **后续规划**
- 工作流引擎集成
- 多语言国际化完善 - 多语言国际化完善
- 性能优化和稳定性提升 - 性能优化和稳定性提升
@ -119,8 +118,8 @@ root
## 🚀 环境要求与安装 ## 🚀 环境要求与安装
### 环境要求 ### 环境要求
- Node.js >= 18.20.0 - Node.js >= 20.19.0
- pnpm >= 8.7.0 - pnpm >= 10.5.0
- Git - Git
### 安装步骤及说明 ### 安装步骤及说明

View File

@ -1,44 +1,51 @@
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime']) #set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1)) #set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
/** /**
* namespace ${ModuleName} * Namespace Api
* *
* backend api module: "${ModuleName}" * All backend api type
*/ */
namespace ${ModuleName} { declare namespace Api {
/** ${businessname} */ /**
type ${BusinessName} = Common.CommonRecord<{ * namespace ${ModuleName}
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField)) *
/** $column.columnComment */ * backend api module: "${ModuleName}"
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end */
#end#end namespace ${ModuleName} {
}>; /** ${businessname} */
type ${BusinessName} = Common.CommonRecord<{
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
/** $column.columnComment */
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
#end#end
}>;
/** ${businessname} search params */ /** ${businessname} search params */
type ${BusinessName}SearchParams = CommonType.RecordNullable< type ${BusinessName}SearchParams = CommonType.RecordNullable<
Pick< Pick<
Api.${ModuleName}.${BusinessName}, Api.${ModuleName}.${BusinessName},
#foreach($column in $columns) #foreach($column in $columns)
#if($column.query && $column.queryType != 'BETWEEN') #if($column.query && $column.queryType != 'BETWEEN')
| '${column.javaField}' | '${column.javaField}'
#end #end
#end #end
> & > &
Api.Common.CommonSearchParams Api.Common.CommonSearchParams
>; >;
/** ${businessname} operate params */ /** ${businessname} operate params */
type ${BusinessName}OperateParams = CommonType.RecordNullable< type ${BusinessName}OperateParams = CommonType.RecordNullable<
Pick< Pick<
Api.${ModuleName}.${BusinessName}, Api.${ModuleName}.${BusinessName},
#foreach($column in $columns) #foreach($column in $columns)
#if($column.insert || $column.edit) #if($column.insert || $column.edit)
| '${column.javaField}' | '${column.javaField}'
#end #end
#end #end
> >
>; >;
/** ${businessname} list */ /** ${businessname} list */
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>; type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
}
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "ruoyi-vue-plus", "name": "ruoyi-vue-plus",
"type": "module", "type": "module",
"version": "1.1.0", "version": "1.1.2",
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。", "description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
"author": { "author": {
"name": "xlsea", "name": "xlsea",
@ -21,7 +21,7 @@
"Soybean Admin", "Soybean Admin",
"Vue3 admin ", "Vue3 admin ",
"vue-admin-template", "vue-admin-template",
"Vite6", "Vite7",
"TypeScript", "TypeScript",
"naive-ui", "naive-ui",
"naive-ui-admin", "naive-ui-admin",
@ -70,7 +70,7 @@
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@tauri-apps/api": "2.5.0", "@tauri-apps/api": "2.5.0",
"@types/streamsaver": "^2.0.5", "@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.4.0", "@vueuse/core": "13.5.0",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"defu": "6.1.4", "defu": "6.1.4",
@ -87,44 +87,44 @@
"vue": "3.5.17", "vue": "3.5.17",
"vue-advanced-cropper": "^2.8.9", "vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0", "vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.7", "vue-i18n": "11.1.9",
"vue-router": "4.5.1" "vue-router": "4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.353", "@iconify/json": "2.2.357",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.0", "@soybeanjs/eslint-config": "1.7.1",
"@tauri-apps/cli": "2.5.0", "@tauri-apps/cli": "2.5.0",
"@types/node": "24.0.4", "@types/node": "24.0.13",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.3.2", "@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.2", "@unocss/preset-icons": "66.3.3",
"@unocss/preset-uno": "66.3.2", "@unocss/preset-uno": "66.3.3",
"@unocss/transformer-directives": "66.3.2", "@unocss/transformer-directives": "66.3.3",
"@unocss/transformer-variant-group": "66.3.2", "@unocss/transformer-variant-group": "66.3.3",
"@unocss/vite": "66.3.2", "@unocss/vite": "66.3.3",
"@vitejs/plugin-vue": "6.0.0", "@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.0", "@vitejs/plugin-vue-jsx": "5.0.1",
"consola": "3.4.2", "consola": "3.4.2",
"eslint": "9.29.0", "eslint": "9.31.0",
"eslint-plugin-vue": "10.2.0", "eslint-plugin-vue": "10.3.0",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"sass": "1.89.2", "sass": "1.89.2",
"simple-git-hooks": "2.13.0", "simple-git-hooks": "2.13.0",
"tsx": "4.20.3", "tsx": "4.20.3",
"typescript": "5.8.3", "typescript": "5.8.3",
"unplugin-icons": "22.1.0", "unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.7.0", "unplugin-vue-components": "28.8.0",
"vite": "7.0.0", "vite": "7.0.4",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.0.2", "vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7", "vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.1.4", "vue-eslint-parser": "10.2.0",
"vue-tsc": "2.2.10" "vue-tsc": "3.0.1"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@ -15,6 +15,6 @@
"dependencies": { "dependencies": {
"@alova/mock": "2.0.17", "@alova/mock": "2.0.17",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"alova": "3.3.3" "alova": "3.3.4"
} }
} }

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"simplebar-vue": "2.4.1" "simplebar-vue": "2.4.2"
}, },
"devDependencies": { "devDependencies": {
"typed-css-modules": "0.9.1" "typed-css-modules": "0.9.1"

View File

@ -5,7 +5,7 @@ import type { CliOption } from '../types';
const defaultOptions: CliOption = { const defaultOptions: CliOption = {
cwd: process.cwd(), cwd: process.cwd(),
cleanupDirs: [ cleanupDirs: [
'**/dist', 'dist',
'**/package-lock.json', '**/package-lock.json',
'**/yarn.lock', '**/yarn.lock',
'**/pnpm-lock.yaml', '**/pnpm-lock.yaml',

View File

@ -1,6 +1,6 @@
{ {
"name": "@sa/tinymce", "name": "@sa/tinymce",
"version": "1.3.13", "version": "1.3.15",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

1844
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,179 @@
<!--
mitm.html is the lite "man in the middle"
This is only meant to signal the opener's messageChannel to
the service worker - when that is done this mitm can be closed
but it's better to keep it alive since this also stops the sw
from restarting
The service worker is capable of intercepting all request and fork their
own "fake" response - wish we are going to craft
when the worker then receives a stream then the worker will tell the opener
to open up a link that will start the download
-->
<script>
// This will prevent the sw from restarting
let keepAlive = () => {
keepAlive = () => {};
var ping = location.href.substr(0, location.href.lastIndexOf('/')) + '/ping';
var interval = setInterval(() => {
if (sw) {
sw.postMessage('ping');
} else {
fetch(ping).then(res => res.text(!res.ok && clearInterval(interval)));
}
}, 10000);
};
// message event is the first thing we need to setup a listner for
// don't want the opener to do a random timeout - instead they can listen for
// the ready event
// but since we need to wait for the Service Worker registration, we store the
// message for later
let messages = [];
window.onmessage = evt => messages.push(evt);
let sw = null;
let scope = '';
function registerWorker() {
return navigator.serviceWorker
.getRegistration('./')
.then(swReg => {
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' });
})
.then(swReg => {
const swRegTmp = swReg.installing || swReg.waiting;
scope = swReg.scope;
return (
(sw = swReg.active) ||
new Promise(resolve => {
swRegTmp.addEventListener(
'statechange',
(fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.removeEventListener('statechange', fn);
sw = swReg.active;
resolve();
}
})
);
})
);
});
}
// Now that we have the Service Worker registered we can process messages
function onMessage(event) {
let { data, ports, origin } = event;
// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) {
throw new TypeError("[StreamSaver] You didn't send a messageChannel");
}
if (typeof data !== 'object') {
throw new TypeError("[StreamSaver] You didn't send a object");
}
// the default public service worker for StreamSaver is shared among others.
// so all download links needs to be prefixed to avoid any other conflict
data.origin = origin;
// if we ever (in some feature versoin of streamsaver) would like to
// redirect back to the page of who initiated a http request
data.referrer = data.referrer || document.referrer || origin;
// pass along version for possible backwards compatibility in sw.js
data.streamSaverVersion = new URLSearchParams(location.search).get('version');
if (data.streamSaverVersion === '1.2.0') {
console.warn('[StreamSaver] please update streamsaver');
}
/** @since v2.0.0 */
if (!data.headers) {
console.warn(
"[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\nit should be a 2D array or a key/val object that fetch's Headers api accepts"
);
} else {
// test if it's correct
// should thorw a typeError if not
new Headers(data.headers);
}
/** @since v2.0.0 */
if (typeof data.filename === 'string') {
console.warn(
"[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option"
);
// Do what File constructor do with fileNames
data.filename = data.filename.replace(/\//g, ':');
}
/** @since v2.0.0 */
if (data.size) {
console.warn(
"[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option"
);
}
/** @since v2.0.0 */
if (data.readableStream) {
console.warn('[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm');
}
/** @since v2.0.0 */
if (!data.pathname) {
console.warn('[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)');
data.pathname = Math.random().toString().slice(-6) + '/' + data.filename;
}
// remove all leading slashes
data.pathname = data.pathname.replace(/^\/+/g, '');
// remove protocol
let org = origin.replace(/(^\w+:|^)\/\//, '');
// set the absolute pathname to the download url.
data.url = new URL(`${scope + org}/${data.pathname}`).toString();
if (!data.url.startsWith(`${scope + org}/`)) {
throw new TypeError('[StreamSaver] bad `data.pathname`');
}
// This sends the message data as well as transferring
// messageChannel.port2 to the service worker. The service worker can
// then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
const transferable = data.readableStream ? [ports[0], data.readableStream] : [ports[0]];
if (!(data.readableStream || data.transferringReadable)) {
keepAlive();
}
return sw.postMessage(data, transferable);
}
if (window.opener) {
// The opener can't listen to onload event, so we need to help em out!
// (telling them that we are ready to accept postMessage's)
window.opener.postMessage('StreamSaver::loadedPopup', '*');
}
if (navigator.serviceWorker) {
registerWorker().then(() => {
window.onmessage = onMessage;
messages.forEach(window.onmessage);
});
}
// FF v102 just started to supports transferable streams, but still needs to ping sw.js
// even tough the service worker dose not have to do any kind of work and listen to any
// messages... #305
keepAlive();
</script>

132
public/streamsaver/sw.js Normal file
View File

@ -0,0 +1,132 @@
/* eslint-disable */
/* global self ReadableStream Response */
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
self.onmessage = event => {
// We send a heartbeat every x second to keep the
// service worker alive if a transferable stream is not sent
if (event.data === 'ping') {
return;
}
const data = event.data;
const downloadUrl =
data.url || `${self.registration.scope + Math.random()}/${typeof data === 'string' ? data : data.filename}`;
const port = event.ports[0];
const metadata = Array.from({ length: 3 }); // [stream, data, port]
metadata[1] = data;
metadata[2] = port;
// Note to self:
// old streamsaver v1.2.0 might still use `readableStream`...
// but v2.0.0 will always transfer the stream through MessageChannel #94
if (event.data.readableStream) {
metadata[0] = event.data.readableStream;
} else if (event.data.transferringReadable) {
port.onmessage = evt => {
port.onmessage = null;
metadata[0] = evt.data.readableStream;
};
} else {
metadata[0] = createStream(port);
}
map.set(downloadUrl, metadata);
port.postMessage({ download: downloadUrl });
};
function createStream(port) {
// ReadableStream is only supported by chrome 52
return new ReadableStream({
start(controller) {
// When we receive data on the messageChannel, we write
port.onmessage = ({ data }) => {
if (data === 'end') {
return controller.close();
}
if (data === 'abort') {
controller.error('Aborted the download');
return;
}
controller.enqueue(data);
};
},
cancel(reason) {
console.log('user aborted', reason);
port.postMessage({ abort: true });
}
});
}
self.onfetch = event => {
const url = event.request.url;
// this only works for Firefox
if (url.endsWith('/ping')) {
return event.respondWith(new Response('pong'));
}
const hijacke = map.get(url);
if (!hijacke) return null;
const [stream, data, port] = hijacke;
map.delete(url);
// Not comfortable letting any user control all headers
// so we only copy over the length & disposition
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream; charset=utf-8',
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
'Content-Security-Policy': "default-src 'none'",
'X-Content-Security-Policy': "default-src 'none'",
'X-WebKit-CSP': "default-src 'none'",
'X-XSS-Protection': '1; mode=block',
'Cross-Origin-Embedder-Policy': 'require-corp'
});
const headers = new Headers(data.headers || {});
if (headers.has('Content-Length')) {
responseHeaders.set('Content-Length', headers.get('Content-Length'));
}
if (headers.has('Content-Disposition')) {
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'));
}
// data, data.filename and size should not be used anymore
if (data.size) {
console.warn('Depricated');
responseHeaders.set('Content-Length', data.size);
}
let fileName = typeof data === 'string' ? data : data.filename;
if (fileName) {
console.warn('Depricated');
// Make filename RFC5987 compatible
fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A');
responseHeaders.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`);
}
event.respondWith(new Response(stream, { headers: responseHeaders }));
port.postMessage({ debug: 'Download started' });
};

View File

@ -20,44 +20,45 @@ const fileList = ref<UploadFileInfo[]>([]);
async function handleFetchOssList(ossIds: string[]) { async function handleFetchOssList(ossIds: string[]) {
startLoading(); startLoading();
const { error, data } = await fetchGetOssListByIds(ossIds); try {
if (error) return; const { error, data } = await fetchGetOssListByIds(ossIds);
fileList.value = data.map(item => ({ if (error) return;
id: String(item.ossId), fileList.value = data.map(item => ({
url: item.url, id: String(item.ossId),
name: item.originalName, url: item.url,
status: 'finished' name: item.originalName,
})); status: 'finished'
endLoading(); }));
} catch (error) {
window.$message?.error(`获取文件列表失败: ${error}`);
} finally {
endLoading();
}
} }
watch( watch(
value, value,
async val => { async val => {
const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || []; const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || [];
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
if (ossIds.every(item => fileIds.has(item))) {
return;
}
if (ossIds.length === 0) { if (ossIds.length === 0) {
fileList.value = []; fileList.value = [];
return; return;
} }
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
if (ossIds.every(item => fileIds.has(item))) {
return;
}
await handleFetchOssList(ossIds); await handleFetchOssList(ossIds);
}, },
{ immediate: true } { immediate: true }
); );
watch( watch(fileList, val => {
fileList, value.value = val
val => { .filter(item => item.status === 'finished')
value.value = val .map(item => item.id)
.filter(item => item.status === 'finished') .join(',');
.map(item => item.id) });
.join(',');
},
{ deep: true }
);
</script> </script>
<template> <template>

View File

@ -51,6 +51,7 @@ export function useDownload() {
contentLength?: number contentLength?: number
): Promise<void> { ): Promise<void> {
window.$loading?.endLoading(); window.$loading?.endLoading();
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength }); const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
if (window.WritableStream && readableStream?.pipeTo) { if (window.WritableStream && readableStream?.pipeTo) {

View File

@ -2,6 +2,7 @@ import { ref, toValue } from 'vue';
import type { ComputedRef, Ref } from 'vue'; import type { ComputedRef, Ref } from 'vue';
import type { FormInst } from 'naive-ui'; import type { FormInst } from 'naive-ui';
import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg'; import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg';
import { isNull } from '@/utils/common';
import { $t } from '@/locales'; import { $t } from '@/locales';
export function useFormRules() { export function useFormRules() {
@ -52,7 +53,7 @@ export function useFormRules() {
required: true, required: true,
trigger: ['input', 'blur'], trigger: ['input', 'blur'],
validator: (_rule: any, value: any) => { validator: (_rule: any, value: any) => {
if (value === null || value === undefined || value === '') { if (isNull(value) || (Array.isArray(value) && value.length === 0)) {
return new Error(message); return new Error(message);
} }
return true; return true;

View File

@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: 'Update', update: 'Update',
saveSuccess: 'Save Success', saveSuccess: 'Save Success',
updateSuccess: 'Update Success', updateSuccess: 'Update Success',
noChange: 'No actions were taken',
userCenter: 'User Center', userCenter: 'User Center',
yesOrNo: { yesOrNo: {
yes: 'Yes', yes: 'Yes',

View File

@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: '更新', update: '更新',
saveSuccess: '保存成功', saveSuccess: '保存成功',
updateSuccess: '更新成功', updateSuccess: '更新成功',
noChange: '没有进行任何操作',
userCenter: '个人中心', userCenter: '个人中心',
yesOrNo: { yesOrNo: {
yes: '是', yes: '是',

View File

@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
params params
}); });
} }
/** 批量选择用户授权 */
export function fetchUpdateRoleAuthUser(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
return request<boolean>({
url: '/system/role/authUser/selectAll',
method: 'put',
params: { roleId, userIds: userIds.join(',') }
});
}
/** 批量取消用户授权 */
export function fetchUpdateRoleAuthUserCancel(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
return request<boolean>({
url: '/system/role/authUser/cancelAll',
method: 'put',
params: { roleId, userIds: userIds.join(',') }
});
}

View File

@ -65,37 +65,46 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg); request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
} }
const isLogin = Boolean(localStg.get('token'));
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page // when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
// const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || []; const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
// if (logoutCodes.includes(responseCode)) { if (logoutCodes.includes(responseCode) && !isLogin) {
// handleLogout(); logoutAndCleanup();
// return null; return null;
// } }
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal // 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(',') || []; const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode)) { if (modalLogoutCodes.includes(responseCode) && isLogin) {
const isExist = request.state.errMsgStack && request.state.errMsgStack.includes(response.data.msg);
if (isExist) {
return null;
}
if (window.location.pathname?.startsWith('/login')) {
logoutAndCleanup();
return null;
}
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg]; request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
// prevent the user from refreshing the page // prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout); window.addEventListener('beforeunload', handleLogout);
if (!window.location.pathname?.startsWith('/login')) { window.$dialog?.warning({
window.$dialog?.warning({ title: '系统提示',
title: '系统提示', content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录', positiveText: '重新登录',
positiveText: '重新登录', negativeText: '取消',
negativeText: '取消', maskClosable: false,
maskClosable: false, closeOnEsc: false,
closeOnEsc: false, onPositiveClick() {
onPositiveClick() { logoutAndCleanup();
logoutAndCleanup(); },
} onClose() {
}); window.removeEventListener('beforeunload', handleLogout);
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
request.cancelAllRequest(); }
} });
request.cancelAllRequest();
return null; return null;
} }

View File

@ -65,11 +65,13 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
routes.forEach(route => { routes.forEach(route => {
if (authRouteMode.value === 'dynamic') { if (authRouteMode.value === 'dynamic') {
if (route.path === '/') { if (route.path === '/' && route.children?.length) {
route.children?.forEach(child => { const child = route.children[0];
parseRouter(child); parseRouter(child);
authRoutesMap.set(child.name, child); child.name = Math.random().toString(36).slice(2, 12);
}); Object.assign(route, child);
delete route.children;
authRoutesMap.set(route.name, route);
return; return;
} }
parseRouter(route); parseRouter(route);

View File

@ -376,6 +376,7 @@ declare namespace App {
update: string; update: string;
updateSuccess: string; updateSuccess: string;
saveSuccess: string; saveSuccess: string;
noChange: string;
userCenter: string; userCenter: string;
yesOrNo: { yesOrNo: {
yes: string; yes: string;

View File

@ -78,12 +78,12 @@ export function humpToLine(str: string, line: string = '-') {
/** 判断是否为空 */ /** 判断是否为空 */
export function isNotNull(value: any) { export function isNotNull(value: any) {
return value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null'; return value !== undefined && value !== null && value !== '';
} }
/** 判断是否为空 */ /** 判断是否为空 */
export function isNull(value: any) { export function isNull(value: any) {
return value === undefined || value === null || value === '' || value === 'undefined' || value === 'null'; return value === undefined || value === null || value === '';
} }
/** 判断是否为图片类型 */ /** 判断是否为图片类型 */
@ -191,3 +191,13 @@ export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys
}); });
return searchParams; return searchParams;
} }
/** 判断两个数组是否相等 */
export function arraysEqualSet(arr1: Array<any>, arr2: Array<any>) {
return (
arr1.length === arr2.length &&
new Set(arr1).size === arr1.length &&
new Set(arr2).size === arr2.length &&
[...arr1].sort().join() === [...arr2].sort().join()
);
}

View File

@ -200,7 +200,7 @@ async function handleExport() {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -34,13 +34,13 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="user-search"> <NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="IP地址" path="ipaddr" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" label="IP地址" path="ipaddr" class="pr-24px">
<NInput v-model:value="model.ipaddr" placeholder="请输入IP地址" /> <NInput v-model:value="model.ipaddr" placeholder="请输入IP地址" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="用户账号" path="userName" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" label="用户账号" path="userName" class="pr-24px">
<NInput v-model:value="model.userName" placeholder="请输入用户账号" /> <NInput v-model:value="model.userName" placeholder="请输入用户账号" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24" class="pr-24px"> <NFormItemGi span="24 s:24 m:8" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -239,7 +239,7 @@ async function handleExport() {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -61,7 +61,7 @@ async function search() {
clearable clearable
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -234,7 +234,7 @@ async function handleRefreshCache() {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -77,7 +77,7 @@ async function search() {
clearable clearable
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24 s:12 m:12" label="创建时间" path="createTime" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" label="创建时间" path="createTime" class="pr-24px">
<NDatePicker <NDatePicker
v-model:formatted-value="dateRangeCreateTime" v-model:formatted-value="dateRangeCreateTime"
type="datetimerange" type="datetimerange"
@ -86,7 +86,7 @@ async function search() {
@update:formatted-value="onDateRangeCreateTimeUpdate" @update:formatted-value="onDateRangeCreateTimeUpdate"
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24 s:12 m:12" class="pr-24px"> <NFormItemGi span="24" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -36,10 +36,10 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="user-search"> <NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi span="8" :label="$t('page.system.dept.deptName')" path="deptName" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" :label="$t('page.system.dept.deptName')" path="deptName" class="pr-24px">
<NInput v-model:value="model.deptName" :placeholder="$t('page.system.dept.form.deptName.required')" /> <NInput v-model:value="model.deptName" :placeholder="$t('page.system.dept.form.deptName.required')" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8 " :label="$t('page.system.dept.status')" path="status" class="pr-24px"> <NFormItemGi span="24 s:12 m:8 " :label="$t('page.system.dept.status')" path="status" class="pr-24px">
<NSelect <NSelect
v-model:value="model.status" v-model:value="model.status"
:placeholder="$t('page.system.dept.form.status.required')" :placeholder="$t('page.system.dept.form.status.required')"
@ -47,7 +47,7 @@ async function search() {
clearable clearable
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -405,7 +405,7 @@ const selectable = computed(() => {
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
:dict-type="searchParams.dictType || ''" :dict-type="searchParams.dictType || ''"
@submitted="getDataByPage" @submitted="getData"
/> />
<DictTypeOperateDrawer <DictTypeOperateDrawer
v-model:visible="dictTypeDrawerVisible" v-model:visible="dictTypeDrawerVisible"

View File

@ -33,17 +33,16 @@ async function search() {
<NCollapse> <NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search"> <NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="self" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi <NFormItemGi
:show-feedback="false" span="24 s:12 m:12"
span="12"
:label="$t('page.system.dict.data.label')" :label="$t('page.system.dict.data.label')"
path="dictLabel" path="dictLabel"
class="pr-24px" class="pr-24px"
> >
<NInput v-model:value="model.dictLabel" :placeholder="$t('page.system.dict.form.dictLabel.required')" /> <NInput v-model:value="model.dictLabel" :placeholder="$t('page.system.dict.form.dictLabel.required')" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi :show-feedback="false" span="12" class="pr-24px"> <NFormItemGi span="24 s:12 m:12" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -194,7 +194,7 @@ async function edit(noticeId: CommonType.IdType) {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -33,13 +33,13 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="user-search"> <NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi span="8" label="公告标题" path="noticeTitle" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" label="公告标题" path="noticeTitle" class="pr-24px">
<NInput v-model:value="model.noticeTitle" placeholder="请输入公告标题" /> <NInput v-model:value="model.noticeTitle" placeholder="请输入公告标题" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8" label="公告类型" path="noticeType" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" label="公告类型" path="noticeType" class="pr-24px">
<DictSelect v-model:value="model.noticeType" dict-code="sys_notice_type" placeholder="请选择公告类型" /> <DictSelect v-model:value="model.noticeType" dict-code="sys_notice_type" placeholder="请选择公告类型" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -252,7 +252,7 @@ async function handleStatusChange(
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -49,6 +49,8 @@ const {
originalName: null, originalName: null,
fileSuffix: null, fileSuffix: null,
service: null, service: null,
isAsc: 'desc',
orderByColumn: 'createTime',
params: {} params: {}
}, },
columns: () => [ columns: () => [
@ -333,7 +335,7 @@ function handleToOssConfig() {
:pagination="mobilePagination" :pagination="mobilePagination"
class="sm:h-full" class="sm:h-full"
/> />
<OssUploadModal v-model:visible="uploadVisible" :upload-type="fileUploadType" @close="getData" /> <OssUploadModal v-model:visible="uploadVisible" :upload-type="fileUploadType" @close="getDataByPage" />
</NCard> </NCard>
</div> </div>
</template> </template>

View File

@ -28,7 +28,9 @@ const accept = computed(() => (props.uploadType === 'file' ? AcceptType.File : A
const fileList = ref<UploadFileInfo[]>([]); const fileList = ref<UploadFileInfo[]>([]);
function handleUpdateModelWhenUpload() {} function handleUpdateModelWhenUpload() {
fileList.value = [];
}
function closeDrawer() { function closeDrawer() {
visible.value = false; visible.value = false;

View File

@ -288,7 +288,7 @@ function handleResetSearch() {
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
:dept-data="deptData" :dept-data="deptData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -36,13 +36,13 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="user-search"> <NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi span="6 s:12 m:6" label="岗位编码" path="postCode" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" label="岗位编码" path="postCode" class="pr-24px">
<NInput v-model:value="model.postCode" placeholder="请输入岗位编码" /> <NInput v-model:value="model.postCode" placeholder="请输入岗位编码" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="6 s:12 m:6" label="岗位名称" path="postName" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" label="岗位名称" path="postName" class="pr-24px">
<NInput v-model:value="model.postName" placeholder="请输入岗位名称" /> <NInput v-model:value="model.postName" placeholder="请输入岗位名称" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="6 s:12 m:6" label="状态" path="status" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" label="状态" path="status" class="pr-24px">
<NSelect <NSelect
v-model:value="model.status" v-model:value="model.status"
placeholder="请选择状态" placeholder="请选择状态"
@ -50,7 +50,7 @@ async function search() {
clearable clearable
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="6" class="pr-24px"> <NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -273,7 +273,7 @@ function handleAuthUser(row: Api.System.Role) {
:data="data" :data="data"
size="small" size="small"
:flex-height="!appStore.isMobile" :flex-height="!appStore.isMobile"
:scroll-x="962" :scroll-x="1200"
:loading="loading" :loading="loading"
remote remote
:row-key="row => row.roleId" :row-key="row => row.roleId"
@ -284,14 +284,10 @@ function handleAuthUser(row: Api.System.Role) {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
<RoleDataScopeDrawer <RoleDataScopeDrawer v-model:visible="dataScopeDrawerVisible" :row-data="editingData" @submitted="getData" />
v-model:visible="dataScopeDrawerVisible" <RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getData" />
:row-data="editingData"
@submitted="getDataByPage"
/>
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getDataByPage" />
</NCard> </NCard>
</div> </div>
</template> </template>

View File

@ -1,9 +1,16 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { computed, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { fetchGetRoleUserList, fetchGetUserList } from '@/service/api/system'; import { NDatePicker } from 'naive-ui';
import {
fetchGetRoleUserList,
fetchGetUserList,
fetchUpdateRoleAuthUser,
fetchUpdateRoleAuthUserCancel
} from '@/service/api/system';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useDict } from '@/hooks/business/dict'; import { useDict } from '@/hooks/business/dict';
import { useTable, useTableOperate } from '@/hooks/common/table'; import { useTable, useTableOperate } from '@/hooks/common/table';
import { arraysEqualSet } from '@/utils/common';
import { $t } from '@/locales'; import { $t } from '@/locales';
import DictTag from '@/components/custom/dict-tag.vue'; import DictTag from '@/components/custom/dict-tag.vue';
@ -109,13 +116,16 @@ const { columns, data, getData, getDataByPage, loading, mobilePagination, search
const { checkedRowKeys } = useTableOperate(data, getData); const { checkedRowKeys } = useTableOperate(data, getData);
const checkedUserIds = ref<CommonType.IdType[]>([]);
async function handleUpdateModelWhenEdit() { async function handleUpdateModelWhenEdit() {
checkedRowKeys.value = []; checkedRowKeys.value = [];
getDataByPage(); getDataByPage();
const { data: roleUserList } = await fetchGetRoleUserList({ const { data: roleUserList } = await fetchGetRoleUserList({
roleId: props.rowData?.roleId roleId: props.rowData?.roleId
}); });
checkedRowKeys.value = roleUserList?.rows.map(item => item.userId) || []; checkedUserIds.value = roleUserList?.rows.map(item => item.userId) || [];
checkedRowKeys.value = checkedUserIds.value;
} }
function closeDrawer() { function closeDrawer() {
@ -123,6 +133,25 @@ function closeDrawer() {
} }
async function handleSubmit() { async function handleSubmit() {
if (arraysEqualSet(checkedUserIds.value, checkedRowKeys.value)) {
window.$message?.warning($t('common.noChange'));
return;
}
// 批量取消用户授权
const cancelUserIds = checkedUserIds.value.filter(item => !checkedRowKeys.value.includes(item));
if (cancelUserIds.length > 0) {
const { error: cancelError } = await fetchUpdateRoleAuthUserCancel(props.rowData!.roleId, cancelUserIds);
if (cancelError) return;
}
// 批量选择用户授权
const addUserIds = checkedRowKeys.value.filter(item => !checkedUserIds.value.includes(item));
if (addUserIds.length > 0) {
const { error: addError } = await fetchUpdateRoleAuthUser(props.rowData!.roleId, addUserIds);
if (addError) return;
}
window.$message?.success($t('common.updateSuccess')); window.$message?.success($t('common.updateSuccess'));
closeDrawer(); closeDrawer();
emit('submitted'); emit('submitted');
@ -133,6 +162,22 @@ watch(visible, () => {
handleUpdateModelWhenEdit(); handleUpdateModelWhenEdit();
} }
}); });
const dateRangeCreateTime = ref<[string, string] | null>(null);
const datePickerRef = ref<InstanceType<typeof NDatePicker>>();
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
if (value?.length) {
searchParams.params!.beginTime = value[0];
searchParams.params!.endTime = value[1];
}
}
function reset() {
dateRangeCreateTime.value = null;
resetSearchParams();
}
</script> </script>
<template> <template>
@ -158,9 +203,22 @@ watch(visible, () => {
<NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px"> <NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px">
<NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" /> <NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="24 s:12 m:24" class="pr-24px" :show-feedback="false"> <NFormItemGi span="24 s:12 m:8" label="所属部门" path="deptId" class="pr-24px">
<DeptTreeSelect v-model:value="searchParams.deptId" placeholder="请选择部门" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:10" label="创建时间" path="createTime" class="pr-24px">
<NDatePicker
ref="datePickerRef"
v-model:formatted-value="dateRangeCreateTime"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
@update:formatted-value="onDateRangeCreateTimeUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px" :show-feedback="false">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="resetSearchParams"> <NButton @click="reset">
<template #icon> <template #icon>
<icon-ic-round-refresh class="text-icon" /> <icon-ic-round-refresh class="text-icon" />
</template> </template>

View File

@ -233,7 +233,7 @@ async function handleStatusChange(
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -72,8 +72,11 @@ async function handleUpdateModelWhenEdit() {
model.menuIds = []; model.menuIds = [];
if (props.operateType === 'add') { if (props.operateType === 'add') {
menuTreeRef.value?.refresh();
Object.assign(model, createDefaultModel()); Object.assign(model, createDefaultModel());
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(0);
if (error) return;
model.menuIds = data.checkedKeys;
menuOptions.value = data.menus;
return; return;
} }
@ -145,7 +148,7 @@ watch(visible, () => {
v-model:options="menuOptions" v-model:options="menuOptions"
v-model:cascade="model.menuCheckStrictly" v-model:cascade="model.menuCheckStrictly"
v-model:loading="menuLoading" v-model:loading="menuLoading"
:immediate="operateType === 'add'" :immediate="false"
/> />
</NFormItem> </NFormItem>
<NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark"> <NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark">

View File

@ -38,7 +38,7 @@ async function search() {
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive> <NGrid responsive="screen" item-responsive>
<NFormItemGi <NFormItemGi
span="8" span="24 s:12 m:8"
:label="$t('page.system.tenantPackage.packageName')" :label="$t('page.system.tenantPackage.packageName')"
path="packageName" path="packageName"
class="pr-24px" class="pr-24px"
@ -48,7 +48,12 @@ async function search() {
:placeholder="$t('page.system.tenantPackage.form.packageName.required')" :placeholder="$t('page.system.tenantPackage.form.packageName.required')"
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8" :label="$t('page.system.tenantPackage.status')" path="status" class="pr-24px"> <NFormItemGi
span="24 s:12 m:8"
:label="$t('page.system.tenantPackage.status')"
path="status"
class="pr-24px"
>
<NSelect <NSelect
v-model:value="model.status" v-model:value="model.status"
:placeholder="$t('page.system.tenantPackage.form.status.required')" :placeholder="$t('page.system.tenantPackage.form.status.required')"
@ -56,7 +61,7 @@ async function search() {
clearable clearable
/> />
</NFormItemGi> </NFormItemGi>
<NFormItemGi span="8" class="pr-24px"> <NFormItemGi span="24 s:24 m:8" class="pr-24px">
<NSpace class="w-full" justify="end"> <NSpace class="w-full" justify="end">
<NButton @click="reset"> <NButton @click="reset">
<template #icon> <template #icon>

View File

@ -261,7 +261,7 @@ async function handleExport() {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="getData"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -348,7 +348,7 @@ function handleResetSearch() {
:pagination="mobilePagination" :pagination="mobilePagination"
class="h-full" class="h-full"
/> />
<UserImportModal v-model:visible="importVisible" @submitted="getDataByPage" /> <UserImportModal v-model:visible="importVisible" @submitted="getData" />
<UserOperateDrawer <UserOperateDrawer
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"

View File

@ -142,8 +142,12 @@ watch(visible, () => {
<div class="flex-center"> <div class="flex-center">
<NCheckbox v-model="data.updateSupport">{{ $t('common.updateExisting') }}</NCheckbox> <NCheckbox v-model="data.updateSupport">{{ $t('common.updateExisting') }}</NCheckbox>
</div> </div>
<NAlert v-if="message" :title="$t('common.importResult')" :type="success ? 'success' : 'error'" :bordered="false"> <NAlert v-if="message" :title="$t('common.importResult')" :type="success ? 'success' : 'error'" :bordered="false">
{{ message }} <NScrollbar class="max-h-200px">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="message" />
</NScrollbar>
</NAlert> </NAlert>
<template #footer> <template #footer>
<NSpace justify="end" :size="16"> <NSpace justify="end" :size="16">

View File

@ -65,14 +65,15 @@ function createDefaultModel(): Model {
}; };
} }
type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'status' | 'phonenumber'>; type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'status' | 'phonenumber' | 'roleIds'>;
const rules: Record<RuleKey, App.Global.FormRule[]> = { const rules: Record<RuleKey, App.Global.FormRule[]> = {
userName: [createRequiredRule($t('page.system.user.form.userName.required'))], userName: [createRequiredRule($t('page.system.user.form.userName.required'))],
nickName: [createRequiredRule($t('page.system.user.form.nickName.required'))], nickName: [createRequiredRule($t('page.system.user.form.nickName.required'))],
password: [{ ...patternRules.pwd, required: props.operateType === 'add' }], password: [{ ...patternRules.pwd, required: props.operateType === 'add' }],
phonenumber: [patternRules.phone], phonenumber: [patternRules.phone],
status: [createRequiredRule($t('page.system.user.form.status.required'))] status: [createRequiredRule($t('page.system.user.form.status.required'))],
roleIds: [{ ...createRequiredRule('请选择角色'), type: 'array' }]
}; };
async function getUserInfo() { async function getUserInfo() {

View File

@ -313,8 +313,8 @@ getDataNames();
:pagination="mobilePagination" :pagination="mobilePagination"
class="sm:h-full" class="sm:h-full"
/> />
<GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getDataByPage" /> <GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getData" />
<GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getDataByPage" /> <GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getData" />
<GenTablePreviewDrawer <GenTablePreviewDrawer
v-model:visible="previewVisible" v-model:visible="previewVisible"
:row-data="editingData" :row-data="editingData"