mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow
This commit is contained in:
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["@playwright/mcp@latest"]
|
"args": ["@playwright/mcp@0.0.29"]
|
||||||
},
|
},
|
||||||
"mcp-server-time": {
|
"mcp-server-time": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||||
"env": {
|
"env": {
|
||||||
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data",
|
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
|
||||||
"TEMPLATES_USE": "en",
|
"TEMPLATES_USE": "en",
|
||||||
"ENABLE_GUI": "false"
|
"ENABLE_GUI": "false"
|
||||||
}
|
}
|
||||||
|
@ -119,8 +119,8 @@ root
|
|||||||
## 🚀 环境要求与安装
|
## 🚀 环境要求与安装
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
- Node.js >= 18.20.0
|
- Node.js >= 20.19.0
|
||||||
- pnpm >= 8.7.0
|
- pnpm >= 10.5.0
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
### 安装步骤及说明
|
### 安装步骤及说明
|
||||||
|
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' });
|
||||||
|
};
|
@ -51,6 +51,7 @@ export function useDownload() {
|
|||||||
contentLength?: number
|
contentLength?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
window.$loading?.endLoading();
|
window.$loading?.endLoading();
|
||||||
|
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
|
||||||
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
||||||
|
|
||||||
if (window.WritableStream && readableStream?.pipeTo) {
|
if (window.WritableStream && readableStream?.pipeTo) {
|
||||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
|||||||
update: 'Update',
|
update: 'Update',
|
||||||
saveSuccess: 'Save Success',
|
saveSuccess: 'Save Success',
|
||||||
updateSuccess: 'Update Success',
|
updateSuccess: 'Update Success',
|
||||||
|
noChange: 'No actions were taken',
|
||||||
userCenter: 'User Center',
|
userCenter: 'User Center',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
|||||||
update: '更新',
|
update: '更新',
|
||||||
saveSuccess: '保存成功',
|
saveSuccess: '保存成功',
|
||||||
updateSuccess: '更新成功',
|
updateSuccess: '更新成功',
|
||||||
|
noChange: '没有进行任何操作',
|
||||||
userCenter: '个人中心',
|
userCenter: '个人中心',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: '是',
|
yes: '是',
|
||||||
|
@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
|
|||||||
params
|
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;
|
update: string;
|
||||||
updateSuccess: string;
|
updateSuccess: string;
|
||||||
saveSuccess: string;
|
saveSuccess: string;
|
||||||
|
noChange: string;
|
||||||
userCenter: string;
|
userCenter: string;
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: string;
|
yes: string;
|
||||||
|
@ -220,3 +220,13 @@ export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys
|
|||||||
});
|
});
|
||||||
return searchParams;
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { NDatePicker } from 'naive-ui';
|
import { NDatePicker } from 'naive-ui';
|
||||||
import { fetchGetRoleUserList, fetchGetUserList } from '@/service/api/system';
|
import {
|
||||||
|
fetchGetRoleUserList,
|
||||||
|
fetchGetUserList,
|
||||||
|
fetchUpdateRoleAuthUser,
|
||||||
|
fetchUpdateRoleAuthUserCancel
|
||||||
|
} from '@/service/api/system';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||||
|
import { arraysEqualSet } from '@/utils/common';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import DictTag from '@/components/custom/dict-tag.vue';
|
import DictTag from '@/components/custom/dict-tag.vue';
|
||||||
|
|
||||||
@ -110,13 +116,16 @@ const { columns, data, getData, getDataByPage, loading, mobilePagination, search
|
|||||||
|
|
||||||
const { checkedRowKeys } = useTableOperate(data, getData);
|
const { checkedRowKeys } = useTableOperate(data, getData);
|
||||||
|
|
||||||
|
const checkedUserIds = ref<CommonType.IdType[]>([]);
|
||||||
|
|
||||||
async function handleUpdateModelWhenEdit() {
|
async function handleUpdateModelWhenEdit() {
|
||||||
checkedRowKeys.value = [];
|
checkedRowKeys.value = [];
|
||||||
getDataByPage();
|
getDataByPage();
|
||||||
const { data: roleUserList } = await fetchGetRoleUserList({
|
const { data: roleUserList } = await fetchGetRoleUserList({
|
||||||
roleId: props.rowData?.roleId
|
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() {
|
function closeDrawer() {
|
||||||
@ -124,6 +133,25 @@ function closeDrawer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
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'));
|
window.$message?.success($t('common.updateSuccess'));
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
emit('submitted');
|
emit('submitted');
|
||||||
|
Reference in New Issue
Block a user