Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e5ec915fd9 | ||
![]() |
89c716e12a | ||
![]() |
2d02128164 | ||
![]() |
9ca7ca8fda | ||
![]() |
d85424ee83 | ||
![]() |
ff87415d7b | ||
![]() |
312709706b | ||
![]() |
3ae9922dc4 | ||
![]() |
247b98a542 | ||
![]() |
566b2c2db8 | ||
![]() |
90d32ee29a | ||
![]() |
efc953c094 | ||
![]() |
ad48d8e840 | ||
![]() |
62f2c6d571 | ||
![]() |
03dd64c543 | ||
![]() |
aeb6369005 | ||
![]() |
133196f337 | ||
![]() |
41191d54fb | ||
![]() |
229e00443f | ||
![]() |
85c8a9fffa | ||
![]() |
b99999355c |
@ -16,7 +16,7 @@
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
"args": ["@playwright/mcp@0.0.29"]
|
||||
},
|
||||
"mcp-server-time": {
|
||||
"command": "uvx",
|
||||
@ -26,7 +26,7 @@
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data",
|
||||
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -11,6 +11,9 @@
|
||||
"i18n-ally.editor.preferEditor": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"prettier.enable": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"unocss.root": ["./"],
|
||||
|
65
CHANGELOG.md
65
CHANGELOG.md
@ -1,5 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
# 更新日志
|
||||
|
||||
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- **hooks**:
|
||||
- 重构下载方法,支持流式下载 - by @m-xlsea [<samp>(65067)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/650673e2)
|
||||
- **projects**:
|
||||
- 角色分配用户新增部门与时间查询条件 - by @m-xlsea [<samp>(ad48d)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
|
||||
- 修改操作后列表查询方式 - by @m-xlsea [<samp>(d8542)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/d85424ee)
|
||||
|
||||
### 🐞 Bug 修复
|
||||
|
||||
- **hooks**:
|
||||
- 解决 streamsaver 访问不到 Github 资源问题 - by @m-xlsea [<samp>(566b2)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/566b2c2d)
|
||||
- **other**:
|
||||
- 修复代码生成类型定义文件重复问题 - by @m-xlsea [<samp>(f7c7f)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
|
||||
- **packages**:
|
||||
- 修复 cleanup 会删除富文本编辑器资源问题 - by @m-xlsea [<samp>(9ca7c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
|
||||
- **projects**:
|
||||
- 修复字典数据重复获取问题 - by @m-xlsea [<samp>(3628c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/3628c249)
|
||||
- 修改强退在线设备接口 - by **AN** [<samp>(dbcf8)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
|
||||
- 修复代码生成逻辑判断问题 - by **AN** [<samp>(6fc7b)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
|
||||
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A - by @m-xlsea [<samp>(ad938)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ad9386eb)
|
||||
- 修复未清空文件列表,上传回显问题 - by **AN** [<samp>(229e0)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/229e0044)
|
||||
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 - by @xiaobao0505 in https://github.com/m-xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/41191d54)
|
||||
- 修复角色列表操作栏展示不全问题 - by @m-xlsea [<samp>(62f2c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
|
||||
- 修复用户导入结果信息未渲染标签问题 - by **AN** [<samp>(efc95)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/efc953c0)
|
||||
- 修复角色用户分配未调用接口问题 - by @m-xlsea [<samp>(ff874)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ff87415d)
|
||||
- **styles**:
|
||||
- 修复登录页平板界面滚动问题 - by @m-xlsea [<samp>(90145)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/90145fa5)
|
||||
- **utils**:
|
||||
- 修复isNull和IsNotNull判断方法潜在问题 - by **AN** [<samp>(90d32)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/90d32ee2)
|
||||
|
||||
### 💅 重构
|
||||
|
||||
- **projects**: 调整租户套餐菜单接口 - by **AN** [<samp>(b9999)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/b9999935)
|
||||
|
||||
### 📖 文档
|
||||
|
||||
- **other**: 修改文档内容 - by @m-xlsea [<samp>(3ae99)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/3ae9922d)
|
||||
- **projects**: 优化 cursor 规则及 mcp - by @m-xlsea [<samp>(a3199)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/a31994dc)
|
||||
- **readme**: 更新 README.md 文件 - by @m-xlsea [<samp>(99675)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/99675cbc)
|
||||
|
||||
### 🏡 杂项
|
||||
|
||||
- **deps**:
|
||||
- update NodeJS and pnpm version requirements in package.json and documentation - by **Junior25306** [<samp>(a5c4b)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
|
||||
- update deps - by @soybeanjs [<samp>(5cb1c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
|
||||
- update deps - by @soybeanjs [<samp>(aeb63)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/aeb63690)
|
||||
- update deps - by @m-xlsea [<samp>(89c71)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/89c716e1)
|
||||
- **packages**:
|
||||
- update Vite version to 7 in package.json and documentation. - by **Azir** [<samp>(03dd6)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/03dd64c5)
|
||||
- **projects**:
|
||||
- update pnpm-lock.yaml - by @m-xlsea [<samp>(7c6ca)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
|
||||
- **vscode**:
|
||||
- remove unused vue.server.hybridMode setting from .vscode/settings.json - by @soybeanjs [<samp>(13319)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/133196f3)
|
||||
|
||||
### ❤️ 贡献值
|
||||
|
||||
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://github.com/xiaobao0505) [](https://gitee.com/elio-an) [](https://github.com/Azir-11) [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)
|
||||
|
||||
### 🚀 新功能
|
||||
@ -90,7 +153,7 @@
|
||||
|
||||
### ❤️ 贡献者
|
||||
|
||||
[](https://gitee.com/xlsea) [](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://gitee.com/elio-an) [](https://github.com/chen-ziwen)
|
||||
[](https://gitee.com/xlsea) [](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://gitee.com/elio-an) [](https://github.com/chen-ziwen)
|
||||
[](https://gitee.com/wangzhongqi0917) [](https://gitee.com/qq1822213252) [](https://gitee.com/tangzc), [metabytes](https://gitee.com/metabytes)
|
||||
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
# 📢 重要通知
|
||||
|
||||
1.1.0 版本已发布,但仍然建议:
|
||||
1.1.1 版本已发布,但仍然建议:
|
||||
- 在生产环境使用前进行充分测试
|
||||
- 关注项目更新,及时获取最新版本
|
||||
- 积极反馈问题,帮助我们快速迭代
|
||||
@ -119,8 +119,8 @@ root
|
||||
## 🚀 环境要求与安装
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 18.20.0
|
||||
- pnpm >= 8.7.0
|
||||
- Node.js >= 20.19.0
|
||||
- pnpm >= 10.5.0
|
||||
- Git
|
||||
|
||||
### 安装步骤及说明
|
||||
|
28
package.json
28
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ruoyi-vue-plus",
|
||||
"type": "module",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
|
||||
"author": {
|
||||
"name": "xlsea",
|
||||
@ -21,7 +21,7 @@
|
||||
"Soybean Admin",
|
||||
"Vue3 admin ",
|
||||
"vue-admin-template",
|
||||
"Vite6",
|
||||
"Vite7",
|
||||
"TypeScript",
|
||||
"naive-ui",
|
||||
"naive-ui-admin",
|
||||
@ -66,7 +66,7 @@
|
||||
"@sa/tinymce": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@types/streamsaver": "^2.0.5",
|
||||
"@vueuse/core": "13.4.0",
|
||||
"@vueuse/core": "13.5.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.13",
|
||||
"defu": "6.1.4",
|
||||
@ -83,16 +83,16 @@
|
||||
"vue": "3.5.17",
|
||||
"vue-advanced-cropper": "^2.8.9",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.1.7",
|
||||
"vue-i18n": "11.1.9",
|
||||
"vue-router": "4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.353",
|
||||
"@iconify/json": "2.2.354",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.0",
|
||||
"@types/node": "24.0.4",
|
||||
"@types/node": "24.0.10",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.3.2",
|
||||
"@unocss/preset-icons": "66.3.2",
|
||||
@ -101,25 +101,25 @@
|
||||
"@unocss/transformer-variant-group": "66.3.2",
|
||||
"@unocss/vite": "66.3.2",
|
||||
"@vitejs/plugin-vue": "6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "5.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "5.0.1",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.29.0",
|
||||
"eslint-plugin-vue": "10.2.0",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-plugin-vue": "10.3.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.7.0",
|
||||
"vite": "7.0.0",
|
||||
"unplugin-vue-components": "28.8.0",
|
||||
"vite": "7.0.1",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"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-vue-devtools": "7.7.7",
|
||||
"vue-eslint-parser": "10.1.4",
|
||||
"vue-tsc": "2.2.10"
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.1"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
@ -15,6 +15,6 @@
|
||||
"dependencies": {
|
||||
"@alova/mock": "2.0.17",
|
||||
"@sa/utils": "workspace:*",
|
||||
"alova": "3.3.3"
|
||||
"alova": "3.3.4"
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"simplebar-vue": "2.4.1"
|
||||
"simplebar-vue": "2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.9.1"
|
||||
|
@ -5,7 +5,7 @@ import type { CliOption } from '../types';
|
||||
const defaultOptions: CliOption = {
|
||||
cwd: process.cwd(),
|
||||
cleanupDirs: [
|
||||
'**/dist',
|
||||
'dist',
|
||||
'**/package-lock.json',
|
||||
'**/yarn.lock',
|
||||
'**/pnpm-lock.yaml',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/tinymce",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
1502
pnpm-lock.yaml
1502
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
179
public/streamsaver/mitm.html
Normal file
179
public/streamsaver/mitm.html
Normal 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
132
public/streamsaver/sw.js
Normal 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' });
|
||||
};
|
@ -20,44 +20,45 @@ const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
async function handleFetchOssList(ossIds: string[]) {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
endLoading();
|
||||
try {
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
} catch (error) {
|
||||
window.$message?.error(`获取文件列表失败: ${error}`);
|
||||
} finally {
|
||||
endLoading();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
value,
|
||||
async val => {
|
||||
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) {
|
||||
fileList.value = [];
|
||||
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);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
fileList,
|
||||
val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
watch(fileList, val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -51,6 +51,7 @@ export function useDownload() {
|
||||
contentLength?: number
|
||||
): Promise<void> {
|
||||
window.$loading?.endLoading();
|
||||
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
|
||||
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
||||
|
||||
if (window.WritableStream && readableStream?.pipeTo) {
|
||||
|
@ -2,6 +2,7 @@ import { ref, toValue } from 'vue';
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
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';
|
||||
|
||||
export function useFormRules() {
|
||||
@ -52,7 +53,7 @@ export function useFormRules() {
|
||||
required: true,
|
||||
trigger: ['input', 'blur'],
|
||||
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 true;
|
||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
||||
update: 'Update',
|
||||
saveSuccess: 'Save Success',
|
||||
updateSuccess: 'Update Success',
|
||||
noChange: 'No actions were taken',
|
||||
userCenter: 'User Center',
|
||||
yesOrNo: {
|
||||
yes: 'Yes',
|
||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
||||
update: '更新',
|
||||
saveSuccess: '保存成功',
|
||||
updateSuccess: '更新成功',
|
||||
noChange: '没有进行任何操作',
|
||||
userCenter: '个人中心',
|
||||
yesOrNo: {
|
||||
yes: '是',
|
||||
|
@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
|
||||
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(',') }
|
||||
});
|
||||
}
|
||||
|
1
src/typings/app.d.ts
vendored
1
src/typings/app.d.ts
vendored
@ -376,6 +376,7 @@ declare namespace App {
|
||||
update: string;
|
||||
updateSuccess: string;
|
||||
saveSuccess: string;
|
||||
noChange: string;
|
||||
userCenter: string;
|
||||
yesOrNo: {
|
||||
yes: string;
|
||||
|
@ -78,12 +78,12 @@ export function humpToLine(str: string, line: string = '-') {
|
||||
|
||||
/** 判断是否为空 */
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
/** 判断两个数组是否相等 */
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -239,7 +239,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -234,7 +234,7 @@ async function handleRefreshCache() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -405,7 +405,7 @@ const selectable = computed(() => {
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:dict-type="searchParams.dictType || ''"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
<DictTypeOperateDrawer
|
||||
v-model:visible="dictTypeDrawerVisible"
|
||||
|
@ -194,7 +194,7 @@ async function edit(noticeId: CommonType.IdType) {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -252,7 +252,7 @@ async function handleStatusChange(
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -49,6 +49,8 @@ const {
|
||||
originalName: null,
|
||||
fileSuffix: null,
|
||||
service: null,
|
||||
isAsc: 'desc',
|
||||
orderByColumn: 'createTime',
|
||||
params: {}
|
||||
},
|
||||
columns: () => [
|
||||
@ -333,7 +335,7 @@ function handleToOssConfig() {
|
||||
:pagination="mobilePagination"
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -28,7 +28,9 @@ const accept = computed(() => (props.uploadType === 'file' ? AcceptType.File : A
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
function handleUpdateModelWhenUpload() {}
|
||||
function handleUpdateModelWhenUpload() {
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
|
@ -288,7 +288,7 @@ function handleResetSearch() {
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:dept-data="deptData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -273,7 +273,7 @@ function handleAuthUser(row: Api.System.Role) {
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="962"
|
||||
:scroll-x="1200"
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.roleId"
|
||||
@ -284,14 +284,10 @@ function handleAuthUser(row: Api.System.Role) {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
<RoleDataScopeDrawer
|
||||
v-model:visible="dataScopeDrawerVisible"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getDataByPage" />
|
||||
<RoleDataScopeDrawer v-model:visible="dataScopeDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,9 +1,16 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, watch } from 'vue';
|
||||
import { fetchGetRoleUserList, fetchGetUserList } from '@/service/api/system';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { NDatePicker } from 'naive-ui';
|
||||
import {
|
||||
fetchGetRoleUserList,
|
||||
fetchGetUserList,
|
||||
fetchUpdateRoleAuthUser,
|
||||
fetchUpdateRoleAuthUserCancel
|
||||
} from '@/service/api/system';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { arraysEqualSet } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
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 checkedUserIds = ref<CommonType.IdType[]>([]);
|
||||
|
||||
async function handleUpdateModelWhenEdit() {
|
||||
checkedRowKeys.value = [];
|
||||
getDataByPage();
|
||||
const { data: roleUserList } = await fetchGetRoleUserList({
|
||||
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() {
|
||||
@ -123,6 +133,25 @@ function closeDrawer() {
|
||||
}
|
||||
|
||||
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'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
@ -133,6 +162,22 @@ watch(visible, () => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -158,9 +203,22 @@ watch(visible, () => {
|
||||
<NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px">
|
||||
<NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" />
|
||||
</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">
|
||||
<NButton @click="resetSearchParams">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
|
@ -233,7 +233,7 @@ async function handleStatusChange(
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -72,8 +72,11 @@ async function handleUpdateModelWhenEdit() {
|
||||
model.menuIds = [];
|
||||
|
||||
if (props.operateType === 'add') {
|
||||
menuTreeRef.value?.refresh();
|
||||
Object.assign(model, createDefaultModel());
|
||||
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(0);
|
||||
if (error) return;
|
||||
model.menuIds = data.checkedKeys;
|
||||
menuOptions.value = data.menus;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -145,7 +148,7 @@ watch(visible, () => {
|
||||
v-model:options="menuOptions"
|
||||
v-model:cascade="model.menuCheckStrictly"
|
||||
v-model:loading="menuLoading"
|
||||
:immediate="operateType === 'add'"
|
||||
:immediate="false"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark">
|
||||
|
@ -261,7 +261,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -348,7 +348,7 @@ function handleResetSearch() {
|
||||
:pagination="mobilePagination"
|
||||
class="h-full"
|
||||
/>
|
||||
<UserImportModal v-model:visible="importVisible" @submitted="getDataByPage" />
|
||||
<UserImportModal v-model:visible="importVisible" @submitted="getData" />
|
||||
<UserOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
|
@ -142,8 +142,12 @@ watch(visible, () => {
|
||||
<div class="flex-center">
|
||||
<NCheckbox v-model="data.updateSupport">{{ $t('common.updateExisting') }}</NCheckbox>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<template #footer>
|
||||
<NSpace justify="end" :size="16">
|
||||
|
@ -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[]> = {
|
||||
userName: [createRequiredRule($t('page.system.user.form.userName.required'))],
|
||||
nickName: [createRequiredRule($t('page.system.user.form.nickName.required'))],
|
||||
password: [{ ...patternRules.pwd, required: props.operateType === 'add' }],
|
||||
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() {
|
||||
|
@ -313,8 +313,8 @@ getDataNames();
|
||||
:pagination="mobilePagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getDataByPage" />
|
||||
<GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getDataByPage" />
|
||||
<GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getData" />
|
||||
<GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getData" />
|
||||
<GenTablePreviewDrawer
|
||||
v-model:visible="previewVisible"
|
||||
:row-data="editingData"
|
||||
|
Loading…
Reference in New Issue
Block a user