批量更新租户规则

This commit is contained in:
扫地僧 2026-03-09 22:55:27 +08:00
parent 108ec50978
commit a5a02eb8c9
19 changed files with 399 additions and 66 deletions

1
components.d.ts vendored
View File

@ -21,6 +21,7 @@ declare module 'vue' {
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']

View File

@ -6,7 +6,7 @@ import { onMounted } from 'vue';
const authStore = useAuthStore();
// 获取租户ID
const tenantId = (authStore.user as any)?.tenant_id;
const tenantId = (authStore.user as any)?.tid;
// 获取用户信息
const userInfo = authStore.user;

View File

@ -101,7 +101,7 @@ export function getEmployeeList(tenantId) {
return request({
url: '/admin/erp/getEmployee',
method: 'get',
params: { tenant_id: tenantId }
params: { tid: tenantId }
});
}

View File

@ -144,6 +144,18 @@ export function deleteFile(id) {
});
}
/**
* 删除文件
* @param {number|string} id 文件ID
* @returns {Promise}
*/
export function deleteFilePermanently(id) {
return request({
url: `/admin/deleteFilePermanently/${id}`,
method: "delete",
});
}
/**
* 移动文件
* @param {number|string} id 文件ID
@ -159,37 +171,41 @@ export function moveFile(id, cate) {
}
/**
* 上传头像
* @param {FormData} formData 文件数据
* @param {Object} options 额外选项
* @param {string} [options.cate]
* 批量删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function uploadAvatar(formData, options = {}) {
if (options.cate) {
formData.append('cate', options.cate);
}
export function batchDeleteFiles(ids) {
return request({
url: "/admin/uploadavatar",
url: "/admin/batchDeleteFiles",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
data: { ids },
});
}
/**
* 更新头像
* @param {number|string} id 文件ID
* @param {Object} fileData 更新头像
* 批量彻底删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function updateAvatar(id, fileData) {
export function batchDeleteFilesPermanently(ids) {
return request({
url: `/admin/uploadavatar/${id}`,
url: "/admin/batchDeleteFilesPermanently",
method: "post",
data: fileData,
data: { ids },
});
}
/**
* 批量移动文件
* @param {Array} ids 文件ID数组
* @param {number} cate 目标分类ID
* @returns {Promise}
*/
export function batchMoveFiles(ids, cate) {
return request({
url: "/admin/batchMoveFiles",
method: "post",
data: { ids, cate },
});
}

View File

@ -40,7 +40,7 @@ export const useAuthStore = defineStore('auth', () => {
account: userInfo.account || '',
name: userInfo.name || '',
group_id: userInfo.group_id || '',
tenant_id: userInfo.tenant_id || '',
tid: userInfo.tid || '',
avatar: userInfo.avatar || ''
}

View File

@ -1,5 +1,19 @@
<template>
<div class="category-manager">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h3>文章分类</h3>
<p> {{ totalCount }} 个分类</p>
</div>
<div class="page-actions">
<el-button type="primary" :icon="Plus" @click="handleAdd">
新增分类
</el-button>
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
<div class="category-list" v-loading="loading">
<div v-if="filteredCategories.length === 0" class="empty-state">
<el-empty description="暂无文章分类" />
@ -55,6 +69,13 @@ const currentEdit = ref(null);
const searchText = ref("");
const categories = ref([]);
//
const addCategory = ref(null);
const handleAdd = () => {
addCategory.value = { parentId: 0 };
dialogVisible.value = true;
}
//
const totalCount = computed(() => categories.value.length);
@ -178,6 +199,8 @@ onMounted(() => {
<style lang="less" scoped>
.category-manager {
min-height: 100vh;
padding: 20px;
background-color: var(--el-bg-color-page);
//
.page-header {

View File

@ -8,7 +8,7 @@
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索文章标题/作者"
placeholder="搜索文章标题"
clearable
@clear="handleSearch"
>

View File

@ -0,0 +1,3 @@
<template></template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,3 @@
<template></template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -79,7 +79,7 @@ const total = ref(0);
const editRef = ref<any>(null);
const authStore = useAuthStore();
const tenantId = (authStore.user as any)?.tenant_id;
const tenantId = (authStore.user as any)?.tid;
// console.log(tenantId);

View File

@ -90,7 +90,7 @@ const loading = ref(false)
const formRef = ref<FormInstance>()
const isEdit = ref(false)
const authStore = useAuthStore()
const tenantId = (authStore.user as any)?.tenant_id
const tenantId = (authStore.user as any)?.tid
const leaderList = ref([])
const formData = reactive({
id: 0,
@ -192,7 +192,7 @@ const handleSubmit = async () => {
}
})
if (tenantId) {
submitData.tenant_id = tenantId
submitData.tid = tenantId
}
res = await editOrganization(formData.id, submitData)
} else {
@ -205,7 +205,7 @@ const handleSubmit = async () => {
}
})
if (tenantId) {
submitData.append('tenant_id', String(tenantId))
submitData.append('tid', String(tenantId))
}
res = await createOrganization(submitData)
}

View File

@ -117,7 +117,7 @@ import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '
//
interface TreeNode {
id: number
tenant_id?: number
tid?: number
org_name: string
org_code?: string
parent_id?: number
@ -153,7 +153,7 @@ const buildTree = (list: any[]): TreeNode[] => {
list.forEach(item => {
const node: TreeNode = {
id: item.id,
tenant_id: item.tenant_id,
tid: item.tid,
org_name: item.org_name,
org_code: item.org_code,
parent_id: item.parent_id,
@ -193,7 +193,7 @@ const loadOrganizationDetail = async (id: number) => {
if (data) {
selectedNode.value = {
id: data.id,
tenant_id: data.tenant_id,
tid: data.tid,
org_name: data.org_name,
org_code: data.org_code,
parent_id: data.parent_id,

View File

@ -56,7 +56,7 @@
详情
</el-button>
<el-button
v-if="row.default !== 1 && row.default !== 2"
v-if="userInfo?.id === 1"
size="small"
type="primary"
link
@ -66,7 +66,7 @@
编辑
</el-button>
<el-button
v-if="row.id !== 1"
v-if="userInfo?.id === 1"
size="small"
type="danger"
link

View File

@ -88,7 +88,7 @@ const submitForm = async () => {
try {
const submitData = {
...formData,
tenant_id: currentTenantId.value
tid: currentTenantId.value
};
const res = await addUser(submitData);

View File

@ -72,7 +72,7 @@
const formRef = ref();
const formData = reactive({
tenant_id: null,
tid: null,
type: '',
file_url: '',
expire_time: '',
@ -89,7 +89,7 @@
const open = (row: any) => {
visible.value = true;
tenantName.value = row.tenant_name;
formData.tenant_id = row.id;
formData.tid = row.id;
//
// fetchDetail(row.id);

View File

@ -113,7 +113,7 @@ const fetchDomains = async () => {
loading.value = true
try {
//
const res = await getMyDomains({ tid: authStore.user.tenant_id })
const res = await getMyDomains({ tid: authStore.user.tid })
if (res.code === 200) {
tableData.value = res.data || []
}
@ -133,7 +133,7 @@ const handleSubmit = async () => {
submitLoading.value = true
try {
const res = await applyTenantDomain({
tenant_id: authStore.user.tenant_id,
tid: authStore.user.tid,
main_domain: form.main_domain,
sub_domain: form.sub_domain
})

View File

@ -55,9 +55,9 @@
<el-tag size="small">{{ row.dict_code }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="tenant_id" label="租户ID" width="100" align="center">
<el-table-column prop="tid" label="租户ID" width="100" align="center">
<template #default="{ row }">
<span>{{ row.tenant_id || 0 }}</span>
<span>{{ row.tid || 0 }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">

View File

@ -57,7 +57,7 @@
</div>
<div class="group-count">{{ group.total }} 个文件</div>
</div>
<div class="group-actions" v-if="group.id !== 0 && group.id !== 999">
<div class="group-actions" v-if="group.id !== 0">
<div class="action-buttons" @click.stop>
<el-button
type="primary"
@ -128,11 +128,46 @@
>
上传文件
</el-button>
<el-button
type="warning"
:icon="FolderOpened"
@click="handleBatchMove"
:disabled="selectedFileIds.length === 0"
>
批量移动 ({{ selectedFileIds.length }})
</el-button>
<el-button
type="danger"
:icon="Delete"
@click="handleBatchDelete"
:disabled="selectedFileIds.length === 0"
>
批量删除 ({{ selectedFileIds.length }})
</el-button>
<el-button
type="danger"
plain
@click="handleBatchDeletePermanently"
:disabled="selectedFileIds.length === 0"
>
<el-icon><DeleteFilled /></el-icon>
批量永久删除 ({{ selectedFileIds.length }})
</el-button>
<el-button
type="info"
@click="clearSelection"
:disabled="selectedFileIds.length === 0"
>
取消选择
</el-button>
</div>
</div>
<!-- 文件卡片网格 -->
<div class="file-grid-container">
<div class="file-grid-header" v-if="files.length > 0">
<button class="allbtns" @click="toggleSelectAll">全选/反选</button>
</div>
<div
v-loading="loading"
class="file-grid"
@ -142,14 +177,14 @@
v-for="file in files"
:key="file.id"
shadow="hover"
class="file-card"
@click="handleFileClick(file)"
:class="['file-card', { 'file-card-selected': selectedFileIds.includes(file.id) }]"
@click="toggleFileSelection(file)"
>
<div class="file-checkbox">
<el-checkbox :model-value="selectedFileIds.includes(file.id)" />
</div>
<div class="file-icon">
<div
v-if="isImage(file)"
@click.stop="handleImagePreview(file)"
>
<div v-if="isImage(file)">
<img
:src="getFileUrl(file.url)"
alt="file"
@ -179,27 +214,46 @@
}}</span>
</div>
<div class="file-actions">
<el-button
v-if="isImage(file)"
type="primary"
link
@click.stop="handleImagePreview(file)"
title="预览"
>
<i class="fa-solid fa-eye"></i>
</el-button>
<el-button
type="primary"
link
@click.stop="handleMoveClick(file)"
title="移动"
>
移动
<i class="fa-solid fa-arrows-up-down-left-right"></i>
</el-button>
<el-button
type="primary"
link
@click.stop="handleDownload(file)"
title="下载"
>
下载
<i class="fa-solid fa-download"></i>
</el-button>
<el-button
type="danger"
link
@click.stop="handleDelete(file)"
title="删除"
>
删除
<i class="fa-regular fa-trash-can"></i>
</el-button>
<el-button
type="danger"
link
@click.stop="handlePermanentDelete(file)"
title="彻底删除"
>
<i class="fa-solid fa-trash"></i>
</el-button>
</div>
</el-card>
@ -211,7 +265,7 @@
</div>
<!-- 分页组件 -->
<div class="pagination-container" v-if="total > 0">
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@ -371,6 +425,33 @@
@success="handleCreateCategorySuccess"
@close="handleCreateCategoryClose"
/>
<!-- 批量移动文件对话框 -->
<el-dialog
v-model="showBatchMoveDialog"
title="批量移动文件"
width="400px"
>
<el-form label-width="80px">
<el-form-item label="目标分组">
<el-select v-model="batchMoveTargetCate" placeholder="请选择目标分组">
<el-option :value="0" label="未分类" />
<el-option
v-for="group in groups"
:key="group.id"
:value="group.id"
:label="group.name"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchMoveDialog = false">取消</el-button>
<el-button type="primary" @click="confirmBatchMove" :loading="batchMoveLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
@ -391,6 +472,7 @@ import {
Files,
Download,
Delete,
DeleteFilled,
Edit,
ZoomIn,
ZoomOut,
@ -404,7 +486,11 @@ import {
createFileCate,
renameFileCate,
deleteFileCate,
deleteFilePermanently,
deleteFile,
batchDeleteFiles,
batchDeleteFilesPermanently,
batchMoveFiles,
} from "@/api/file";
import UploadFile from "./components/uploadFile.vue";
import MoveFile from "./components/moveFile.vue";
@ -440,7 +526,7 @@ const loading = ref(false);
//
const currentPage = ref(1);
const pageSize = ref(24);
const pageSize = ref(20);
const total = ref(0);
//
@ -450,6 +536,14 @@ const showUploadDialog = ref(false);
const showMoveDialog = ref(false);
const selectedFile = ref<FileItem | null>(null);
//
const selectedFileIds = ref<number[]>([]);
//
const showBatchMoveDialog = ref(false);
const batchMoveTargetCate = ref(0);
const batchMoveLoading = ref(false);
//
const showImagePreview = ref(false);
const previewImageUrl = ref("");
@ -500,7 +594,7 @@ const getFileUrl = (url: string) => {
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
};
//
//
const uncategorizedGroup = ref<Group>({
id: 0,
name: "未分类",
@ -508,17 +602,9 @@ const uncategorizedGroup = ref<Group>({
description: "未分类的文件",
});
//
const avatarGroup = ref<Group>({
id: 999,
name: "头像",
total: 0,
description: "用户头像",
});
//
const filteredGroups = computed(() => {
let result = [uncategorizedGroup.value, avatarGroup.value, ...groups.value];
let result = [uncategorizedGroup.value, ...groups.value];
if (groupSearchQuery.value) {
const query = groupSearchQuery.value.toLowerCase();
@ -651,9 +737,6 @@ const loadFiles = async () => {
if (selectedGroup.value.id === 0) {
//
uncategorizedGroup.value.total = res.data.total || 0;
} else if (selectedGroup.value.id === 999) {
//
avatarGroup.value.total = res.data.total || 0;
} else {
//
const group = groups.value.find(
@ -965,6 +1048,30 @@ function handleDelete(row) {
});
}
//
function handlePermanentDelete(row) {
ElMessageBox.confirm("确定要彻底删除文件吗?此操作不可恢复,服务器上的文件也会被删除!", "警告", {
confirmButtonText: "确定删除",
cancelButtonText: "取消",
type: "error",
}).then(() => {
deleteFilePermanently(row.id).then((res) => {
const resp =
res && typeof res.code !== "undefined"
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success("彻底删除成功");
loadFiles();
} else {
ElMessage.error((resp && resp.message) || "彻底删除失败");
}
});
});
}
const handleUpload = () => {
if (!selectedGroup.value) {
ElMessage.warning("请先选择一个分组");
@ -979,6 +1086,158 @@ const handleUploadSuccess = () => {
loadFiles();
};
//
const toggleFileSelection = (file: FileItem) => {
const index = selectedFileIds.value.indexOf(file.id);
if (index > -1) {
selectedFileIds.value.splice(index, 1);
} else {
selectedFileIds.value.push(file.id);
}
};
// /
const toggleSelectAll = () => {
const allFileIds = files.value.map(file => file.id);
const isAllSelected = allFileIds.length === selectedFileIds.value.length && allFileIds.length > 0;
if (isAllSelected) {
selectedFileIds.value = [];
} else {
selectedFileIds.value = allFileIds;
}
};
//
const clearSelection = () => {
selectedFileIds.value = [];
};
//
const handleBatchMove = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要移动的文件');
return;
}
batchMoveTargetCate.value = 0;
showBatchMoveDialog.value = true;
};
//
const confirmBatchMove = async () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要移动的文件');
return;
}
if (batchMoveTargetCate.value === null || batchMoveTargetCate.value === undefined) {
ElMessage.warning('请选择目标分组');
return;
}
batchMoveLoading.value = true;
try {
const res = await batchMoveFiles(selectedFileIds.value, batchMoveTargetCate.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量移动成功');
showBatchMoveDialog.value = false;
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量移动失败');
}
} catch (error) {
console.error('批量移动失败:', error);
ElMessage.error('批量移动失败');
} finally {
batchMoveLoading.value = false;
}
};
//
const handleBatchDelete = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要删除的文件');
return;
}
ElMessageBox.confirm(
`确定要删除选中的 ${selectedFileIds.value.length} 个文件吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await batchDeleteFiles(selectedFileIds.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量删除成功');
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
ElMessage.error('批量删除失败');
}
});
};
//
const handleBatchDeletePermanently = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要永久删除的文件');
return;
}
ElMessageBox.confirm(
`确定要永久删除选中的 ${selectedFileIds.value.length} 个文件吗?此操作不可恢复,服务器上的文件也会被删除!`,
'危险操作',
{
confirmButtonText: '确定永久删除',
cancelButtonText: '取消',
type: 'error',
}
).then(async () => {
try {
const res = await batchDeleteFilesPermanently(selectedFileIds.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量永久删除成功');
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量永久删除失败');
}
} catch (error) {
console.error('批量永久删除失败:', error);
ElMessage.error('批量永久删除失败');
}
});
};
//
onMounted(() => {
getUserCateData();
@ -1111,9 +1370,23 @@ onMounted(() => {
min-height: 400px;
}
.file-grid-header {
margin-bottom: 12px;
display: flex;
justify-content: flex-start;
.allbtns{
color:#0081ff;
&:hover{
color:#0088ff;
}
}
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: auto;
align-items: start;
gap: 16px;
@ -1126,12 +1399,25 @@ onMounted(() => {
// height: 100%;
display: flex;
flex-direction: column;
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.file-card-selected {
border: 2px solid var(--el-color-primary);
box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.2);
}
.file-checkbox {
position: absolute;
top: 8px;
left: 8px;
z-index: 10;
}
:deep(.el-card__body) {
padding: 16px;
height: 100%;

View File

@ -24,6 +24,7 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="mid" label="MID" width="80" align="center" />
<el-table-column
prop="name"
label="模块名称"