✨ 完善组织人员增量同步与用户管理交互

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-10 13:56:29 +08:00
parent eaece0209d
commit ea797356ce
9 changed files with 322 additions and 178 deletions
+12 -2
View File
@@ -57,26 +57,36 @@ export const clearNonTopDept = signal => {
}); });
}; };
export const syncOaCompany = (current = 1, size = 20, signal) => { export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
return request({ return request({
url: '/blade-system/dept/sync-oa-company', url: '/blade-system/dept/sync-oa-company',
method: 'post', method: 'post',
params: { params: {
current, current,
size, size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
}, },
timeout: 60000, timeout: 60000,
signal, signal,
}); });
}; };
export const syncOaDepartment = (current = 1, size = 20, signal) => { export const syncOaDepartment = (
current = 1,
size = 20,
signal,
incremental = false,
modifiedSince
) => {
return request({ return request({
url: '/blade-system/dept/sync-oa-department', url: '/blade-system/dept/sync-oa-department',
method: 'post', method: 'post',
params: { params: {
current, current,
size, size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
}, },
timeout: 60000, timeout: 60000,
signal, signal,
+14 -1
View File
@@ -31,13 +31,15 @@ export const add = row => {
}); });
}; };
export const syncIamAccounts = (current = 1, size = 50, signal) => { export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
return request({ return request({
url: '/blade-system/user/sync-iam-accounts', url: '/blade-system/user/sync-iam-accounts',
method: 'post', method: 'post',
params: { params: {
current, current,
size, size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
}, },
timeout: 60000, timeout: 60000,
signal, signal,
@@ -52,6 +54,17 @@ export const update = row => {
}); });
}; };
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/user/change-status',
method: 'post',
params: {
id,
status,
},
});
};
export const updatePlatform = (userId, userType, userExt) => { export const updatePlatform = (userId, userType, userExt) => {
return request({ return request({
url: '/blade-system/user/update-platform', url: '/blade-system/user/update-platform',
+5 -2
View File
@@ -27,7 +27,8 @@ export const userOption = safe => {
index: true, index: true,
selection: true, selection: true,
addBtn: false, addBtn: false,
viewBtn: true, viewBtn: false,
menuWidth: 160,
dialogType: 'dialog', dialogType: 'dialog',
dialogWidth: '90%', dialogWidth: '90%',
labelPosition: 'right', labelPosition: 'right',
@@ -37,6 +38,7 @@ export const userOption = safe => {
{ {
label: '账号名', label: '账号名',
prop: 'account', prop: 'account',
slot: true,
search: true, search: true,
searchOrder: 2, searchOrder: 2,
searchPlaceholder: '支持模糊搜索', searchPlaceholder: '支持模糊搜索',
@@ -63,7 +65,8 @@ export const userOption = safe => {
label: '所属部门', label: '所属部门',
prop: 'deptName', prop: 'deptName',
slot: true, slot: true,
overHidden: true, minWidth: 200,
overHidden: false,
display: false, display: false,
}, },
{ label: '岗位', prop: 'postName', overHidden: true, display: false }, { label: '岗位', prop: 'postName', overHidden: true, display: false },
@@ -16,7 +16,7 @@
class="contract-manage-form__attachment-table" class="contract-manage-form__attachment-table"
@selection-change="selected = $event" @selection-change="selected = $event"
> >
<el-table-column v-if="!readonly" type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center"> <el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
@@ -726,7 +726,15 @@ export default {
downloadFileByUrl(url, attachmentName(row)); downloadFileByUrl(url, attachmentName(row));
}, },
batchDownload() { batchDownload() {
(this.selected.length ? this.selected : this.rows || []).forEach(this.download); const sourceRows = this.selected.length ? this.selected : this.rows || [];
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
return;
}
downloadableRows.forEach((row, index) => {
window.setTimeout(() => this.download(row), index * 400);
});
}, },
formatSize(size) { formatSize(size) {
const value = Number(size); const value = Number(size);
@@ -1666,6 +1666,7 @@ const downloadTemplate = async () => {
ElMessage.error('模板下载失败'); ElMessage.error('模板下载失败');
} }
}; };
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
const validateImportRows = () => { const validateImportRows = () => {
if (!rows.value.length) { if (!rows.value.length) {
ElMessage.warning('请上传至少一条运单明细'); ElMessage.warning('请上传至少一条运单明细');
@@ -1677,6 +1678,11 @@ const validateImportRows = () => {
ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`); ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`);
return false; return false;
} }
const driverPhone = String(row.driverPhone ?? '').trim();
if (driverPhone && !isValidMobile(driverPhone)) {
ElMessage.warning(`第${index + 1}行司机/船长手机号格式不正确,应为11位数字`);
return false;
}
} }
return true; return true;
}; };
+1 -1
View File
@@ -1022,7 +1022,7 @@
v-model="detailBillingPlanBox" v-model="detailBillingPlanBox"
:value="detailBillingPlanForm" :value="detailBillingPlanForm"
:index="detailBillingPlanIndex" :index="detailBillingPlanIndex"
:settlement-currency="detail.settlementCurrency || form.settlementCurrency" :settlement-currency="detailRow.settlementCurrency || form.settlementCurrency"
readonly readonly
/> />
+94 -21
View File
@@ -35,8 +35,16 @@
plain plain
:loading="oaSyncLoading" :loading="oaSyncLoading"
v-if="isAdministrator" v-if="isAdministrator"
@click="handleOaOrgSync" @click="handleOaOrgFullSync"
>自动同步组织 >全量同步
</el-button>
<el-button
type="primary"
plain
:loading="oaSyncLoading"
v-if="isAdministrator"
@click="handleOaOrgIncrementalSync"
>增量同步
</el-button> </el-button>
</template> </template>
<template #menu="scope"> <template #menu="scope">
@@ -191,10 +199,12 @@ const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const ORGANIZATION_TYPE_DICT_CODE = 'organization_type'; const ORGANIZATION_TYPE_DICT_CODE = 'organization_type';
/** /**
* 组织类型名称 → 字典数字键(兼容 dictKey 配成中文的情况) * 组织类型名称 → 字典数字键(兼容 dictKey 配成中文的情况)
* 与业务字典 organization_type 及历史别名对齐;优先仍以数字 dictKey 为准
*/ */
const ORGANIZATION_TYPE_CODE_BY_LABEL = { const ORGANIZATION_TYPE_CODE_BY_LABEL = {
集团: 1, 集团: 1,
一级集团: 1, 一级集团: 1,
公司: 2,
事业部: 2, 事业部: 2,
二级事业部: 2, 二级事业部: 2,
部门: 3, 部门: 3,
@@ -234,6 +244,7 @@ export default {
oaSyncCancelled: false, oaSyncCancelled: false,
oaSyncStatus: 'running', oaSyncStatus: 'running',
oaSyncStage: 'company', oaSyncStage: 'company',
oaSyncMode: 'full',
oaSyncClearNonTop: false, oaSyncClearNonTop: false,
oaSyncController: null, oaSyncController: null,
oaSyncProgress: { oaSyncProgress: {
@@ -278,6 +289,7 @@ export default {
minWidth: 160, minWidth: 160,
order: 80, order: 80,
search: true, search: true,
searchOrder: 2,
rules: [ rules: [
{ {
required: true, required: true,
@@ -389,6 +401,8 @@ export default {
prop: 'shortName', prop: 'shortName',
minWidth: 140, minWidth: 140,
order: 60, order: 60,
search: true,
searchOrder: 1,
}, },
{ {
label: '助记码', label: '助记码',
@@ -462,15 +476,33 @@ export default {
{ {
label: '排序', label: '排序',
prop: 'sort', prop: 'sort',
type: 'number', type: 'input',
align: 'right', align: 'right',
width: 80, width: 80,
order: 30, order: 30,
change: ({ value }) => {
if (value === undefined || value === null || value === '') {
return;
}
const digits = String(value).replace(/\D/g, '');
if (String(value) !== digits) {
this.form.sort = digits;
}
},
rules: [ rules: [
{ {
required: true, validator: (rule, value, callback) => {
message: '请输入排序', if (value === '' || value === undefined || value === null) {
trigger: 'blur', callback();
return;
}
if (!/^\d+$/.test(String(value).trim())) {
callback(new Error('排序只能输入数字'));
return;
}
callback();
},
trigger: ['blur', 'change'],
}, },
], ],
}, },
@@ -509,12 +541,10 @@ export default {
value: 'tenantId', value: 'tenantId',
}, },
hide: true, hide: true,
search: website.tenantMode,
}, },
{ {
label: '机构全称', label: '机构全称',
prop: 'fullName', prop: 'fullName',
search: true,
hide: true, hide: true,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
@@ -568,16 +598,17 @@ export default {
return ids.join(','); return ids.join(',');
}, },
oaSyncDialogTitle() { oaSyncDialogTitle() {
const modeLabel = this.oaSyncMode === 'incremental' ? '增量同步' : '全量同步';
if (this.oaSyncStatus === 'done') { if (this.oaSyncStatus === 'done') {
return '同步完成'; return `${modeLabel}完成`;
} }
if (this.oaSyncStatus === 'cancelled') { if (this.oaSyncStatus === 'cancelled') {
return '已取消同步'; return `已取消${modeLabel}`;
} }
if (this.oaSyncStatus === 'error') { if (this.oaSyncStatus === 'error') {
return '同步失败'; return `${modeLabel}失败`;
} }
return '自动同步组织'; return modeLabel;
}, },
oaSyncStageLabel() { oaSyncStageLabel() {
if (this.oaSyncStage === 'clear') { if (this.oaSyncStage === 'clear') {
@@ -617,10 +648,10 @@ export default {
}, },
}, },
methods: { methods: {
handleOaOrgSync() { handleOaOrgFullSync() {
this.$confirm( this.$confirm(
'是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接同步。', '全量同步将拉取 OA 全部公司/部门;已存在且 OA 更新时间一致的记录会跳过。是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接全量同步。',
'提示', '全量同步',
{ {
confirmButtonText: '是', confirmButtonText: '是',
cancelButtonText: '否', cancelButtonText: '否',
@@ -630,20 +661,37 @@ export default {
} }
) )
.then(() => { .then(() => {
this.startOaOrgSync(true); this.startOaOrgSync({ clearNonTop: true, incremental: false });
}) })
.catch(action => { .catch(action => {
if (action === 'cancel') { if (action === 'cancel') {
this.startOaOrgSync(false); this.startOaOrgSync({ clearNonTop: false, incremental: false });
} }
}); });
}, },
startOaOrgSync(clearNonTop) { handleOaOrgIncrementalSync() {
this.$confirm(
'增量同步仅拉取本地最大 OA 更新时间之后有变更的组织;不存在的新增,已存在且更新时间不一致的修改,一致的跳过。是否继续?',
'增量同步',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
type: 'info',
}
)
.then(() => {
this.startOaOrgSync({ clearNonTop: false, incremental: true });
})
.catch(() => {});
},
startOaOrgSync({ clearNonTop = false, incremental = false } = {}) {
this.oaSyncLoading = true; this.oaSyncLoading = true;
this.oaSyncVisible = true; this.oaSyncVisible = true;
this.oaSyncRunning = true; this.oaSyncRunning = true;
this.oaSyncCancelled = false; this.oaSyncCancelled = false;
this.oaSyncStatus = 'running'; this.oaSyncStatus = 'running';
this.oaSyncMode = incremental ? 'incremental' : 'full';
this.oaSyncClearNonTop = !!clearNonTop; this.oaSyncClearNonTop = !!clearNonTop;
this.oaSyncStage = clearNonTop ? 'clear' : 'company'; this.oaSyncStage = clearNonTop ? 'clear' : 'company';
this.oaSyncController = new AbortController(); this.oaSyncController = new AbortController();
@@ -666,10 +714,21 @@ export default {
}, },
async runOaOrgSyncStage(syncApi) { async runOaOrgSyncStage(syncApi) {
const size = 20; const size = 20;
const incremental = this.oaSyncMode === 'incremental';
let current = 1; let current = 1;
let modifiedSince = '';
while (!this.oaSyncCancelled) { while (!this.oaSyncCancelled) {
const res = await syncApi(current, size, this.oaSyncController?.signal); const res = await syncApi(
current,
size,
this.oaSyncController?.signal,
incremental,
modifiedSince
);
const page = res?.data?.data || {}; const page = res?.data?.data || {};
if (!modifiedSince && page.modifiedSince) {
modifiedSince = page.modifiedSince;
}
this.oaSyncProgress.current = page.current || current; this.oaSyncProgress.current = page.current || current;
this.oaSyncProgress.size = page.size || size; this.oaSyncProgress.size = page.size || size;
this.oaSyncProgress.total = page.total || 0; this.oaSyncProgress.total = page.total || 0;
@@ -806,7 +865,7 @@ export default {
}, },
/** /**
* 业务字典项转下拉选项。后端 deptCategory 为 Integer,value 必须始终为数字。 * 业务字典项转下拉选项。后端 deptCategory 为 Integer,value 必须始终为数字。
* 优先用数字 dictKey;若 dictKey 配成中文,则按名称映射已知编码。 * 优先用数字 dictKey;其次按名称映射;再回退 sort,避免字典项被静默丢弃。
*/ */
normalizeOrganizationTypeOption(item) { normalizeOrganizationTypeOption(item) {
const label = item?.dictValue ?? item?.label; const label = item?.dictValue ?? item?.label;
@@ -833,10 +892,19 @@ export default {
value: codeByLabel, value: codeByLabel,
}; };
} }
// 未命中名称映射时,用排序号兜底,保证与业务字典项一致展示
const sortNumber = Number(item?.sort);
if (!Number.isNaN(sortNumber) && sortNumber >= 1) {
return {
label: labelText,
value: sortNumber,
};
}
return null; return null;
}, },
/** /**
* 提交前将组织类型规范为 Integer;空状态字段去掉,避免 "" 反序列化失败。 * 提交前将组织类型规范为 Integer;空状态/排序字段去掉,避免 "" 反序列化失败。
* 排序为空时由后端默认填充 1。
*/ */
prepareDeptSubmitRow(row) { prepareDeptSubmitRow(row) {
const category = this.resolveDeptCategoryValue(row.deptCategory); const category = this.resolveDeptCategoryValue(row.deptCategory);
@@ -849,6 +917,11 @@ export default {
} else { } else {
row.status = Number(row.status); row.status = Number(row.status);
} }
if (row.sort === '' || row.sort === null || row.sort === undefined) {
delete row.sort;
} else {
row.sort = Number(String(row.sort).trim());
}
return true; return true;
}, },
resolveDeptCategoryValue(value) { resolveDeptCategoryValue(value) {
+145 -127
View File
@@ -51,72 +51,31 @@
<el-button <el-button
type="primary" type="primary"
plain plain
icon="el-icon-operation" :loading="iamSyncLoading"
v-if="isAdministrator && !auditMode" v-if="isAdministrator && !auditMode"
@click="handleAudit" @click="handleOaPersonFullSync"
>审 核 >全量同步
</el-button> </el-button>
<el-button <el-button
type="primary" type="primary"
plain plain
:loading="iamSyncLoading" :loading="iamSyncLoading"
v-if="isAdministrator && !auditMode" v-if="isAdministrator && !auditMode"
@click="handleIamSync" @click="handleOaPersonIncrementalSync"
>同步人员 >增量同步
</el-button>
<el-button
type="success"
plain
icon="el-icon-check"
v-if="isAdministrator && auditMode"
@click="handleAuditPass"
>通 过
</el-button>
<el-button
type="danger"
plain
icon="el-icon-close"
v-if="isAdministrator && auditMode"
@click="handleAuditRefuse"
>拒 绝
</el-button> </el-button>
<el-button <el-button
type="primary" type="primary"
plain v-if="isAdministrator && !auditMode"
icon="el-icon-refresh-left" @click="handleImport"
v-if="isAdministrator && auditMode" >导入
@click="handleAuditBack"
>返 回
</el-button> </el-button>
<el-button <el-button
type="info" type="primary"
plain
v-if="permission.user_role && !auditMode"
icon="el-icon-user"
@click="handleGrant"
>角色配置
</el-button>
<el-dropdown
v-if="isAdministrator && !auditMode" v-if="isAdministrator && !auditMode"
@command="handleDataManageCommand" @click="handleExport"
style="margin-left: 10px" >导出
> </el-button>
<el-button type="primary" plain icon="el-icon-files">
数据管理<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="import">
<el-icon><upload /></el-icon>
导入数据
</el-dropdown-item>
<el-dropdown-item command="export">
<el-icon><download /></el-icon>
导出数据
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template> </template>
<template #menu-right> <template #menu-right>
<el-button <el-button
@@ -132,47 +91,23 @@
>锁定管理 >锁定管理
</el-button> </el-button>
</template> </template>
<template #menu="{ row, index, size }"> <template #account="{ row }">
<el-link type="primary" @click.stop="openUserDialog('view', row)">{{ row.account }}</el-link>
</template>
<template #menu="{ row }">
<el-link <el-link
type="primary" type="primary"
v-if="this.permission.user_view" v-if="permission.user_edit && !auditMode"
@click.stop="$refs.crud.rowView(row, index)"
>查看
</el-link>
<el-link
type="primary"
v-if="this.permission.user_edit"
@click.stop="openUserDialog('edit', row)" @click.stop="openUserDialog('edit', row)"
>编辑 >修改
</el-link>
<el-link
type="primary"
v-if="permission.user_edit && !auditMode"
@click.stop="handleStatus(row)"
>
{{ Number(row.status) === 1 ? '停用' : '启用' }}
</el-link> </el-link>
<el-dropdown @command="command => handleDropdownCommand(command, row, index)">
<el-link type="primary">更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="setLeader" v-if="isAdministrator">
{{ row.isLeader === 1 ? '取消主管' : '设置主管' }}
</el-dropdown-item>
<el-dropdown-item command="grantRole" v-if="permission.user_role">
角色配置
</el-dropdown-item>
<el-dropdown-item command="authLog" v-if="isAdministrator">
认证日志
</el-dropdown-item>
<el-dropdown-item command="lockUser" v-if="isAdministrator">
锁定账号
</el-dropdown-item>
<el-dropdown-item command="resetPassword" v-if="permission.user_reset">
密码重置
</el-dropdown-item>
<el-dropdown-item command="setPassword" v-if="permission.user_reset">
修改密码
</el-dropdown-item>
<el-dropdown-item command="delete" v-if="this.permission.user_delete">
删除用户
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template> </template>
<template #roleName="{ row }"> <template #roleName="{ row }">
<el-tag>{{ row.roleName }}</el-tag> <el-tag>{{ row.roleName }}</el-tag>
@@ -191,7 +126,7 @@
<el-dialog <el-dialog
v-model="userDialog" v-model="userDialog"
:title="userDialogMode === 'add' ? '新增用户' : '编辑用户'" :title="userDialogTitle"
width="90%" width="90%"
top="5vh" top="5vh"
append-to-body append-to-body
@@ -199,12 +134,12 @@
class="user-edit-dialog dialog-form--carded" class="user-edit-dialog dialog-form--carded"
> >
<template #header> <template #header>
<span class="dialog-title">{{ userDialogMode === 'add' ? '新增用户' : '编辑用户' }}</span> <span class="dialog-title">{{ userDialogTitle }}</span>
</template> </template>
<section-card title="用户信息"> <section-card title="用户信息">
<el-form ref="userFormRef" :model="form" :rules="userRules" label-position="right" label-width="calc(6em + 24px)"> <el-form ref="userFormRef" :model="form" :rules="isUserViewMode ? {} : userRules" label-position="right" label-width="calc(6em + 24px)">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"><el-form-item label="账号名" prop="account"><el-input v-model="form.account" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="账号名" prop="account"><el-input v-model="form.account" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col v-if="userDialogMode === 'add'" :span="6"> <el-col v-if="userDialogMode === 'add'" :span="6">
<el-form-item label="登录密码" prop="password"> <el-form-item label="登录密码" prop="password">
<el-input <el-input
@@ -216,17 +151,18 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"><el-form-item label="姓名" prop="realName"><el-input v-model="form.realName" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="姓名" prop="realName"><el-input v-model="form.realName" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col> <el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex" :disabled="isUserViewMode" clearable placeholder="请选择"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly /></el-form-item></el-col> <el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="所属组织" prop="deptId"> <el-form-item label="所属组织" prop="deptId">
<el-cascader <el-cascader
v-model="deptCascaderValue" v-model="deptCascaderValue"
:options="permissionDeptTree" :options="permissionDeptTree"
:props="deptCascaderProps" :props="deptCascaderProps"
:disabled="isUserViewMode"
clearable clearable
filterable filterable
collapse-tags collapse-tags
@@ -235,10 +171,10 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col> <el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col> <el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col> <el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注"> <el-form-item label="备注">
<el-input <el-input
@@ -247,6 +183,7 @@
:rows="2" :rows="2"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
:disabled="isUserViewMode"
style="width: 100%" style="width: 100%"
/> />
</el-form-item> </el-form-item>
@@ -260,18 +197,28 @@
<el-table-column type="index" label="序号" width="70" /> <el-table-column type="index" label="序号" width="70" />
<el-table-column prop="id" label="角色编号" /> <el-table-column prop="id" label="角色编号" />
<el-table-column prop="title" label="角色名称" /> <el-table-column prop="title" label="角色名称" />
<el-table-column label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column> <el-table-column v-if="!isUserViewMode" label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column>
</el-table> </el-table>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限"> <el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限">
<el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" /> <el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="客商权限"> <el-tab-pane label="客商权限">
<div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0">包含新增的客商</el-checkbox></div> <div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0" :disabled="isUserViewMode">包含新增的客商</el-checkbox></div>
<el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table> <el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" :selectable="isCustomerSelectable" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<template #footer><span class="dialog-footer"><el-button type="primary" @click="submitUserForm">保存</el-button><el-button @click="userDialog = false">取消</el-button><el-button v-if="userDialogMode === 'edit'" @click="resetDialogPassword">重置密码</el-button><el-button @click="handleAddRole">添加角色</el-button></span></template> <template #footer>
<span v-if="isUserViewMode" class="dialog-footer">
<el-button @click="userDialog = false">关闭</el-button>
</span>
<span v-else class="dialog-footer">
<el-button type="primary" @click="submitUserForm">保存</el-button>
<el-button @click="userDialog = false">取消</el-button>
<el-button v-if="userDialogMode === 'edit'" @click="resetDialogPassword">重置密码</el-button>
<el-button @click="handleAddRole">添加角色</el-button>
</span>
</template>
</el-dialog> </el-dialog>
<!-- 用户角色配置 --> <!-- 用户角色配置 -->
@@ -442,6 +389,7 @@ import {
remove, remove,
update, update,
add, add,
changeStatus,
syncIamAccounts, syncIamAccounts,
grant, grant,
resetPassword, resetPassword,
@@ -494,6 +442,7 @@ export default {
oaSyncRunning: false, oaSyncRunning: false,
oaSyncCancelled: false, oaSyncCancelled: false,
oaSyncStatus: 'running', oaSyncStatus: 'running',
oaSyncMode: 'full',
oaSyncController: null, oaSyncController: null,
oaSyncProgress: { oaSyncProgress: {
current: 0, current: 0,
@@ -570,16 +519,17 @@ export default {
computed: { computed: {
...mapGetters(['userInfo', 'permission', 'isAdministrator']), ...mapGetters(['userInfo', 'permission', 'isAdministrator']),
oaSyncDialogTitle() { oaSyncDialogTitle() {
const modeLabel = this.oaSyncMode === 'incremental' ? '增量同步' : '全量同步';
if (this.oaSyncStatus === 'done') { if (this.oaSyncStatus === 'done') {
return '同步完成'; return `${modeLabel}完成`;
} }
if (this.oaSyncStatus === 'cancelled') { if (this.oaSyncStatus === 'cancelled') {
return '已取消同步'; return `已取消${modeLabel}`;
} }
if (this.oaSyncStatus === 'error') { if (this.oaSyncStatus === 'error') {
return '同步失败'; return `${modeLabel}失败`;
} }
return '同步人员'; return modeLabel;
}, },
oaSyncTotalPage() { oaSyncTotalPage() {
const total = Number(this.oaSyncProgress.total) || 0; const total = Number(this.oaSyncProgress.total) || 0;
@@ -642,6 +592,18 @@ export default {
this.$refs.userFormRef?.validateField('deptId'); this.$refs.userFormRef?.validateField('deptId');
}, },
}, },
isUserViewMode() {
return this.userDialogMode === 'view';
},
userDialogTitle() {
if (this.userDialogMode === 'add') {
return '新增用户';
}
if (this.userDialogMode === 'view') {
return '用户详情';
}
return '编辑用户';
},
permissionList() { permissionList() {
return { return {
addBtn: this.validData(this.permission.user_add, false), addBtn: this.validData(this.permission.user_add, false),
@@ -714,28 +676,35 @@ export default {
this.showResetPassword(res.data.data); this.showResetPassword(res.data.data);
}); });
}, },
handleDataManageCommand(command) { handleOaPersonFullSync() {
if (command === 'import') { this.$confirm('全量同步将拉取 OA 全部人员及组织;已存在且 OA 更新时间一致的记录会跳过。是否继续?', '全量同步', {
this.handleImport();
} else if (command === 'export') {
this.handleExport();
}
},
handleIamSync() {
this.$confirm('确定从OA同步人员及组织信息?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(() => { }).then(() => {
this.startOaSync(); this.startOaSync(false);
}); });
}, },
startOaSync() { handleOaPersonIncrementalSync() {
this.$confirm(
'增量同步仅拉取本地最大 OA 更新时间之后有变更的人员;不存在的新增,已存在且更新时间不一致的修改,一致的跳过。是否继续?',
'增量同步',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(() => {
this.startOaSync(true);
});
},
startOaSync(incremental = false) {
this.iamSyncLoading = true; this.iamSyncLoading = true;
this.oaSyncVisible = true; this.oaSyncVisible = true;
this.oaSyncRunning = true; this.oaSyncRunning = true;
this.oaSyncCancelled = false; this.oaSyncCancelled = false;
this.oaSyncStatus = 'running'; this.oaSyncStatus = 'running';
this.oaSyncMode = incremental ? 'incremental' : 'full';
this.oaSyncController = new AbortController(); this.oaSyncController = new AbortController();
this.oaSyncProgress = { this.oaSyncProgress = {
current: 0, current: 0,
@@ -756,11 +725,22 @@ export default {
}, },
async runOaSyncPages() { async runOaSyncPages() {
const size = 50; const size = 50;
const incremental = this.oaSyncMode === 'incremental';
let current = 1; let current = 1;
let modifiedSince = '';
try { try {
while (!this.oaSyncCancelled) { while (!this.oaSyncCancelled) {
const res = await syncIamAccounts(current, size, this.oaSyncController?.signal); const res = await syncIamAccounts(
current,
size,
this.oaSyncController?.signal,
incremental,
modifiedSince
);
const page = res?.data?.data || {}; const page = res?.data?.data || {};
if (!modifiedSince && page.modifiedSince) {
modifiedSince = page.modifiedSince;
}
this.oaSyncProgress.current = page.current || current; this.oaSyncProgress.current = page.current || current;
this.oaSyncProgress.size = page.size || size; this.oaSyncProgress.size = page.size || size;
this.oaSyncProgress.total = page.total || 0; this.oaSyncProgress.total = page.total || 0;
@@ -892,11 +872,33 @@ export default {
.filter(id => String(id) !== String(roleId)); .filter(id => String(id) !== String(roleId));
}, },
handleDataScopeCheck(_, checked) { handleDataScopeCheck(_, checked) {
if (this.isUserViewMode) {
return;
}
this.form.dataScopeDeptIds = checked.checkedKeys; this.form.dataScopeDeptIds = checked.checkedKeys;
}, },
handleCustomerSelection(rows) { handleCustomerSelection(rows) {
if (this.isUserViewMode) {
return;
}
this.form.customerIds = rows.map(row => row.id); this.form.customerIds = rows.map(row => row.id);
}, },
isCustomerSelectable() {
return !this.isUserViewMode;
},
normalizeSexDisplay(sex) {
if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) {
return undefined;
}
return Number(sex);
},
normalizeSexSubmit(sex, clearInvalid = false) {
const value = Number(sex);
if (value === 1 || value === 2 || value === 3) {
return value;
}
return clearInvalid ? -1 : null;
},
formatDeptNames(deptName) { formatDeptNames(deptName) {
if (!deptName) { if (!deptName) {
return []; return [];
@@ -906,6 +908,20 @@ export default {
.map(item => item.trim()) .map(item => item.trim())
.filter(Boolean); .filter(Boolean);
}, },
handleStatus(row) {
const nextStatus = Number(row.status) === 1 ? 0 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
this.$confirm(`确定${actionName}账号【${row.account}】?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => changeStatus(row.id, nextStatus))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
openUserDialog(mode, row = {}) { openUserDialog(mode, row = {}) {
this.userDialogMode = mode; this.userDialogMode = mode;
if (mode === 'add') { if (mode === 'add') {
@@ -915,7 +931,7 @@ export default {
realName: '', realName: '',
phone: '', phone: '',
email: '', email: '',
sex: 3, sex: undefined,
postId: '', postId: '',
deptId: [], deptId: [],
code: '', code: '',
@@ -946,6 +962,7 @@ export default {
personCategory: user.personCategory || 1, personCategory: user.personCategory || 1,
dataScopeRange: user.dataScopeRange || 1, dataScopeRange: user.dataScopeRange || 1,
dataLevelRange: user.dataLevelRange || 1, dataLevelRange: user.dataLevelRange || 1,
sex: this.normalizeSexDisplay(user.sex),
}; };
this.sensitiveManager.saveInitialData(this.form); this.sensitiveManager.saveInitialData(this.form);
this.userDialog = true; this.userDialog = true;
@@ -967,6 +984,7 @@ export default {
postId: func.join(this.form.postId), postId: func.join(this.form.postId),
roleId: func.join(this.form.roleId), roleId: func.join(this.form.roleId),
leaderId: func.join(this.form.leaderId), leaderId: func.join(this.form.leaderId),
sex: this.normalizeSexSubmit(this.form.sex, this.userDialogMode !== 'add'),
}; };
if (this.userDialogMode !== 'add') { if (this.userDialogMode !== 'add') {
delete row.password; delete row.password;
@@ -987,6 +1005,7 @@ export default {
row.roleId = func.join(row.roleId); row.roleId = func.join(row.roleId);
row.postId = func.join(row.postId); row.postId = func.join(row.postId);
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
row.sex = this.normalizeSexSubmit(row.sex);
add(row).then( add(row).then(
() => { () => {
this.initFlag = false; this.initFlag = false;
@@ -1008,6 +1027,7 @@ export default {
row.roleId = func.join(row.roleId); row.roleId = func.join(row.roleId);
row.postId = func.join(row.postId); row.postId = func.join(row.postId);
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
row.sex = this.normalizeSexSubmit(row.sex, true);
// 获取需要提交的数据 // 获取需要提交的数据
const submitData = this.sensitiveManager.getSubmitData(row); const submitData = this.sensitiveManager.getSubmitData(row);
update(submitData).then( update(submitData).then(
@@ -1334,9 +1354,7 @@ export default {
if (this.form.hasOwnProperty('leaderId')) { if (this.form.hasOwnProperty('leaderId')) {
this.form.leaderId = func.split(this.form.leaderId); this.form.leaderId = func.split(this.form.leaderId);
} }
if (this.form.sex === -1) { this.form.sex = this.normalizeSexDisplay(this.form.sex);
this.form.sex = '';
}
// 保存初始脱敏数据 // 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.form); this.sensitiveManager.saveInitialData(this.form);
}); });
+35 -22
View File
@@ -339,7 +339,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="经营范围" prop="businessScope" required> <el-form-item label="经营范围" prop="businessScope">
<el-select v-model="archiveForm.businessScope" multiple filterable clearable> <el-select v-model="archiveForm.businessScope" multiple filterable clearable>
<el-option <el-option
v-for="item in businessScopeOptions" v-for="item in businessScopeOptions"
@@ -460,9 +460,8 @@
v-model="deptCascaderValue" v-model="deptCascaderValue"
:options="deptTree" :options="deptTree"
:props="deptCascaderProps" :props="deptCascaderProps"
clearable disabled
filterable placeholder="桂物物流集团"
placeholder="请选择 所属组织"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -1552,6 +1551,8 @@ import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/m
const customerArchiveListRoute = '/vehicle/customer-archive'; const customerArchiveListRoute = '/vehicle/customer-archive';
const customerArchiveFormRoute = '/vehicle/customer-archive/form'; const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh'; const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '桂物物流集团';
const createTimeRangeMap = { const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1788,15 +1789,6 @@ export default {
{ required: true, message: '请输入详细地址', trigger: 'blur' }, { required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' }, { max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
], ],
businessScope: [
{
required: true,
type: 'array',
min: 1,
message: '请选择经营范围',
trigger: 'change',
},
],
remark: [{ max: 500, message: '备注最多500个字符', trigger: 'blur' }], remark: [{ max: 500, message: '备注最多500个字符', trigger: 'blur' }],
}, },
contactRules: { contactRules: {
@@ -3043,17 +3035,34 @@ export default {
}); });
return result; return result;
}, },
setDefaultTopLevelDept() { findTopLevelDeptByName(name) {
const deptIds = Array.isArray(this.archiveForm.deptId) const targetName = String(name || '').trim();
? this.archiveForm.deptId if (!targetName) {
: this.archiveForm.deptId return null;
? [this.archiveForm.deptId] }
: []; return (
const firstTopLevelDept = this.deptTree[0]; (this.deptTree || []).find(item => {
if (!this.isArchivePage || this.$route.query.id || deptIds.length || !firstTopLevelDept) { const label = String(item.label || item.title || item.deptName || item.name || '').trim();
return label === targetName;
}) || null
);
},
/**
* 客商所属组织固定为顶层「桂物物流集团」,表单不可编辑。
*/
applyFixedOwnerDept() {
if (!this.isArchivePage || this.readonly) {
return; return;
} }
this.onDeptChange([firstTopLevelDept.value]); const fixedDept =
this.findTopLevelDeptByName(FIXED_OWNER_DEPT_NAME) || (this.deptTree || [])[0] || null;
if (!fixedDept) {
return;
}
this.onDeptChange([fixedDept.value]);
},
setDefaultTopLevelDept() {
this.applyFixedOwnerDept();
}, },
onDeptChange(value) { onDeptChange(value) {
// 级联单选:value 为根到选中节点的 ID 路径,取末级作为客商所属组织 // 级联单选:value 为根到选中节点的 ID 路径,取末级作为客商所属组织
@@ -4104,6 +4113,7 @@ export default {
this.ocrQualificationUploadFiles = []; this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = []; this.selectedQualificationFiles = [];
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm); if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.applyFixedOwnerDept();
this.archiveBox = true; this.archiveBox = true;
this.loadChangeRecords(); this.loadChangeRecords();
if (this.reviewMode) this.openReviewScore(); if (this.reviewMode) this.openReviewScore();
@@ -4120,6 +4130,7 @@ export default {
this.qualificationUploadFiles = []; this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = []; this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = []; this.selectedQualificationFiles = [];
this.applyFixedOwnerDept();
this.archiveBox = true; this.archiveBox = true;
}, },
resetArchive() { resetArchive() {
@@ -4162,6 +4173,8 @@ export default {
return true; return true;
}, },
normalizeArchive() { normalizeArchive() {
// 提交前再次固定所属组织,防止被绕过
this.applyFixedOwnerDept();
const archive = JSON.parse(JSON.stringify(this.archiveForm)); const archive = JSON.parse(JSON.stringify(this.archiveForm));
if (archive.businessTermType === '长期') { if (archive.businessTermType === '长期') {
archive.businessEndDate = null; archive.businessEndDate = null;