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

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