组用户功能修订
deploy-userbase-front / build-and-deploy (push) Successful in 5m15s

This commit is contained in:
chenxing
2026-08-12 19:19:46 +08:00
parent 82f4c623af
commit 4077ef64d8
92 changed files with 16220 additions and 31 deletions
@@ -0,0 +1,257 @@
<template>
<Modal v-model="visible" :title="title" :width="720" :mask-closable="false" :loading="submitting"
@on-visible-change="onVisibleChange">
<div class="adjust-group-body">
<div class="left-panel">
<div class="panel-title">待选:</div>
<div class="tree-wrap">
<GroupTreeSearchPanel ref="treePanelRef" :treeApi="treeApi" :checkable="true"
:defaultSearchVisible="true" :showCloseButton="false" searchPlaceholder="输入组名称搜索"
@on-tree-data="onTreeData" @on-tree-check="onTreeCheck" />
</div>
</div>
<div class="right-panel">
<div class="panel-title">已选:</div>
<div class="selected-wrap">
<div v-if="selectedGroupIds.length === 0" class="empty-tip">暂无已选组</div>
<div v-else class="selected-list">
<div v-for="id in selectedGroupIds" :key="id" class="selected-item">
<div class="item-main">
<span class="item-name">{{ nodeMap[id]?.name || id }}</span>
<span class="item-path">{{ pathMap[id] || '' }}</span>
</div>
<Icon type="md-trash" class="item-delete" @click="removeGroup(id)" />
</div>
</div>
</div>
</div>
</div>
<template #footer>
<Button type="primary" :loading="submitting" @click="handleSubmit">提交</Button>
<Button @click="visible = false">取消</Button>
</template>
</Modal>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from "vue";
import { Message } from "view-ui-plus";
import GroupTreeSearchPanel from '@/components/GroupTreeSearchPanel.vue'
defineOptions({ name: "AdjustGroupContainer" });
const visible = ref(false);
const submitting = ref(false);
const userIds = ref<number[]>([]);
const userName = ref('');
const treeData = ref<any[]>([]);
const selectedGroupIds = ref<number[]>([]);
const nodeMap = ref<Record<number, any>>({});
const pathMap = ref<Record<number, string>>({});
const pendingGroupIds = ref<number[] | null>(null);
let onSuccessCallback: (() => void) | null = null;
const treePanelRef = ref<InstanceType<typeof GroupTreeSearchPanel> | null>(null);
const title = computed(() => {
if (userIds.value.length === 1) return `调整组:${userName.value || ''}`;
return `调整组:已选 ${userIds.value.length} 人`;
});
function treeApi() {
return Promise.resolve(treeData.value || []);
}
function buildNodeMap(list: any[], path = '') {
list.forEach((n: any) => {
const curPath = path ? `${path}/${n.name}` : n.name;
nodeMap.value[n.id] = n;
pathMap.value[n.id] = curPath;
if (n.children?.length) buildNodeMap(n.children, curPath);
});
}
function onTreeData(tree: any[]) {
buildNodeMap(tree || []);
if (userIds.value.length === 1 && pendingGroupIds.value) {
initSelected(pendingGroupIds.value);
pendingGroupIds.value = null;
}
}
async function loadUserGroups(userId: number) {
try {
const groupIds: number[] = await Apis.SysUser.getGroups({ pathParams: { userId }, cacheFor: null });
if (treeData.value?.length) {
initSelected(groupIds || []);
} else {
pendingGroupIds.value = groupIds || [];
}
} catch (e: any) {
Message.error(e?.message || '加载用户所在组失败');
}
}
function initSelected(groupIds: number[]) {
const existIds = groupIds.filter(id => nodeMap.value[id]);
selectedGroupIds.value = existIds;
nextTick(() => {
existIds.forEach(id => treePanelRef.value?.checkNodeById(id, true));
});
}
// 勾选/取消勾选即同步右侧:勾选则加入,取消则移除
function onTreeCheck() {
const checked = treePanelRef.value?.getCheckedNodes() || [];
selectedGroupIds.value = checked.map((n: any) => Number(n.id));
}
function removeGroup(id: number) {
selectedGroupIds.value = selectedGroupIds.value.filter(x => x !== id);
treePanelRef.value?.checkNodeById(id, false);
}
async function handleSubmit() {
if (selectedGroupIds.value.length === 0) {
Message.error('一个人至少要有一个组');
return;
}
submitting.value = true;
try {
await Apis.SysUser.changeGroups({
data: { userIds: userIds.value, groupIds: selectedGroupIds.value }
});
Message.success('调整成功');
visible.value = false;
onSuccessCallback?.();
} catch (e: any) {
Message.error(e?.message || '调整失败');
} finally {
submitting.value = false;
}
}
function onVisibleChange(show: boolean) {
if (!show) {
selectedGroupIds.value = [];
pendingGroupIds.value = null;
treeData.value = [];
nodeMap.value = {};
pathMap.value = {};
}
}
function open(ids: number[], users: any[], tree: any[], onSuccess?: () => void) {
if (!ids?.length) {
Message.error('请选择待调整用户');
return;
}
userIds.value = ids;
userName.value = users?.[0]?.realName || users?.[0]?.account || '';
treeData.value = tree || [];
selectedGroupIds.value = [];
pendingGroupIds.value = null;
nodeMap.value = {};
pathMap.value = {};
onSuccessCallback = onSuccess || null;
visible.value = true;
nextTick(() => {
treePanelRef.value?.loadTree();
if (ids.length === 1) {
loadUserGroups(ids[0]);
}
});
}
defineExpose({ open, visible });
</script>
<style lang="scss" scoped>
.adjust-group-body {
display: flex;
align-items: stretch;
height: 420px;
gap: 12px;
.left-panel,
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.panel-title {
padding: 10px 12px;
font-size: 14px;
font-weight: 500;
color: #333;
border-bottom: 1px solid #ebeef5;
background: #fafafa;
}
.tree-wrap,
.selected-wrap {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.empty-tip {
text-align: center;
color: #999;
padding-top: 60px;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fff;
&:hover {
background: #f5f7fa;
}
}
.item-main {
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
}
.item-name {
font-size: 14px;
color: #333;
}
.item-path {
font-size: 12px;
color: #999;
}
.item-delete {
font-size: 18px;
color: #ed4014;
cursor: pointer;
flex-shrink: 0;
margin-left: 8px;
}
}
</style>
+1
View File
@@ -345,6 +345,7 @@ onMounted(async () => {
<style lang="scss">
.group-user-tree {
width: 300px !important;
display: flex;
flex-direction: column;
height: 100%;
+241 -27
View File
@@ -1,5 +1,5 @@
<template>
<div class="page-wrapper tree-wrapper">
<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">
@@ -7,22 +7,59 @@
<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="onSearch" />
v-model="keyword" @on-search="onSearch" @on-clear="onClear" />
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight">
<template #default="{ checkList }">
<Button type="primary" ghost @click="openAdjust(checkList)">调整组</Button>
<Button type="warning" class="ml-15" :disabled="!checkList?.length" @click="openBatchStatus(checkList)">设置状态</Button>
<Button type="error" 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 useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
@@ -30,10 +67,13 @@ 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 } = useCurrentInstance();
const { $utils, $conf } = useCurrentInstance();
const treeApi = () => Apis.SysGroup.getTree();
@@ -44,6 +84,7 @@ 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[]) {
@@ -51,6 +92,16 @@ function onTreeData(tree: any[]) {
}
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();
}
@@ -73,9 +124,12 @@ 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 }) => {
@@ -83,8 +137,8 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
params: {
name: dataParams.value.name,
groupId: dataParams.value.groupId ?? undefined,
// 选中组织时包含子组用户;未选中时不传
withSub: dataParams.value.groupId != null ? 1 : undefined,
// 默认仅当前组;手动搜索时置 1 才包含子组
withSub: dataParams.value.withSub,
pageIndex: params.pageIndex,
pageSize: params.pageSize,
},
@@ -119,6 +173,7 @@ function describeRoles(roles: any): string {
/* ================= 表格列 ================= */
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 },
@@ -133,6 +188,10 @@ const columns = [
h('Tag', { color: row.disabled ? 'default' : 'success' },
row.disabled ? '已禁用' : '启用'),
},
{
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) => {
@@ -141,20 +200,51 @@ const columns = [
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: { marginRight: '8px', color: '#ed4014' },
style: { color: '#ed4014' },
onClick: () => handleDelete(row),
}, '删除'),
h('a', {
style: { color: row.disabled ? '#19be6b' : '#ff9900' },
onClick: () => handleToggleStatus(row),
}, row.disabled ? '启用' : '禁用'),
];
);
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() {
@@ -172,14 +262,32 @@ function handleEdit(row: any) {
});
}
/* ================= 删除 ================= */
/* ================= 调整组 ================= */
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.delete({ pathParams: { id: row.id } });
await Apis.SysUser.deleteFromGroup({ pathParams: { userId: row.id, groupId: gid } });
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
@@ -189,26 +297,132 @@ async function handleDelete(row: any) {
});
}
/* ================= 禁用 / 启用 ================= */
async function handleToggleStatus(row: any) {
const isDisabled = !!row.disabled;
/* ================= 批量删除 ================= */
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: `确定要${isDisabled ? '启用' : '禁用'}用户「${row.realName || row.account}」吗?`,
msg: `确定删除用户「${names}」这些用户吗?`,
info: '删除后不可恢复!',
async onOk() {
try {
if (isDisabled) {
await Apis.SysUser.enable({ pathParams: { id: row.id } });
} else {
await Apis.SysUser.disable({ pathParams: { id: row.id } });
const gid = selectedNode.value?.id ?? null;
if (gid == null) {
Message.warning('请先选择组');
return;
}
Message.success(isDisabled ? '已启用!' : '已禁用!');
tableRef.value?.reloadData(OperatorEnum.sel);
await Apis.SysUser.batchDelete({
data: { userIds: batchDeleteSelected.value, groupId: gid }
});
Message.success('删除成功!');
tableRef.value?.reloadData(OperatorEnum.del);
} catch (e: any) {
Message.error(e?.message || '操作失败');
Message.error(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) {
Message.error(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" scoped></style>
<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>
@@ -0,0 +1,177 @@
<template>
<Modal v-model="visible" title="批量导入用户" :mask-closable="false" :width="520" @on-visible-change="onVisibleChange">
<div class="import-wrap">
<div class="form-item">
<a @click="downloadTemplate" href="javascript:;">点击链接下载[用户导入模板.xlsx]</a>
</div>
<div class="upload-panel">
<Upload type="drag" :action="uploadUrl" :headers="uploadHeaders" :with-credentials="true"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:before-upload="beforeUpload" :on-success="onUploadSuccess" :on-error="onUploadError">
<div class="upload-box">
<Icon type="ios-cloud-upload" size="52" style="color: #6281eb"></Icon>
<h4>选择要上传的文件</h4>
<div class="info">支持扩展名:xls、xlsx;文件大小不超过 10MB</div>
</div>
</Upload>
</div>
<Alert v-if="resultMsg" :type="resultType" show-icon>
<span v-html="resultMsg"></span>
</Alert>
<Alert v-else style="margin-bottom: 0" type="warning" show-icon>
请按照模板格式填写,组 id 以第二列为准;左侧组名仅供参考。
</Alert>
</div>
<template #footer>
<Button @click="close">关闭</Button>
</template>
</Modal>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { Message } from "view-ui-plus";
import Utils from "@/utils";
import { baseConfig } from "@/assets/config/api.config";
const props = defineProps<{
modelValue: boolean;
currentGroupId?: number | null;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'success': [];
}>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const resultMsg = ref("");
const resultType = ref<"success" | "error">("success");
const uploadUrl = computed(() => {
const base = String(baseConfig.baseURL || "");
return `${base.replace(/\/$/, "")}/api/sysuser/BatchImport`;
});
const uploadHeaders = computed(() => {
const roleInfo = Utils.getCurrentRoleInfo() || {} as any;
return {
yschapirequest: "1",
schoolId: roleInfo.schId ?? "",
};
});
function close() {
visible.value = false;
}
function onVisibleChange(show: boolean) {
if (!show) {
resultMsg.value = "";
}
}
const downloadUrl = computed(() => {
const base = String(baseConfig.baseURL || "").replace(/\/$/, "");
const gid = props.currentGroupId;
return `${base}/api/sysuser/DownloadTemplate${gid ? `?groupId=${gid}` : ""}`;
});
function downloadTemplate() {
// 模板由后端生成真实 .xlsx(平铺当前组及子孙组),沿用 BschImport 的下载方式
window.location.href = downloadUrl.value;
}
function beforeUpload(file: File) {
const isExcel = /\.(xls|xlsx)$/i.test(file.name);
if (!isExcel) {
Message.error("仅支持 xls/xlsx 格式的 Excel 文件");
return false;
}
if (file.size > 10 * 1024 * 1024) {
Message.error("文件大小不能超过 10MB");
return false;
}
resultMsg.value = "";
return true;
}
function onUploadSuccess(res: any) {
// 后端可能把结果统一包装为 {code,msg,data},也可能直接返回 {total,success,fail,fails},两者都兼容。
const wrapped = typeof res?.code === "number";
const payload = wrapped && res.data && typeof res.data === "object" ? res.data : res;
const code = res?.code;
// 包装体且业务码非 0 => 失败(含参数错误 code=4、业务错误等)
if (wrapped && code !== 0) {
resultType.value = "error";
resultMsg.value = res?.msg || "导入失败";
return;
}
const total = Number(payload?.total ?? 0);
const success = Number(payload?.success ?? 0);
const fail = Number(payload?.fail ?? 0);
const fails = Array.isArray(payload?.fails) ? payload.fails : [];
if (total === 0 && success === 0 && fail === 0) {
// 极端兜底:返回结构异常
resultType.value = "error";
resultMsg.value = "导入返回结构异常,请联系管理员";
return;
}
resultType.value = "success";
let msg = `导入完成:共 ${total} 条,成功 ${success} 条,失败 ${fail} 条。`;
if (fail > 0 && fails.length) {
const lines = fails.slice(0, 5).map((f: any) => `第 ${f.row} 行:${f.reason}`);
if (fails.length > 5) lines.push(`…等 ${fails.length} 条`);
msg += `<br/>失败明细:<br/>${lines.join("<br/>")}`;
}
resultMsg.value = msg;
if (fail === 0 && total > 0) {
emit('success');
}
}
function onUploadError(err: any) {
resultType.value = "error";
// 包装体业务错误可能经由 on-error 返回(如 HTTP 非 200),优先取 msg
const msg = err?.msg || (typeof err === "string" ? err : err?.message) || "上传失败,请检查网络或后端服务";
resultMsg.value = msg;
}
</script>
<style lang="scss" scoped>
.import-wrap {
padding: 8px 0;
.form-item {
margin-bottom: 16px;
}
}
.upload-panel {
.upload-box {
background: #f7f8fc;
padding: 28px 0;
text-align: center;
h4 {
color: #505c7e;
margin: 8px 0 4px;
font-weight: normal;
}
}
.info {
font-size: 13px;
color: #999;
}
}
</style>
+88
View File
@@ -0,0 +1,88 @@
<template>
<Modal :model-value="props.visible" title="用户排序" :width="420" :mask-closable="false"
@on-visible-change="onVisibleChange">
<Alert show-icon>排序号为正整数,1 为最靠前,越大越靠后</Alert>
<Form :model="form" :label-width="100">
<FormItem label="当前排序号:">
<span :style="{ color: props.currentSortNo == null ? '#999' : '#333' }">
{{ props.currentSortNo == null ? '暂无' : props.currentSortNo }}
</span>
</FormItem>
<FormItem label="当前用户:">
<span>{{ props.userAccount || '-' }}</span>
</FormItem>
<FormItem label="修改排序号:">
<InputNumber :max="999" :min="1" v-model="form.sortNo" :style="{ width: '140px' }" />
</FormItem>
</Form>
<template #footer>
<Button type="primary" :loading="submitting" @click="handleSubmit">提交</Button>
<Button @click="onVisibleChange(false)">取消</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from "vue";
import { Message } from "view-ui-plus";
import Apis from "@/api";
const props = defineProps<{
visible: boolean;
userId: number | null;
userAccount: string;
currentSortNo: number | null;
groupId: number | null;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean];
saved: [];
}>();
// 弹窗显示状态变化(点 X / ESC / 取消 都会触发 on-visible-change)一律回写父组件
function onVisibleChange(v: boolean) {
emit('update:visible', v);
}
const form = reactive<{
sortNo: number;
}>({
sortNo: 1,
});
const submitting = ref(false);
// 每次打开弹窗:把输入框重置为「自身排序号,没有则默认 1」,不沿用上次输入
watch(() => props.visible, (open) => {
if (open) {
form.sortNo = props.currentSortNo ?? 1;
}
});
async function handleSubmit() {
if (form.sortNo == null) {
Message.warning('请输入排序号(1~999)');
return;
}
if (props.userId == null || props.groupId == null) {
Message.warning('请先选择组与用户');
return;
}
submitting.value = true;
try {
await Apis.SysUser.changeSortNo({
pathParams: { userId: props.userId },
data: { groupId: props.groupId, sortNo: form.sortNo },
});
Message.success('排序设置成功');
emit('update:visible', false);
emit('saved');
} catch (e: any) {
Message.error(e?.message || '排序设置失败');
} finally {
submitting.value = false;
}
}
</script>