✨ 评分减分累计与最高扣分、用户部门标签联动树,并修复对账导入与结算调整等
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -380,7 +380,13 @@ export default {
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
const payload = data?.data ?? data;
|
||||
// 兼容误返回分页对象的情况,只取正式结算单列表
|
||||
this.candidates = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.records)
|
||||
? payload.records
|
||||
: [];
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const { data } = await api.getFeeOptions();
|
||||
|
||||
@@ -926,14 +926,34 @@ const stripInternalRowFields = row => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 合并费用列名与各行 feeItems 键,避免保存/回显时丢动态费用列 */
|
||||
const collectFeeItemNames = (preferred = [], detailRows = []) => {
|
||||
const names = [];
|
||||
const seen = new Set();
|
||||
const pushName = value => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
};
|
||||
(preferred || []).forEach(pushName);
|
||||
(detailRows || []).forEach(row => {
|
||||
const feeItems = row?.feeItems;
|
||||
if (!feeItems || typeof feeItems !== 'object' || Array.isArray(feeItems)) return;
|
||||
Object.keys(feeItems).forEach(pushName);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const { file, ...batch } = form;
|
||||
const payloadRows = rows.value.map(stripInternalRowFields);
|
||||
return {
|
||||
...batch,
|
||||
settlementBillType: form.settlementType === 'payable' ? form.settlementBillType || 'formal' : 'formal',
|
||||
waybillMode: form.waybillMode === 'history' ? 'history' : 'new',
|
||||
feeItemNames: [...feeItemNames.value],
|
||||
rows: rows.value.map(stripInternalRowFields),
|
||||
feeItemNames: collectFeeItemNames(feeItemNames.value, payloadRows),
|
||||
rows: payloadRows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1436,9 +1456,13 @@ const loadDetail = async id => {
|
||||
if (detail.projectId) {
|
||||
await loadContractsByProject(detail.projectId, detail.projectName || '');
|
||||
}
|
||||
feeItemNames.value = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
|
||||
parsedHeaders.value = [];
|
||||
const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
|
||||
// 先按库中列名与明细 feeItems 合并,再规范化行数据,保证草稿再打开仍展示全部费用列
|
||||
feeItemNames.value = collectFeeItemNames(
|
||||
Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [],
|
||||
detailRows
|
||||
);
|
||||
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
|
||||
refreshDuplicateFlags();
|
||||
// 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
|
||||
|
||||
@@ -125,15 +125,17 @@
|
||||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
<div class="reconciliation-editor__filter-actions">
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -189,6 +191,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -318,6 +323,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -476,6 +484,7 @@
|
||||
<span v-else-if="column.feeItemName">{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span v-else-if="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{ formatOptionalMoney(row.unitPrice) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -630,7 +639,10 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isExcelResponse, parseBlobJson } from '@/utils/import-excel';
|
||||
import {
|
||||
consumeImportResponse,
|
||||
resolveImportErrorMessage,
|
||||
} from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -1042,12 +1054,24 @@ export default {
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
blankOptionalNumber(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
};
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
@@ -1057,6 +1081,9 @@ export default {
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
@@ -1284,6 +1311,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
@@ -1297,6 +1327,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
|
||||
return snapshot;
|
||||
});
|
||||
@@ -1326,7 +1359,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||||
await this.$confirm('将以外部账单数据更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
@@ -1508,22 +1541,23 @@ export default {
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
if (isExcelResponse(response)) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
const consumed = await consumeImportResponse(response, {
|
||||
businessName: '运输对账',
|
||||
dayjsFormat: this.$dayjs().format('YYYY-MM-DD HH-mm-ss'),
|
||||
});
|
||||
if (consumed.kind === 'excel') {
|
||||
downloadXls(consumed.blob, consumed.fileName);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(response.data);
|
||||
if (Number(result.code) !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = false;
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
if (!error?._notified) {
|
||||
this.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
@@ -1831,7 +1865,10 @@ export default {
|
||||
: Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatMileage(value) {
|
||||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||||
return this.blankOptionalNumber(value) === null ? '' : this.displayValue(value);
|
||||
},
|
||||
formatOptionalMoney(value, currency = 'RMB') {
|
||||
return this.blankOptionalNumber(value) === null ? '-' : this.formatMoney(value, currency);
|
||||
},
|
||||
formatDate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
@@ -1952,15 +1989,26 @@ export default {
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-actions .el-button {
|
||||
min-width: 64px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -6,15 +6,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance, nextTick, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.query.mode === 'view') return '查看导入结算单';
|
||||
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
|
||||
});
|
||||
|
||||
const syncTagTitle = () => {
|
||||
nextTick(() => {
|
||||
store.commit('SET_TAG', {
|
||||
fullPath: route.fullPath,
|
||||
name: pageTitle.value,
|
||||
});
|
||||
instance?.proxy?.$router?.$avueRouter?.setTitle?.(pageTitle.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncTagTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -239,19 +239,22 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { name: '新增导入结算单' },
|
||||
});
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!row?.id) return;
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id, mode: 'view' },
|
||||
query: { id: row.id, mode: 'view', name: '查看导入结算单' },
|
||||
});
|
||||
},
|
||||
editDraft(row) {
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id },
|
||||
query: { id: row.id, name: '编辑导入结算单' },
|
||||
});
|
||||
},
|
||||
async handleDelete(row) {
|
||||
|
||||
Reference in New Issue
Block a user