Compare commits

...

21 Commits

Author SHA1 Message Date
xlsea
e5ec915fd9 chore(projects): release v1.1.1 2025-07-11 18:04:51 +08:00
xlsea
89c716e12a chore(deps): update deps 2025-07-11 17:49:57 +08:00
xlsea
2d02128164 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	.vscode/settings.json
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-07-11 17:46:01 +08:00
xlsea
9ca7ca8fda fix(packages): 修复 cleanup 会删除富文本编辑器资源问题 2025-07-11 17:40:29 +08:00
xlsea
d85424ee83 feat(projects): 修改操作后列表查询方式 2025-07-11 17:16:03 +08:00
xlsea
ff87415d7b fix(projects): 修复角色用户分配未调用接口问题 2025-07-10 13:55:07 +08:00
xlsea
312709706b Merge remote-tracking branch 'origin/dev' into dev 2025-07-09 09:38:12 +08:00
xlsea
3ae9922dc4 docs(other): 修改文档内容 2025-07-09 09:30:46 +08:00
xlsea
247b98a542 Merge remote-tracking branch 'origin/dev' into dev 2025-07-08 23:06:21 +08:00
xlsea
566b2c2db8 fix(hooks): 解决 streamsaver 访问不到 Github 资源问题 2025-07-08 23:05:56 +08:00
AN
90d32ee29a fix(utils): 修复isNull和IsNotNull判断方法潜在问题 2025-07-08 22:09:45 +08:00
AN
efc953c094 fix(projects): 修复用户导入结果信息未渲染标签问题 2025-07-08 17:39:44 +08:00
xlsea
ad48d8e840 feat(projects): 角色分配用户新增部门与时间查询条件 2025-07-04 15:37:11 +08:00
xlsea
62f2c6d571 fix(projects): 修复角色列表操作栏展示不全问题 2025-07-04 15:19:35 +08:00
Azir
03dd64c543 chore(packages): update Vite version to 7 in package.json and documentation.
(cherry picked from commit ef806edd9d0c48ad8669863516d52e2eb8870d6f)
2025-07-03 22:08:50 +08:00
Soybean
aeb6369005 chore(deps): update deps 2025-07-03 22:06:30 +08:00
Soybean
133196f337 chore(vscode): remove unused vue.server.hybridMode setting from .vscode/settings.json 2025-07-03 22:02:30 +08:00
xiaobao
41191d54fb
fix(projects): Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 2025-07-03 21:59:18 +08:00
AN
229e00443f fix(projects): 修复未清空文件列表,上传回显问题 2025-07-02 10:26:42 +08:00
AN
85c8a9fffa Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-01 19:09:27 +08:00
AN
b99999355c refactor(projects): 调整租户套餐菜单接口 2025-07-01 19:06:02 +08:00
38 changed files with 1361 additions and 782 deletions

View File

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

View File

@ -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": ["./"],

View File

@ -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**:
- 重构下载方法,支持流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(65067)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/650673e2)
- **projects**:
- 角色分配用户新增部门与时间查询条件 &nbsp;-&nbsp; by @m-xlsea [<samp>(ad48d)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
- 修改操作后列表查询方式 &nbsp;-&nbsp; by @m-xlsea [<samp>(d8542)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/d85424ee)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **hooks**:
- 解决 streamsaver 访问不到 Github 资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(566b2)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/566b2c2d)
- **other**:
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7f)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **packages**:
- 修复 cleanup 会删除富文本编辑器资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(9ca7c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
- **projects**:
- 修复字典数据重复获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(3628c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/3628c249)
- 修改强退在线设备接口 &nbsp;-&nbsp; by **AN** [<samp>(dbcf8)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
- 修复代码生成逻辑判断问题 &nbsp;-&nbsp; 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 &nbsp;-&nbsp; by @m-xlsea [<samp>(ad938)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ad9386eb)
- 修复未清空文件列表,上传回显问题 &nbsp;-&nbsp; 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 &nbsp;-&nbsp; 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)
- 修复角色列表操作栏展示不全问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(62f2c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
- 修复用户导入结果信息未渲染标签问题 &nbsp;-&nbsp; by **AN** [<samp>(efc95)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/efc953c0)
- 修复角色用户分配未调用接口问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ff874)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/ff87415d)
- **styles**:
- 修复登录页平板界面滚动问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(90145)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/90145fa5)
- **utils**:
- 修复isNull和IsNotNull判断方法潜在问题 &nbsp;-&nbsp; by **AN** [<samp>(90d32)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/90d32ee2)
### &nbsp;&nbsp;&nbsp;💅 重构
- **projects**: 调整租户套餐菜单接口 &nbsp;-&nbsp; by **AN** [<samp>(b9999)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/b9999935)
### &nbsp;&nbsp;&nbsp;📖 文档
- **other**: 修改文档内容 &nbsp;-&nbsp; by @m-xlsea [<samp>(3ae99)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/3ae9922d)
- **projects**: 优化 cursor 规则及 mcp &nbsp;-&nbsp; by @m-xlsea [<samp>(a3199)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/a31994dc)
- **readme**: 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675)</samp>](https://github.com/m-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://github.com/m-xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(5cb1c)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(aeb63)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/aeb63690)
- update deps &nbsp;-&nbsp; 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. &nbsp;-&nbsp; by **Azir** [<samp>(03dd6)</samp>](https://github.com/m-xlsea/ruoyi-plus-soybean/commit/03dd64c5)
- **projects**:
- update pnpm-lock.yaml &nbsp;-&nbsp; 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 &nbsp;-&nbsp; by @soybeanjs [<samp>(13319)</samp>](https://github.com/m-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)
### &nbsp;&nbsp;&nbsp;🚀 新功能
@ -90,7 +153,7 @@
### &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)

View File

@ -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
### 安装步骤及说明

View File

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

View File

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

View File

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

View File

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

View File

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

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[]) {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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[]> = {
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() {

View File

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