From 566b2c2db8ced2725379ee0d6b79812b7c4c3725 Mon Sep 17 00:00:00 2001 From: xlsea Date: Tue, 8 Jul 2025 23:05:56 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(hooks):=20=E8=A7=A3=E5=86=B3=20streamsa?= =?UTF-8?q?ver=20=E8=AE=BF=E9=97=AE=E4=B8=8D=E5=88=B0=20Github=20=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/streamsaver/mitm.html | 179 +++++++++++++++++++++++++++++++++ public/streamsaver/sw.js | 132 ++++++++++++++++++++++++ src/hooks/business/download.ts | 1 + 3 files changed, 312 insertions(+) create mode 100644 public/streamsaver/mitm.html create mode 100644 public/streamsaver/sw.js 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) { From 3ae9922dc4dee01124990fd0faaf71e324654d68 Mon Sep 17 00:00:00 2001 From: xlsea Date: Wed, 9 Jul 2025 09:30:46 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(other):=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/mcp.json | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 ### 安装步骤及说明 From ff87415d7bddb06de9ef599ab3266b5c1fb875b2 Mon Sep 17 00:00:00 2001 From: xlsea Date: Thu, 10 Jul 2025 13:55:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(projects):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E7=94=A8=E6=88=B7=E5=88=86=E9=85=8D=E6=9C=AA?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=8E=A5=E5=8F=A3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/langs/en-us.ts | 1 + src/locales/langs/zh-cn.ts | 1 + src/service/api/system/role.ts | 18 +++++++++++ src/typings/app.d.ts | 1 + src/utils/common.ts | 10 ++++++ .../role/modules/role-auth-user-drawer.vue | 32 +++++++++++++++++-- 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 17c811f6..3928cbae 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 6c87c818..449a839c 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 1888dd27..47be6e4d 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -191,3 +191,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 @@