This repository has been archived on 2026-09-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
UserBaseFront/src/views/group-user/index.vue
T
chenxing 73066b5ab3
deploy-userbase-front / build-and-deploy (push) Successful in 2m14s
转出状态
2026-08-26 12:37:56 +08:00

443 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-wrapper tree-wrapper group-user-page">
<Group class="tree-container" v-model:curTreeNode="selectedNode" :treeApi="treeApi" @on-change="onTreeChange"
@on-tree-data="onTreeData" />
<div class="main-container">
<div class="search-wrap">
<Button type="primary" @click="showAddUser">
<Icon type="md-add" style="margin-right: 4px" />新增用户
</Button>
<Button type="success" class="ml-15" @click="showImportUser">批量导入</Button>
<Button class="ml-15" @click="onExport">导出组用户</Button>
<Input placeholder="输入用户名 / 真实姓名搜索" class="fr" search clearable suffix="ios-search" style="width: 220px"
v-model="keyword" @on-search="onSearch" @on-clear="onClear" />
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight">
<template #default="{ checkList }">
<Button type="primary" ghost :disabled="!checkList?.length" @click="openAdjust(checkList)">调整组</Button>
<Button type="warning" ghost class="ml-15" :disabled="!checkList?.length" @click="openBatchStatus(checkList)">设置状态</Button>
<Button type="error" ghost class="ml-15" :disabled="!checkList?.length" @click="openBatchDelete(checkList)">批量删除</Button>
</template>
</TablePageAlovaNew>
<!-- 容器组件:新增 / 编辑 / 调整组 / 批量导入 -->
<AddUserContainer ref="addRef" />
<EditUserContainer ref="editRef" />
<AdjustGroupContainer ref="adjustRef" />
<BatchImportUser v-model="importModal" :current-group-id="selectedNode?.id" @success="onImportSuccess" />
<UserSort v-model:visible="sortModal" :user-id="sortRow?.id ?? null"
:user-account="sortRow?.account || sortRow?.realName || ''"
:current-sort-no="sortRow?.sortNo ?? null" :group-id="selectedNode?.id ?? null" @saved="onSortSaved" />
<!-- 批量设置用户状态 -->
<Modal v-model="statusModal" title="设置用户的状态" :width="520" :mask-closable="false">
<div class="batch-status-body">
<div class="user-list-row">
<span class="user-list-label">需设置用户:</span>
<ul class="user-list">
<li v-for="item in statusSelectedRows" :key="item.id">{{ item.realName || item.account }}</li>
</ul>
</div>
<div class="status-radio-row">
<span class="status-label">选择设置状态:</span>
<RadioGroup v-model="statusValue" type="button">
<Radio :label="0">正常</Radio>
<Radio :label="1">禁用</Radio>
</RadioGroup>
</div>
</div>
<template #footer>
<Button type="primary" :loading="statusSubmitting" @click="confirmBatchStatus">确定</Button>
<Button @click="statusModal = false">取消</Button>
</template>
</Modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { Message } from "view-ui-plus";
import { useDefaultStore } from "@/store";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import { OperatorEnum } from '@/types';
import Group from './group/index.vue'
import AddUserContainer from './user/AddUserContainer.vue';
import EditUserContainer from './user/EditUserContainer.vue';
import AdjustGroupContainer from './AdjustGroupContainer.vue';
import BatchImportUser from './user/BatchImportUser.vue';
import UserSort from './user/UserSort.vue';
defineOptions({ name: "GroupUser" });
const { $utils, $conf } = useCurrentInstance();
const treeApi = () => Apis.SysGroup.getTree();
/* ================= 组织树 ================= */
const selectedNode = ref<any>({})
const treeData = ref<any[]>([])
function onTreeChange(node: any) {
selectedNode.value = node;
dataParams.value.groupId = node?.id ?? null;
dataParams.value.withSub = 0;
tableRef.value?.refreshData();
}
function onTreeData(tree: any[]) {
treeData.value = tree;
}
function onSearch() {
dataParams.value.name = keyword.value.trim() || undefined;
// 手动搜索:包含子组用户
dataParams.value.withSub = 1;
tableRef.value?.refreshData();
}
function onClear() {
keyword.value = '';
dataParams.value.name = undefined;
// 取消搜索:回到仅当前组浏览
dataParams.value.withSub = 0;
tableRef.value?.refreshData();
}
/* ================= 所在组下拉(拍平组织树) ================= */
const groupOptions = computed(() => {
const flat: { id: number; name: string }[] = [];
const walk = (list: any[], prefix = '') => {
list.forEach(n => {
flat.push({ id: n.id, name: prefix ? `${prefix} / ${n.name}` : n.name });
if (n.children?.length) walk(n.children, prefix ? `${prefix} / ${n.name}` : n.name);
});
};
walk(treeData.value);
return flat;
})
/* ================= 表格数据源 ================= */
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const keyword = ref('')
const dataParams = ref<{
name: string | undefined;
groupId: number | null;
withSub: number;
}>({
name: undefined,
groupId: null,
// 默认仅当前组;仅手动搜索时才包含子组
withSub: 0,
});
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
return Apis.SysUser.list({
params: {
name: dataParams.value.name,
groupId: dataParams.value.groupId ?? undefined,
// 默认仅当前组;手动搜索时置 1 才包含子组
withSub: dataParams.value.withSub,
pageIndex: params.pageIndex,
pageSize: params.pageSize,
},
cacheFor: null,
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }))
/* ================= 用户身份(roles[])展示 ================= */
// 唯一真源:后端 SysRoleEnum(经 GetSysDict.sysrole 下发),杜绝前端硬编码漂移。
// roles 中仅存放 SysRoleEnum 的 key(1=老师 … 7=管理员),不再混用 UserTypeEnum。
const SYS_ROLE_MAP_FALLBACK: Record<number, string> = {
1: '老师', 2: '校领导', 3: '学生', 4: '家长',
5: '局领导', 6: '工作号', 7: '管理员',
};
const store = useDefaultStore();
const userTypeMap = computed<Record<number, string>>(() => {
const sysrole = (store.getSysDict as any)?.sysrole;
if (Array.isArray(sysrole) && sysrole.length) {
const m: Record<number, string> = {};
sysrole.forEach((it: any) => { m[Number(it.k)] = it.v; });
return m;
}
return SYS_ROLE_MAP_FALLBACK;
});
function describeRoles(roles: any): string {
if (!Array.isArray(roles) || roles.length === 0) return '-';
const names = roles
.map((r: any) => userTypeMap.value[Number(r)])
.filter((n: string) => !!n);
return names.length ? names.join('/') : '-';
}
/* ================= 表格列 ================= */
const columns = [
{ type: 'selection', width: 55, align: 'center' },
{ type: 'index', title: '序号', width: 70, align: 'center' },
{ title: '用户名', key: 'account', minWidth: 120 },
{ title: '真实姓名', key: 'realName', minWidth: 120 },
{ title: '手机号', key: 'phone', minWidth: 140 },
{
title: '用户身份', key: 'roles', minWidth: 180,
render: (h: any, { row }: any) => h('span', describeRoles(row.roles)),
},
{
title: '状态', key: 'disabled', width: 130, align: 'center',
render: (h: any, { row }: any) => {
if (row.disabled) return h('Tag', { color: 'default' }, '禁用');
if (row.groupStatus === 1) {
// 正常 Tag + 转出徽标(view-ui-plus Badge status 模式:圆点 + 文本)
return h('div', { style: 'display:inline-flex;align-items:center;gap:6px' }, [
h('Tag', { color: 'success' }, '正常'),
h('Badge', { props: { status: 'warning', text: '转出' } }),
]);
}
return h('Tag', { color: 'success' }, '正常');
},
},
{
title: '排序号', key: 'sortNo', width: 90, align: 'center',
render: (h: any, { row }: any) => h('span', row.sortNo == null ? '-' : row.sortNo),
},
{
title: '操作', key: 'action', width: 220, align: 'center', fixed: 'right' as const,
render: (h: any, { row }: any) => {
const opList: any[] = [
h('a', {
style: { marginRight: '8px', color: '#577afb' },
onClick: () => handleEdit(row),
}, '编辑'),
];
// 仅「单组浏览」(withSub=0)时开放排序:此时每个用户都直接属于当前组,排序号语义明确;
// 含子组搜索(withSub=1)下用户可能属于子组,排序号归属不清,故隐藏。
if (dataParams.value.withSub === 0) {
opList.push(
h('a', {
style: { marginRight: '8px', color: '#2d8cf0' },
onClick: () => openSort(row),
}, '排序'),
);
}
opList.push(
h('a', {
style: { color: '#ed4014' },
onClick: () => handleDelete(row),
}, '删除'),
);
return h('div', opList);
},
},
];
/* ================= 导出组用户 ================= */
function onExport() {
const gid = selectedNode.value?.id ?? null;
if (gid == null) {
Message.warning('请先选择要导出的组');
return;
}
const params = new URLSearchParams();
params.set('groupId', String(gid));
params.set('withSub', '1');
if (dataParams.value.name) params.set('name', dataParams.value.name);
window.open(`${$conf.serviceHost}/api/sysuser/Export?${params.toString()}`);
}
/* ================= 批量导入 ================= */
const importModal = ref(false);
function showImportUser() {
importModal.value = true;
}
function onImportSuccess() {
tableRef.value?.reloadData(OperatorEnum.sel);
}
/* ================= 新增 ================= */
const addRef = ref();
function showAddUser() {
const curGroupId = selectedNode.value?.id ?? null;
addRef.value?.open(groupOptions.value, curGroupId, () => {
tableRef.value?.reloadData(OperatorEnum.add);
});
}
/* ================= 编辑 ================= */
const editRef = ref();
function handleEdit(row: any) {
// 当前点击所在的组:优先用选中节点 id,兜底用当前正在浏览的组(dataParams.groupId,
// 由 onTreeChange 设置,比 selectedNode 更可靠),供"所在组"只读展示与兜底
const curGroupId = selectedNode.value?.id ?? dataParams.value.groupId ?? null;
editRef.value?.open(row, groupOptions.value, curGroupId, () => {
tableRef.value?.reloadData(OperatorEnum.edit);
});
}
/* ================= 调整组 ================= */
const adjustRef = ref();
function openAdjust(checkList: number[]) {
if (!checkList?.length) {
Message.error('请选择待调整用户!');
return;
}
const rows = tableRef.value?.list.filter((r: any) => checkList.includes(r.id)) || [];
adjustRef.value?.open(checkList, rows, treeData.value, () => {
tableRef.value?.reloadData(OperatorEnum.sel);
});
}
/* ================= 删除(从当前组移除) ================= */
async function handleDelete(row: any) {
const gid = selectedNode.value?.id ?? null;
if (gid == null) {
Message.warning('请先选择组');
return;
}
await MyConfirm({
msg: `确定删除用户「${row.realName || row.account}」吗?`,
info: '删除后不可恢复!',
async onOk() {
try {
await Apis.SysUser.deleteFromGroup({ pathParams: { userId: row.id, groupId: gid } });
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
console.warn(e?.message || '删除失败');
}
},
});
}
/* ================= 批量删除 ================= */
const batchDeleteSelected = ref<number[]>([]);
async function openBatchDelete(checkList: number[]) {
if (!checkList?.length) {
Message.error('请选择待删除用户!');
return;
}
batchDeleteSelected.value = checkList;
const rows = tableRef.value?.list.filter((r: any) => checkList.includes(r.id)) || [];
const names = rows.map((r: any) => r.realName || r.account).join(',');
await MyConfirm({
msg: `确定删除用户「${names}」这些用户吗?`,
info: '删除后不可恢复!',
async onOk() {
try {
const gid = selectedNode.value?.id ?? null;
if (gid == null) {
Message.warning('请先选择组');
return;
}
await Apis.SysUser.batchDelete({
data: { userIds: batchDeleteSelected.value, groupId: gid }
});
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
console.warn(e?.message || '删除失败');
}
},
});
}
/* ================= 批量设置状态 ================= */
const statusModal = ref(false);
const statusSubmitting = ref(false);
const statusValue = ref<number>(0);
const statusSelectedRows = ref<any[]>([]);
function openBatchStatus(checkList: number[]) {
if (!checkList?.length) {
Message.error('请选择待设置用户!');
return;
}
const rows = tableRef.value?.list.filter((r: any) => checkList.includes(r.id)) || [];
statusSelectedRows.value = rows;
// 默认以第一个选中用户当前状态作为默认选中项
statusValue.value = rows[0]?.disabled ? 1 : 0;
statusModal.value = true;
}
async function confirmBatchStatus() {
if (!statusSelectedRows.value.length) return;
statusSubmitting.value = true;
try {
const userIds = statusSelectedRows.value.map(r => r.id);
await Apis.SysUser.batchChangeStatus({
data: { userIds, disabled: statusValue.value === 1 }
});
Message.success('设置成功!');
statusModal.value = false;
tableRef.value?.reloadData(OperatorEnum.sel);
} catch (e: any) {
console.warn(e?.message || '设置失败');
} finally {
statusSubmitting.value = false;
}
}
/* ================= 组内排序 ================= */
const sortModal = ref(false);
const sortRow = ref<any>(null);
function openSort(row: any) {
sortRow.value = row;
sortModal.value = true;
}
function onSortSaved() {
tableRef.value?.reloadData(OperatorEnum.sel);
}
</script>
<style lang="scss">
.group-user-page .page-panel {
left: 348px !important;
}
.batch-status-body {
padding: 8px 12px;
.user-list-row {
display: flex;
align-items: flex-start;
margin-bottom: 24px;
}
.user-list-label {
flex-shrink: 0;
color: #666;
line-height: 22px;
padding-top: 2px;
}
.user-list {
flex: 1;
margin: 0;
padding-left: 18px;
color: #333;
line-height: 22px;
li {
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
.status-radio-row {
display: flex;
align-items: center;
}
.status-label {
flex-shrink: 0;
color: #666;
margin-right: 12px;
}
}
</style>