Compare commits

..

5 Commits

5 changed files with 124 additions and 42 deletions
+3 -1
View File
@@ -77,7 +77,8 @@ export const syncOaDepartment = (
size = 20, size = 20,
signal, signal,
incremental = false, incremental = false,
modifiedSince modifiedSince,
companyOaId
) => { ) => {
return request({ return request({
url: '/blade-system/dept/sync-oa-department', url: '/blade-system/dept/sync-oa-department',
@@ -87,6 +88,7 @@ export const syncOaDepartment = (
size, size,
incremental: !!incremental, incremental: !!incremental,
modifiedSince: modifiedSince || undefined, modifiedSince: modifiedSince || undefined,
companyOaId: companyOaId || undefined,
}, },
timeout: 60000, timeout: 60000,
signal, signal,
+2 -1
View File
@@ -97,7 +97,8 @@ export const config = {
action: 'startChange', action: 'startChange',
label: '变更', label: '变更',
statusProp: 'approvalStatus', statusProp: 'approvalStatus',
status: ['approved'], // 立项通过、变更通过后均可再次发起变更
status: ['approved', 'change_approved'],
permission: 'project_apply_change', permission: 'project_apply_change',
}, },
], ],
+4 -3
View File
@@ -224,7 +224,7 @@
placeholder="请选择" placeholder="请选择"
filterable filterable
clearable clearable
:disabled="isBasicInfoReadonly" :disabled="dialogReadonly"
/> />
</el-form-item> </el-form-item>
<el-form-item label="平台公司" prop="undertakeDeptId"> <el-form-item label="平台公司" prop="undertakeDeptId">
@@ -236,7 +236,7 @@
collapse-tags-tooltip collapse-tags-tooltip
clearable clearable
filterable filterable
:disabled="isBasicInfoReadonly" :disabled="dialogReadonly"
@change="handleUndertakeDeptChange" @change="handleUndertakeDeptChange"
> >
<el-option <el-option
@@ -2493,7 +2493,8 @@ export default {
loadDeptOptions() { loadDeptOptions() {
getDeptTree().then(res => { getDeptTree().then(res => {
const deptTree = res.data.data || []; const deptTree = res.data.data || [];
const businessDeptTree = ['add', 'edit', 'majorSupplement'].includes(this.dialogType) // 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织
const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType)
? this.excludeExternalOrganization(deptTree) ? this.excludeExternalOrganization(deptTree)
: deptTree; : deptTree;
this.deptOptions = this.flattenDept(businessDeptTree); this.deptOptions = this.flattenDept(businessDeptTree);
@@ -1278,10 +1278,13 @@ export default {
delete snapshot._externalKey; delete snapshot._externalKey;
delete snapshot.feeItems; delete snapshot.feeItems;
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {}); snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
return snapshot; return snapshot;
}); });
}, },
serializeExternalRows() { serializeExternalRows() {
const duplicateIds = new Set(this.duplicateRows.map(row => String(row.id)));
return this.externalDetails.map(row => { return this.externalDetails.map(row => {
const snapshot = { ...row }; const snapshot = { ...row };
delete snapshot._sourceRows; delete snapshot._sourceRows;
@@ -1290,6 +1293,7 @@ export default {
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {}); snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime); snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime); snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
return snapshot; return snapshot;
}); });
}, },
@@ -1336,6 +1340,11 @@ export default {
} }
}, },
async handleComplete() { async handleComplete() {
const unmatchedInternal = this.internalDetails.filter(item => item.matchResult !== 'matched').length;
const unmatchedExternal = this.externalDetails.filter(item => item.matchStatus !== 'matched').length;
if (this.duplicateRows.length || unmatchedInternal || unmatchedExternal) {
return this.$message.warning('所有内外部账单明细必须一一匹配且不存在疑似重复');
}
if ( if (
Number(this.form.differenceCount || 0) !== 0 || Number(this.form.differenceCount || 0) !== 0 ||
Number(this.form.differenceQuantity || 0) !== 0 || Number(this.form.differenceQuantity || 0) !== 0 ||
@@ -1694,8 +1703,11 @@ export default {
}, },
normalizeExternalDateTime(value) { normalizeExternalDateTime(value) {
if (!value) return null; if (!value) return null;
const text = String(value); const text = String(value).trim();
return text.length === 10 ? `${text}T00:00:00` : text; if (!text) return null;
const date = this.$dayjs(text);
if (date.isValid()) return date.format('YYYY-MM-DD HH:mm:ss');
return text.length === 10 ? `${text} 00:00:00` : text.replace('T', ' ');
}, },
refreshLocalStats() { refreshLocalStats() {
const internalQuantity = this.internalDetails.reduce( const internalQuantity = this.internalDetails.reduce(
+101 -35
View File
@@ -153,7 +153,12 @@
/> />
<div class="oa-sync-meta" v-if="oaSyncStage === 'clear'">当前阶段:{{ oaSyncStageLabel }}</div> <div class="oa-sync-meta" v-if="oaSyncStage === 'clear'">当前阶段:{{ oaSyncStageLabel }}</div>
<div class="oa-sync-meta" v-else> <div class="oa-sync-meta" v-else>
当前阶段:{{ oaSyncStageLabel }},第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页 当前阶段:{{ oaSyncStageLabel }}
<template v-if="oaSyncStage === 'department' && oaSyncProgress.companyTotal">
,公司 {{ oaSyncProgress.companyIndex || 0 }} / {{ oaSyncProgress.companyTotal }}
<template v-if="oaSyncProgress.companyOaId">(oaId={{ oaSyncProgress.companyOaId }})</template>
</template>
,第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页
</div> </div>
<div class="oa-sync-stats"> <div class="oa-sync-stats">
<div class="oa-sync-stat"> <div class="oa-sync-stat">
@@ -257,6 +262,9 @@ export default {
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '', dumpFilePath: '',
companyOaId: '',
companyIndex: 0,
companyTotal: 0,
}, },
parentId: 0, parentId: 0,
page: { page: {
@@ -290,7 +298,9 @@ export default {
{ {
label: '组织名称', label: '组织名称',
prop: 'deptName', prop: 'deptName',
minWidth: 160, minWidth: 320,
align: 'left',
headerAlign: 'left',
order: 80, order: 80,
search: true, search: true,
searchOrder: 2, searchOrder: 2,
@@ -642,12 +652,18 @@ export default {
} }
const totalPage = this.oaSyncTotalPage; const totalPage = this.oaSyncTotalPage;
const current = Number(this.oaSyncProgress.current) || 0; const current = Number(this.oaSyncProgress.current) || 0;
const stageBase = this.oaSyncStage === 'department' ? 50 : 0; if (this.oaSyncStage === 'department') {
if (totalPage <= 0) { const companyTotal = Math.max(1, Number(this.oaSyncProgress.companyTotal) || 1);
return stageBase; const companyIndex = Math.max(1, Number(this.oaSyncProgress.companyIndex) || 1);
const companyBase = ((companyIndex - 1) / companyTotal) * 50;
const pageRatio = totalPage > 0 ? Math.min(1, current / totalPage) : 0;
const companyPagePercent = (pageRatio * 50) / companyTotal;
return Math.min(99, Math.round(50 + companyBase + companyPagePercent));
} }
const stagePercent = Math.min(50, Math.round((current / totalPage) * 50)); if (totalPage <= 0) {
return Math.min(99, stageBase + stagePercent); return 0;
}
return Math.min(49, Math.round((current / totalPage) * 50));
}, },
oaSyncProgressStatus() { oaSyncProgressStatus() {
if (this.oaSyncStatus === 'done') { if (this.oaSyncStatus === 'done') {
@@ -661,25 +677,7 @@ export default {
}, },
methods: { methods: {
handleOaOrgFullSync() { handleOaOrgFullSync() {
this.$confirm( this.startOaOrgSync({ clearNonTop: false, incremental: false });
'全量同步将拉取 OA 公司/部门(仅保留二级公司 oaId=2、135 及其子公司与部门);已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=1/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
'全量同步',
{
confirmButtonText: '是',
cancelButtonText: '否',
distinguishCancelAndClose: true,
closeOnClickModal: false,
type: 'warning',
}
)
.then(() => {
this.startOaOrgSync({ clearNonTop: true, incremental: false });
})
.catch(action => {
if (action === 'cancel') {
this.startOaOrgSync({ clearNonTop: false, incremental: false });
}
});
}, },
handleOaOrgIncrementalSync() { handleOaOrgIncrementalSync() {
this.$confirm( this.$confirm(
@@ -714,6 +712,9 @@ export default {
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '', dumpFilePath: '',
companyOaId: '',
companyIndex: 0,
companyTotal: 0,
}; };
this.runOaOrgSyncPages(); this.runOaOrgSyncPages();
}, },
@@ -725,6 +726,25 @@ export default {
error?.name === 'AbortError' error?.name === 'AbortError'
); );
}, },
applyOaOrgSyncPage(page, current, size) {
this.oaSyncProgress.current = page.current || current;
this.oaSyncProgress.size = page.size || size;
this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.companyOaId != null) {
this.oaSyncProgress.companyOaId = page.companyOaId || '';
}
if (page.companyIndex != null) {
this.oaSyncProgress.companyIndex = page.companyIndex || 0;
}
if (page.companyTotal != null) {
this.oaSyncProgress.companyTotal = page.companyTotal || 0;
}
},
async runOaOrgSyncStage(syncApi) { async runOaOrgSyncStage(syncApi) {
const size = 20; const size = 20;
const incremental = this.oaSyncMode === 'incremental'; const incremental = this.oaSyncMode === 'incremental';
@@ -742,20 +762,57 @@ export default {
if (!modifiedSince && page.modifiedSince) { if (!modifiedSince && page.modifiedSince) {
modifiedSince = page.modifiedSince; modifiedSince = page.modifiedSince;
} }
this.oaSyncProgress.current = page.current || current; this.applyOaOrgSyncPage(page, current, size);
this.oaSyncProgress.size = page.size || size;
this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.finished) { if (page.finished) {
break; break;
} }
current += 1; current += 1;
} }
}, },
/**
* 部门阶段:公司同步完成后,按公司 oaId 轮询;当前公司部门拉完后切换下一公司并重置页码
*/
async runOaOrgDepartmentSyncStage() {
const size = 20;
const incremental = this.oaSyncMode === 'incremental';
let current = 1;
let modifiedSince = '';
let companyOaId = '';
while (!this.oaSyncCancelled) {
const res = await syncOaDepartment(
current,
size,
this.oaSyncController?.signal,
incremental,
modifiedSince,
companyOaId
);
const page = res?.data?.data || {};
if (!modifiedSince && page.modifiedSince) {
modifiedSince = page.modifiedSince;
}
if (page.companyOaId) {
companyOaId = page.companyOaId;
}
this.applyOaOrgSyncPage(page, current, size);
if (page.finished) {
break;
}
if (page.companyFinished && page.nextCompanyOaId) {
companyOaId = page.nextCompanyOaId;
current = 1;
this.oaSyncProgress.current = 0;
this.oaSyncProgress.total = 0;
// 单公司部门拉完并重建层级后刷新树,便于中途核对挂载结果
this.parentId = 0;
this.data = [];
this.$refs.crud?.refreshTable?.();
this.onLoad(this.page, this.query);
continue;
}
current += 1;
}
},
async runOaOrgSyncPages() { async runOaOrgSyncPages() {
try { try {
if (this.oaSyncClearNonTop) { if (this.oaSyncClearNonTop) {
@@ -775,7 +832,10 @@ export default {
this.oaSyncStage = 'department'; this.oaSyncStage = 'department';
this.oaSyncProgress.current = 0; this.oaSyncProgress.current = 0;
this.oaSyncProgress.total = 0; this.oaSyncProgress.total = 0;
await this.runOaOrgSyncStage(syncOaDepartment); this.oaSyncProgress.companyOaId = '';
this.oaSyncProgress.companyIndex = 0;
this.oaSyncProgress.companyTotal = 0;
await this.runOaOrgDepartmentSyncStage();
if (this.oaSyncCancelled) { if (this.oaSyncCancelled) {
this.oaSyncStatus = 'cancelled'; this.oaSyncStatus = 'cancelled';
return; return;
@@ -1460,6 +1520,12 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* 覆盖全站表格居中:树形组织名称需左对齐,展开后缩进才可读 */
:deep(.el-table th.el-table__cell.is-left .cell),
:deep(.el-table td.el-table__cell.is-left .cell) {
text-align: left;
}
.carrier-search-form { .carrier-search-form {
padding: 12px 12px 4px; padding: 12px 12px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);