diff --git a/.cursor/mcp.json b/.cursor/mcp.json
index 5c4d03bb..8a042511 100644
--- a/.cursor/mcp.json
+++ b/.cursor/mcp.json
@@ -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"
}
diff --git a/README.md b/README.md
index 484050dc..7b0d6f35 100644
--- a/README.md
+++ b/README.md
@@ -119,8 +119,8 @@ root
## 🚀 环境要求与安装
### 环境要求
-- Node.js >= 18.20.0
-- pnpm >= 8.7.0
+- Node.js >= 20.19.0
+- pnpm >= 10.5.0
- Git
### 安装步骤及说明
diff --git a/public/streamsaver/mitm.html b/public/streamsaver/mitm.html
new file mode 100644
index 00000000..f6d94e2f
--- /dev/null
+++ b/public/streamsaver/mitm.html
@@ -0,0 +1,179 @@
+
+
diff --git a/public/streamsaver/sw.js b/public/streamsaver/sw.js
new file mode 100644
index 00000000..f6cb4c3a
--- /dev/null
+++ b/public/streamsaver/sw.js
@@ -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' });
+};
diff --git a/src/hooks/business/download.ts b/src/hooks/business/download.ts
index 210e1253..060b4579 100644
--- a/src/hooks/business/download.ts
+++ b/src/hooks/business/download.ts
@@ -51,6 +51,7 @@ export function useDownload() {
contentLength?: number
): Promise {
window.$loading?.endLoading();
+ StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
if (window.WritableStream && readableStream?.pipeTo) {
diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts
index 5e0fee28..932471ee 100644
--- a/src/locales/langs/en-us.ts
+++ b/src/locales/langs/en-us.ts
@@ -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',
diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts
index f3386b2a..47915fe1 100644
--- a/src/locales/langs/zh-cn.ts
+++ b/src/locales/langs/zh-cn.ts
@@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: '更新',
saveSuccess: '保存成功',
updateSuccess: '更新成功',
+ noChange: '没有进行任何操作',
userCenter: '个人中心',
yesOrNo: {
yes: '是',
diff --git a/src/service/api/system/role.ts b/src/service/api/system/role.ts
index 378f658e..9aea88df 100644
--- a/src/service/api/system/role.ts
+++ b/src/service/api/system/role.ts
@@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
params
});
}
+
+/** 批量选择用户授权 */
+export function fetchUpdateRoleAuthUser(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
+ return request({
+ url: '/system/role/authUser/selectAll',
+ method: 'put',
+ params: { roleId, userIds: userIds.join(',') }
+ });
+}
+
+/** 批量取消用户授权 */
+export function fetchUpdateRoleAuthUserCancel(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
+ return request({
+ url: '/system/role/authUser/cancelAll',
+ method: 'put',
+ params: { roleId, userIds: userIds.join(',') }
+ });
+}
diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts
index d05303b0..d90bbbdd 100644
--- a/src/typings/app.d.ts
+++ b/src/typings/app.d.ts
@@ -376,6 +376,7 @@ declare namespace App {
update: string;
updateSuccess: string;
saveSuccess: string;
+ noChange: string;
userCenter: string;
yesOrNo: {
yes: string;
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 6a7254ce..a6d717c8 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -220,3 +220,13 @@ export function transformToURLSearchParams(obj: Record, excludeKeys
});
return searchParams;
}
+
+/** 判断两个数组是否相等 */
+export function arraysEqualSet(arr1: Array, arr2: Array) {
+ return (
+ arr1.length === arr2.length &&
+ new Set(arr1).size === arr1.length &&
+ new Set(arr2).size === arr2.length &&
+ [...arr1].sort().join() === [...arr2].sort().join()
+ );
+}
diff --git a/src/views/system/role/modules/role-auth-user-drawer.vue b/src/views/system/role/modules/role-auth-user-drawer.vue
index f599a9fa..6bb9e890 100644
--- a/src/views/system/role/modules/role-auth-user-drawer.vue
+++ b/src/views/system/role/modules/role-auth-user-drawer.vue
@@ -1,10 +1,16 @@