Compare commits

..

6 Commits

23 changed files with 164 additions and 153 deletions
@@ -17,6 +17,10 @@ export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post'
export const returnBill = (id, reason) => export const returnBill = (id, reason) =>
request({ url: `${baseUrl}/return`, method: 'post', data: { id, reason } }); request({ url: `${baseUrl}/return`, method: 'post', data: { id, reason } });
export const repush = id => request({ url: `${baseUrl}/repush`, method: 'post', params: { id } }); export const repush = id => request({ url: `${baseUrl}/repush`, method: 'post', params: { id } });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
export const kingdeeWriteOffPrecheck = formalSettlementId =>
request({ url: `${baseUrl}/kingdee-writeoff-precheck`, method: 'get', params: { formalSettlementId } });
export const approvalStatusOptions = [ export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
+1 -3
View File
@@ -77,8 +77,7 @@ 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',
@@ -88,7 +87,6 @@ export const syncOaDepartment = (
size, size,
incremental: !!incremental, incremental: !!incremental,
modifiedSince: modifiedSince || undefined, modifiedSince: modifiedSince || undefined,
companyOaId: companyOaId || undefined,
}, },
timeout: 60000, timeout: 60000,
signal, signal,
+1 -2
View File
@@ -97,8 +97,7 @@ 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',
}, },
], ],
@@ -19,6 +19,8 @@ export const formalSettlementTableColumns = [
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true }, { label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '当前节点', prop: 'currentNode', minWidth: 130 }, { label: '当前节点', prop: 'currentNode', minWidth: 130 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '调整标识', prop: 'adjustFlag', minWidth: 110 },
{ label: '来源原单号', prop: 'originFormalSettlementNo', minWidth: 170 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 }, { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 }, { label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 }, { label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
@@ -1,6 +1,6 @@
export const settlementAdjustmentTableColumns = [ export const settlementAdjustmentTableColumns = [
{ label: '单据号', prop: 'adjustmentNo', minWidth: 160, link: true, fixed: 'left' }, { label: '单据号', prop: 'adjustmentNo', minWidth: 160, link: true, fixed: 'left' },
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160 }, { label: '结算单号', prop: 'formalSettlementNo', minWidth: 160, link: true },
{ label: '结算单类型', prop: 'settlementTypeName', minWidth: 110 }, { label: '结算单类型', prop: 'settlementTypeName', minWidth: 110 },
{ label: '项目名称', prop: 'projectName', minWidth: 140 }, { label: '项目名称', prop: 'projectName', minWidth: 140 },
{ label: '所属组织', prop: 'deptName', minWidth: 140 }, { label: '所属组织', prop: 'deptName', minWidth: 140 },
@@ -9,6 +9,8 @@ export const settlementAdjustmentTableColumns = [
{ label: '合同名称', prop: 'contractName', minWidth: 170 }, { label: '合同名称', prop: 'contractName', minWidth: 170 },
{ label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true }, { label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true },
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true }, { label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 130, kingdee: true },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '当前节点', prop: 'currentNode', minWidth: 120 }, { label: '当前节点', prop: 'currentNode', minWidth: 120 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 }, { label: '创建人', prop: 'createUserName', minWidth: 110 },
+3 -4
View File
@@ -224,7 +224,7 @@
placeholder="请选择" placeholder="请选择"
filterable filterable
clearable clearable
:disabled="dialogReadonly" :disabled="isBasicInfoReadonly"
/> />
</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="dialogReadonly" :disabled="isBasicInfoReadonly"
@change="handleUndertakeDeptChange" @change="handleUndertakeDeptChange"
> >
<el-option <el-option
@@ -2493,8 +2493,7 @@ 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);
@@ -639,6 +639,22 @@
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center"> <el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ displayValue(row.createTime) }}</template> <template #default="{ row }">{{ displayValue(row.createTime) }}</template>
</el-table-column> </el-table-column>
<el-table-column
v-if="hasPermission('settlement_adjustment_repush')"
label="操作"
width="120"
align="center"
>
<template #default="{ row }">
<el-link
v-if="row.approvalStatus === 'approved' && row.kingdeeSyncStatus === 'synced'"
type="danger"
@click="handleAdjustmentRepush(row)"
>重新推送</el-link
>
<span v-else>-</span>
</template>
</el-table-column>
</el-table> </el-table>
</section-card> </section-card>
@@ -1048,6 +1064,7 @@ import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/m
import ProjectAdvanceWriteOff from './project-advance-write-off.vue'; import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
import * as settlementAdjustmentApi from '@/api/settlement/settlementAdjustment';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示 // 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount']; const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
@@ -1316,6 +1333,17 @@ export default {
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false); return this.isAdmin || this.validData(this.permission?.[code], false);
}, },
async handleAdjustmentRepush(row) {
await this.$confirm(
'请确认金蝶旧应付/应收单已由财务手工冲销,继续将生成全新财务应付/应收单。',
'高危操作确认',
{ type: 'warning', confirmButtonText: '确认重新推送' }
);
const res = await settlementAdjustmentApi.repush(row.id);
this.$message.success(`已生成新金蝶单据:${res.data?.data || res.data || '-'}`);
await this.loadFormalDetail(this.recordId);
this.$emit('success');
},
async loadFormalDetail(id) { async loadFormalDetail(id) {
const response = this.publicMode const response = this.publicMode
? await getMkPublicDetail('formal-settlement', id) ? await getMkPublicDetail('formal-settlement', id)
@@ -92,6 +92,7 @@
<span v-else-if="column.prop === 'kingdeeSyncStatus'">{{ <span v-else-if="column.prop === 'kingdeeSyncStatus'">{{
kingdeeSyncName(row.kingdeeSyncStatus) kingdeeSyncName(row.kingdeeSyncStatus)
}}</span> }}</span>
<span v-else-if="column.prop === 'adjustFlag'">{{ adjustFlag(row) }}</span>
<template v-else-if="column.prop === 'kingdeeLockStatus'"> <template v-else-if="column.prop === 'kingdeeLockStatus'">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top"> <el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag> <el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
@@ -275,6 +276,12 @@ export default {
kingdeeSyncName(value) { kingdeeSyncName(value) {
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-'; return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
}, },
adjustFlag(row) {
// 原单已调整 / 重推生成的新单(显示来源原单号)
if (row.adjustStatusName) return row.adjustStatusName;
if (row.originFormalSettlementNo) return '调整后单';
return '-';
},
lockTag(row) { lockTag(row) {
if (row.kingdeeLockStatus === 'locked') { if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' }; return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
@@ -414,6 +414,8 @@ export default {
}); });
this.details = []; this.details = [];
this.recalculate(); this.recalculate();
// 金蝶冲销预检:软提示,不阻断建单(金蝶不可用时静默降级)
this.precheckWriteOff(formalSettlementId);
this.loading = true; this.loading = true;
try { try {
const { data } = await api.getFormalDetails(formalSettlementId); const { data } = await api.getFormalDetails(formalSettlementId);
@@ -431,6 +433,19 @@ export default {
this.loading = false; this.loading = false;
} }
}, },
async precheckWriteOff(formalSettlementId) {
try {
const { data } = await api.kingdeeWriteOffPrecheck(formalSettlementId);
if (String(this.form.formalSettlementId) !== String(formalSettlementId)) return;
const result = data?.data || data || {};
if (!result.message) return;
if (result.syncStatus === 'synced' && result.writtenOff === false)
this.$message.warning(result.message);
else this.$message.info(result.message);
} catch (error) {
// 预检软提示:金蝶查询失败静默降级,不阻断建单
}
},
addFeeRow() { addFeeRow() {
this.details.push({ this.details.push({
formalSettlementDetailId: null, formalSettlementDetailId: null,
@@ -1278,13 +1278,10 @@ 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;
@@ -1293,7 +1290,6 @@ 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;
}); });
}, },
@@ -1340,11 +1336,6 @@ 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 ||
@@ -1703,11 +1694,8 @@ export default {
}, },
normalizeExternalDateTime(value) { normalizeExternalDateTime(value) {
if (!value) return null; if (!value) return null;
const text = String(value).trim(); const text = String(value);
if (!text) return null; return text.length === 10 ? `${text}T00:00:00` : text;
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(
+5 -1
View File
@@ -253,6 +253,10 @@ export default {
if (detailNo) this.openRouteDetail(detailNo); if (detailNo) this.openRouteDetail(detailNo);
}, },
}, },
created() {
// 标签机制按 fullPath 缓存同页多实例:仅当前路由实例应响应 detailNo 跳转,防止缓存旧实例也弹窗
this.ownFullPath = this.$route.fullPath;
},
mounted() { mounted() {
this.loadOrganizationOptions(); this.loadOrganizationOptions();
this.loadTable(); this.loadTable();
@@ -342,7 +346,7 @@ export default {
}, },
async openRouteDetail(detailNo = this.$route.query.detailNo) { async openRouteDetail(detailNo = this.$route.query.detailNo) {
const formalSettlementNo = String(detailNo || '').trim(); const formalSettlementNo = String(detailNo || '').trim();
if (!formalSettlementNo) return; if (!formalSettlementNo || this.ownFullPath !== this.$route.fullPath) return;
const response = await api.getList(1, 10, { formalSettlementNo }); const response = await api.getList(1, 10, { formalSettlementNo });
const data = this.unwrapData(response); const data = this.unwrapData(response);
const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo); const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo);
+49 -18
View File
@@ -88,16 +88,21 @@
><el-link ><el-link
v-if="column.link && row[column.prop]" v-if="column.link && row[column.prop]"
type="primary" type="primary"
@click="openView(row)" @click="handleTableLink(row, column)"
>{{ row[column.prop] }}</el-link >{{ row[column.prop] }}</el-link
><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">{{ ><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">{{
row[column.prop] || '-' row[column.prop] || '-'
}}</el-tag }}</el-tag
><el-tag
v-else-if="column.kingdee"
:type="kingdeeStatusType(row.kingdeeSyncStatus)"
class="status-text"
>{{ kingdeeSyncName(row.kingdeeSyncStatus) }}</el-tag
><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span ><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span
><span v-else>{{ displayValue(row[column.prop]) }}</span></template ><span v-else>{{ displayValue(row[column.prop]) }}</span></template
></el-table-column ></el-table-column
> >
<el-table-column label="操作" width="120" fixed="right" align="center" <el-table-column label="操作" width="320" fixed="right" align="center"
><template #default="{ row }" ><template #default="{ row }"
><div class="settlement-adjustment-page__links"> ><div class="settlement-adjustment-page__links">
<el-link <el-link
@@ -139,14 +144,10 @@
@click="handleReturn(row)" @click="handleReturn(row)"
>驳回</el-link >驳回</el-link
><el-link ><el-link
v-if=" v-if="hasPermission('settlement_adjustment_repush') && row.approvalStatus === 'approved'"
hasPermission('settlement_adjustment_repush') && :type="row.kingdeeSyncStatus === 'synced' ? 'danger' : 'primary'"
row.approvalStatus === 'approved' && @click="handleKingdee(row)"
row.kingdeeSyncStatus === 'synced' >{{ row.kingdeeSyncStatus === 'synced' ? '重新推送(高危)' : '推送金蝶' }}</el-link
"
type="danger"
@click="handleRepush(row)"
>重新推送(高危)</el-link
> >
</div></template </div></template
></el-table-column ></el-table-column
@@ -180,6 +181,7 @@ import * as api from '@/api/settlement/settlementAdjustment';
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch'; import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
import organizationSearch from '@/mixins/organization-search'; import organizationSearch from '@/mixins/organization-search';
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable'; import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
import { kingdeeSyncStatusOptions } from '@/option/settlement/formalSettlementSearch';
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue'; import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
import SettlementTableTools from './components/settlement-table-tools.vue'; import SettlementTableTools from './components/settlement-table-tools.vue';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
@@ -283,6 +285,21 @@ export default {
openView(row) { openView(row) {
this.editor = { visible: true, id: row.id, readonly: true }; this.editor = { visible: true, id: row.id, readonly: true };
}, },
// 单据号→本单查看弹窗;结算单号→跳正式结算列表页并自动打开该单详情(页面支持 ?detailNo=)
handleTableLink(row, column) {
if (column.prop === 'formalSettlementNo') {
this.openSettlementDetail(row.formalSettlementNo);
return;
}
this.openView(row);
},
openSettlementDetail(detailNo) {
if (!detailNo) {
this.$message.info('当前记录未关联结算单详情');
return;
}
this.$router.push({ path: '/settlement/formal-settlement', query: { detailNo } });
},
isEditable(row) { isEditable(row) {
return ['draft', 'returned'].includes(row.approvalStatus); return ['draft', 'returned'].includes(row.approvalStatus);
}, },
@@ -300,12 +317,10 @@ export default {
}, },
async handleApprove(row) { async handleApprove(row) {
await this.$confirm('审批通过后将更新关联正式结算单,确认继续?', '提示'); await this.$confirm('审批通过后将更新关联正式结算单,确认继续?', '提示');
await api.approve(row.id); const res = await api.approve(row.id);
if (row.kingdeeSyncStatus === 'synced') // data=false:自动同步金蝶失败;data=true 且 msg 带已推送场景的手工冲销提示
this.$message.warning( if (res.data.data === true || res.data.data === false) this.$message.warning(res.data.msg);
'调整已生效,本地已更新;原结算单已推送金蝶,请财务先手工冲销旧应付单后再重新推送。' else this.$message.success(res.data.msg || '审批通过');
);
else this.$message.success('审批通过');
this.loadTable(); this.loadTable();
}, },
async handleReturn(row) { async handleReturn(row) {
@@ -317,14 +332,24 @@ export default {
this.$message.success('已驳回'); this.$message.success('已驳回');
this.loadTable(); this.loadTable();
}, },
// 单一入口按状态分流:未同步/失败→推送原结算单;已同步→冲销后重推新单(高危)
async handleKingdee(row) {
if (row.kingdeeSyncStatus === 'synced') return this.handleRepush(row);
return this.handleSyncKingdee(row);
},
async handleSyncKingdee(row) {
const res = await api.syncKingdee(row.id);
this.$message.success(`已推送金蝶:${res.data.data || '-'}`);
this.loadTable();
},
async handleRepush(row) { async handleRepush(row) {
await this.$confirm( await this.$confirm(
'请确认金蝶旧应付单已由财务手工冲销,继续将生成全新财务应付单。', '请确认金蝶旧应付/应收单已由财务手工冲销,继续将生成全新财务应付/应收单。',
'高危操作确认', '高危操作确认',
{ type: 'warning', confirmButtonText: '确认重新推送' } { type: 'warning', confirmButtonText: '确认重新推送' }
); );
const { data } = await api.repush(row.id); const { data } = await api.repush(row.id);
this.$message.success(`已生成新金蝶应付单:${data?.data || data || '-'}`); this.$message.success(`已生成新金蝶单据:${data?.data || data || '-'}`);
this.loadTable(); this.loadTable();
}, },
statusType(status) { statusType(status) {
@@ -333,6 +358,12 @@ export default {
'info' 'info'
); );
}, },
kingdeeStatusType(status) {
return { synced: 'success', failed: 'danger' }[status] || 'info';
},
kingdeeSyncName(value) {
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
},
displayValue(value) { displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value; return value === null || value === undefined || value === '' ? '-' : value;
}, },
+34 -100
View File
@@ -153,12 +153,7 @@
/> />
<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 }} 当前阶段:{{ oaSyncStageLabel }},第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页
<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">
@@ -262,9 +257,6 @@ export default {
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '', dumpFilePath: '',
companyOaId: '',
companyIndex: 0,
companyTotal: 0,
}, },
parentId: 0, parentId: 0,
page: { page: {
@@ -298,9 +290,7 @@ export default {
{ {
label: '组织名称', label: '组织名称',
prop: 'deptName', prop: 'deptName',
minWidth: 320, minWidth: 160,
align: 'left',
headerAlign: 'left',
order: 80, order: 80,
search: true, search: true,
searchOrder: 2, searchOrder: 2,
@@ -652,18 +642,12 @@ export default {
} }
const totalPage = this.oaSyncTotalPage; const totalPage = this.oaSyncTotalPage;
const current = Number(this.oaSyncProgress.current) || 0; const current = Number(this.oaSyncProgress.current) || 0;
if (this.oaSyncStage === 'department') { const stageBase = this.oaSyncStage === 'department' ? 50 : 0;
const companyTotal = Math.max(1, Number(this.oaSyncProgress.companyTotal) || 1);
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));
}
if (totalPage <= 0) { if (totalPage <= 0) {
return 0; return stageBase;
} }
return Math.min(49, Math.round((current / totalPage) * 50)); const stagePercent = Math.min(50, Math.round((current / totalPage) * 50));
return Math.min(99, stageBase + stagePercent);
}, },
oaSyncProgressStatus() { oaSyncProgressStatus() {
if (this.oaSyncStatus === 'done') { if (this.oaSyncStatus === 'done') {
@@ -677,7 +661,25 @@ export default {
}, },
methods: { methods: {
handleOaOrgFullSync() { handleOaOrgFullSync() {
this.startOaOrgSync({ clearNonTop: false, incremental: false }); this.$confirm(
'全量同步将拉取 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(
@@ -712,9 +714,6 @@ export default {
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '', dumpFilePath: '',
companyOaId: '',
companyIndex: 0,
companyTotal: 0,
}; };
this.runOaOrgSyncPages(); this.runOaOrgSyncPages();
}, },
@@ -726,25 +725,6 @@ 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';
@@ -762,57 +742,20 @@ export default {
if (!modifiedSince && page.modifiedSince) { if (!modifiedSince && page.modifiedSince) {
modifiedSince = page.modifiedSince; modifiedSince = page.modifiedSince;
} }
this.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.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) {
@@ -832,10 +775,7 @@ export default {
this.oaSyncStage = 'department'; this.oaSyncStage = 'department';
this.oaSyncProgress.current = 0; this.oaSyncProgress.current = 0;
this.oaSyncProgress.total = 0; this.oaSyncProgress.total = 0;
this.oaSyncProgress.companyOaId = ''; await this.runOaOrgSyncStage(syncOaDepartment);
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;
@@ -1520,12 +1460,6 @@ 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);
+1 -1
View File
@@ -42,5 +42,5 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
</style> </style>
@@ -156,7 +156,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.business-panel { .business-panel {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -60,7 +60,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.finance-panel { .finance-panel {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -67,7 +67,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.fleet-panel { .fleet-panel {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.kpi-card { .kpi-card {
background: #ffffff; background: #ffffff;
+1 -1
View File
@@ -62,7 +62,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.project-panel { .project-panel {
margin-bottom: 16px; margin-bottom: 16px;
@@ -102,7 +102,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.structure-cards__item { .structure-cards__item {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -59,7 +59,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.summary-panel { .summary-panel {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -68,7 +68,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.vehicle-cards__item { .vehicle-cards__item {
margin-bottom: 16px; margin-bottom: 16px;
+1 -1
View File
@@ -41,7 +41,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../dashboard.scss'; @use '../dashboard.scss';
.wel-filter__range { .wel-filter__range {
width: 110px; width: 110px;