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