1、新增结算模块
2、调整业务模块 3、调整客商模块
This commit is contained in:
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="96%" top="2vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="formal-editor">
|
||||
<section class="formal-editor__section">
|
||||
<div class="dialog-section-title">结算基本信息</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in fields" :key="field.prop" :span="8">
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'date' && editable"
|
||||
v-model="form[field.prop]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'contract' && editable"
|
||||
v-model="form.contractId"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="loadContracts"
|
||||
placeholder="请选择合同"
|
||||
@change="handleContractChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="`${item.contractNo} / ${item.contractName}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number' && editable"
|
||||
v-model="form[field.prop]"
|
||||
:min="0.000001"
|
||||
:precision="6"
|
||||
:controls="false"
|
||||
/>
|
||||
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
||||
<span v-else-if="field.type === 'contract'">{{
|
||||
displayValue(form.contractName)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="formal-editor__section">
|
||||
<div class="formal-editor__section-head">
|
||||
<div class="dialog-section-title">来源预结算单</div>
|
||||
<div class="formal-editor__actions">
|
||||
<el-button v-if="editable" type="primary" plain @click="openCandidateDialog"
|
||||
>选择预结算单</el-button
|
||||
>
|
||||
<el-button v-if="editable" type="primary" plain @click="openDetailDialog"
|
||||
>选择结算明细</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="sources" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in sourceTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||||
<template #default="{ $index }"
|
||||
><el-link type="danger" @click="removeSource($index)">删除</el-link></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section v-if="details.length" class="formal-editor__section">
|
||||
<div class="dialog-section-title">结算明细</div>
|
||||
<el-table :data="details" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in detailTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="formal-editor__links">
|
||||
<el-link v-if="row.formalSettlementId" type="primary" @click="openAdjustDialog(row)"
|
||||
>调整</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="!row.sourcePreSettlementId"
|
||||
type="danger"
|
||||
@click="removeDetail($index)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="formal-editor__section">
|
||||
<div class="dialog-section-title">附件</div>
|
||||
<vehicle-attachment-upload
|
||||
v-model="attachments"
|
||||
:readonly="!editable"
|
||||
:multiple="true"
|
||||
:limit="20"
|
||||
:max-size="500"
|
||||
:file-types="attachmentFileTypes"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="readonly && payments.length" class="formal-editor__section">
|
||||
<div class="dialog-section-title">付款信息</div>
|
||||
<el-table :data="payments" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in paymentTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'paymentTypeName'">{{
|
||||
row.paymentType === 'final' ? '尾款' : displayValue(row.paymentType)
|
||||
}}</span>
|
||||
<span v-else-if="column.prop === 'billStatusName'">{{
|
||||
row.billStatus === 'reviewing' ? '审批中' : displayValue(row.billStatus)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||||
>提交</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
||||
<el-form :model="candidate.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="预结算单号"
|
||||
><el-input v-model="candidate.query.preSettlementNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="合同编号"
|
||||
><el-input v-model="candidate.query.contractNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-button @click="resetCandidates">重置</el-button
|
||||
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
<el-table
|
||||
ref="candidateTable"
|
||||
v-loading="candidate.loading"
|
||||
:data="candidate.rows"
|
||||
border
|
||||
@selection-change="candidate.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in candidateTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }"
|
||||
><span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="candidate.page.current"
|
||||
v-model:page-size="candidate.page.size"
|
||||
:total="candidate.page.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadCandidates"
|
||||
@size-change="loadCandidates"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="candidate.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmCandidates">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailCandidate.visible" title="选择结算明细" width="92%" append-to-body>
|
||||
<el-form :model="detailCandidate.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="批次号"
|
||||
><el-input v-model="detailCandidate.query.batchNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="费用日期"
|
||||
><el-date-picker
|
||||
v-model="detailCandidate.query.feeDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-button @click="resetDetailCandidates">重置</el-button
|
||||
><el-button type="primary" @click="loadDetailCandidates">查询</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
<el-table
|
||||
v-loading="detailCandidate.loading"
|
||||
:data="detailCandidate.rows"
|
||||
border
|
||||
@selection-change="detailCandidate.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in candidateDetailTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }"
|
||||
><span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="detailCandidate.page.current"
|
||||
v-model:page-size="detailCandidate.page.size"
|
||||
:total="detailCandidate.page.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadDetailCandidates"
|
||||
@size-change="loadDetailCandidates"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="detailCandidate.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmDetailCandidates">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="adjust.visible" title="结算明细调整" width="92%" append-to-body>
|
||||
<el-table v-loading="adjust.loading" :data="adjust.rows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="130" align="center" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
|
||||
<el-table-column label="运输总量" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:precision="6"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="里程(KM)" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.mileage"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运输单价" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.unitPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运费" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.freightAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="结算金额(含税)" min-width="165" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.settlementAmountTax"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="结算金额(不含税)" min-width="180" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.settlementAmountNoTax"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="备注" min-width="180" align="center"
|
||||
><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
<el-form label-position="right" label-width="auto" class="formal-editor__adjust-reason">
|
||||
<el-form-item label="调整原因" required
|
||||
><el-input v-model="adjust.reason" type="textarea" maxlength="200" show-word-limit
|
||||
/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer
|
||||
><el-button @click="adjust.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="adjust.saving" @click="saveAdjustment"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
adjustDetail,
|
||||
getCandidateDetails,
|
||||
getCandidates,
|
||||
getContractOptions,
|
||||
getDetail,
|
||||
getDetailFees,
|
||||
save,
|
||||
} from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
createFormalSettlementForm,
|
||||
formalSettlementFormFields,
|
||||
} from '@/option/settlement/formalSettlementForm';
|
||||
import {
|
||||
candidateColumns,
|
||||
candidateDetailColumns,
|
||||
detailColumns,
|
||||
paymentColumns,
|
||||
sourceColumns,
|
||||
} from '@/option/settlement/formalSettlementTable';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementEditor',
|
||||
props: { modelValue: Boolean, recordId: [String, Number], readonly: Boolean },
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
form: createFormalSettlementForm(),
|
||||
sources: [],
|
||||
details: [],
|
||||
payments: [],
|
||||
attachments: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
'jpeg',
|
||||
'png',
|
||||
'jpg',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'eml',
|
||||
'msg',
|
||||
'zip',
|
||||
'rar',
|
||||
],
|
||||
contracts: [],
|
||||
fields: formalSettlementFormFields,
|
||||
sourceTableColumns: sourceColumns,
|
||||
detailTableColumns: detailColumns,
|
||||
paymentTableColumns: paymentColumns,
|
||||
candidateTableColumns: candidateColumns,
|
||||
candidateDetailTableColumns: candidateDetailColumns,
|
||||
rules: {
|
||||
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
|
||||
exchangeRateDate: [{ required: true, message: '请选择汇率日期', trigger: 'change' }],
|
||||
exchangeRate: [{ required: true, message: '请输入结算汇率', trigger: 'blur' }],
|
||||
},
|
||||
candidate: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
query: {},
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
detailCandidate: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
query: {},
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjust: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
saving: false,
|
||||
detailId: null,
|
||||
reason: '',
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.form = createFormalSettlementForm();
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.payments = [];
|
||||
this.attachments = [];
|
||||
await this.loadContracts();
|
||||
if (!this.recordId) {
|
||||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await getDetail(this.recordId);
|
||||
const data = this.unwrapData(response);
|
||||
this.form = {
|
||||
...createFormalSettlementForm(),
|
||||
...data,
|
||||
sourcePreSettlementIds: (data.sources || []).map(item => item.preSettlementId),
|
||||
sourceDetailIds: (data.details || [])
|
||||
.filter(item => !item.sourcePreSettlementId)
|
||||
.map(item => item.sourceDetailId),
|
||||
};
|
||||
this.sources = data.sources || [];
|
||||
this.details = data.details || [];
|
||||
this.payments = data.payments || [];
|
||||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadContracts(keyword = '') {
|
||||
const response = await getContractOptions(keyword);
|
||||
this.contracts = this.unwrapData(response) || [];
|
||||
},
|
||||
handleContractChange(id) {
|
||||
const contract = this.contracts.find(item => String(item.id) === String(id));
|
||||
if (!contract) return;
|
||||
Object.assign(this.form, contract, {
|
||||
contractId: contract.id,
|
||||
settlementTypeName: contract.settlementType === 'receivable' ? '应收' : '应付',
|
||||
});
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.form.sourcePreSettlementIds = [];
|
||||
this.form.sourceDetailIds = [];
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
this.candidate.page.current = 1;
|
||||
this.loadCandidates();
|
||||
},
|
||||
async loadCandidates() {
|
||||
this.candidate.loading = true;
|
||||
try {
|
||||
const response = await getCandidates(
|
||||
this.candidate.page.current,
|
||||
this.candidate.page.size,
|
||||
{
|
||||
...this.candidate.query,
|
||||
contractId: this.form.contractId || undefined,
|
||||
}
|
||||
);
|
||||
const data = this.unwrapData(response);
|
||||
this.candidate.rows = data.records || [];
|
||||
this.candidate.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.candidate.loading = false;
|
||||
}
|
||||
},
|
||||
resetCandidates() {
|
||||
this.candidate.query = {};
|
||||
this.candidate.page.current = 1;
|
||||
this.loadCandidates();
|
||||
},
|
||||
confirmCandidates() {
|
||||
const merged = [...this.sources, ...this.candidate.selected];
|
||||
const map = new Map(
|
||||
merged.map(item => [
|
||||
String(item.preSettlementId || item.id),
|
||||
{ ...item, preSettlementId: item.preSettlementId || item.id },
|
||||
])
|
||||
);
|
||||
this.sources = [...map.values()];
|
||||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId);
|
||||
if (this.sources.length) {
|
||||
Object.assign(this.form, this.sources[0], {
|
||||
contractId: this.sources[0].contractId,
|
||||
formalSettlementNo: this.form.formalSettlementNo,
|
||||
});
|
||||
}
|
||||
this.form.settlementAmount = this.sources.reduce(
|
||||
(sum, item) => sum + Number(item.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.localSettlementAmount =
|
||||
this.form.settlementAmount * Number(this.form.exchangeRate || 1);
|
||||
this.candidate.visible = false;
|
||||
},
|
||||
removeSource(index) {
|
||||
this.sources.splice(index, 1);
|
||||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId || item.id);
|
||||
},
|
||||
openDetailDialog() {
|
||||
if (!this.form.contractId) return this.$message.warning('请先选择合同');
|
||||
this.detailCandidate.visible = true;
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
async loadDetailCandidates() {
|
||||
this.detailCandidate.loading = true;
|
||||
try {
|
||||
const range = this.detailCandidate.query.feeDateRange || [];
|
||||
const params = {
|
||||
contractId: this.form.contractId,
|
||||
settlementType: this.form.settlementType,
|
||||
batchNo: this.detailCandidate.query.batchNo,
|
||||
feeStartDate: range[0],
|
||||
feeEndDate: range[1],
|
||||
};
|
||||
const response = await getCandidateDetails(
|
||||
this.detailCandidate.page.current,
|
||||
this.detailCandidate.page.size,
|
||||
params
|
||||
);
|
||||
const data = this.unwrapData(response);
|
||||
this.detailCandidate.rows = data.records || [];
|
||||
this.detailCandidate.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.detailCandidate.loading = false;
|
||||
}
|
||||
},
|
||||
resetDetailCandidates() {
|
||||
this.detailCandidate.query = {};
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
confirmDetailCandidates() {
|
||||
const existing = new Map(
|
||||
this.details
|
||||
.filter(item => !item.formalSettlementId)
|
||||
.map(item => [String(item.sourceDetailId || item.id), item])
|
||||
);
|
||||
this.detailCandidate.selected.forEach(item =>
|
||||
existing.set(String(item.id), {
|
||||
...item,
|
||||
sourceDetailId: item.id,
|
||||
settlementAmountTax: item.totalAmount,
|
||||
})
|
||||
);
|
||||
this.details = [
|
||||
...this.details.filter(item => item.formalSettlementId),
|
||||
...existing.values(),
|
||||
];
|
||||
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
||||
this.form.settlementAmount =
|
||||
this.sources.reduce((sum, item) => sum + Number(item.settlementAmount || 0), 0) +
|
||||
[...existing.values()].reduce((sum, item) => sum + Number(item.totalAmount || 0), 0);
|
||||
this.detailCandidate.visible = false;
|
||||
},
|
||||
removeDetail(index) {
|
||||
this.details.splice(index, 1);
|
||||
this.form.sourceDetailIds = this.details
|
||||
.filter(item => !item.sourcePreSettlementId)
|
||||
.map(item => item.sourceDetailId);
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.adjust = {
|
||||
visible: true,
|
||||
loading: true,
|
||||
saving: false,
|
||||
detailId: row.id,
|
||||
reason: '',
|
||||
rows: [],
|
||||
};
|
||||
try {
|
||||
const response = await getDetailFees(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.adjust.rows = (data || []).map(item => ({
|
||||
...item,
|
||||
feeItems: this.parseFeeItems(item.feeItemsJson),
|
||||
}));
|
||||
} finally {
|
||||
this.adjust.loading = false;
|
||||
}
|
||||
},
|
||||
async saveAdjustment() {
|
||||
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
this.adjust.saving = true;
|
||||
try {
|
||||
await adjustDetail({
|
||||
detailId: this.adjust.detailId,
|
||||
changeReason: this.adjust.reason,
|
||||
rows: this.adjust.rows,
|
||||
});
|
||||
this.$message.success('调整保存成功');
|
||||
this.adjust.visible = false;
|
||||
await this.initialize();
|
||||
} finally {
|
||||
this.adjust.saving = false;
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
|
||||
return this.$message.warning('请选择预结算单或结算明细');
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
await save({
|
||||
id: this.form.id,
|
||||
contractId: this.form.contractId,
|
||||
settlementType: this.form.settlementType,
|
||||
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
|
||||
sourceDetailIds: this.form.sourceDetailIds,
|
||||
exchangeRateDate: this.form.exchangeRateDate,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||||
remark: this.form.remark,
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return `${Number(value || 0).toFixed(2)} RMB`;
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object') return value;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
parseAttachments(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.formal-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.formal-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.formal-editor__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.formal-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.formal-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.formal-editor__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formal-editor__adjust-reason {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="94%" top="3vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="settlement-adjustment-editor">
|
||||
<section class="settlement-adjustment-editor__section">
|
||||
<div class="dialog-section-title">基本信息</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in fields" :key="field.prop" :span="8"
|
||||
><el-form-item :label="field.label" :prop="field.prop"
|
||||
><el-select
|
||||
v-if="field.prop === 'formalSettlementId' && editable"
|
||||
v-model="form.formalSettlementId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="loadFormalSettlements"
|
||||
placeholder="请选择正式结算单"
|
||||
@change="handleFormalChange"
|
||||
><el-option
|
||||
v-for="item in candidates"
|
||||
:key="item.id"
|
||||
:label="`${item.formalSettlementNo} / ${item.projectName || ''}`"
|
||||
:value="item.id" /></el-select
|
||||
><el-input
|
||||
v-else-if="field.prop === 'adjustmentNo'"
|
||||
v-model="form[field.prop]"
|
||||
disabled
|
||||
/><el-input-number
|
||||
v-else-if="field.money && editable"
|
||||
v-model="form[field.prop]"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
disabled
|
||||
/><span v-else>{{
|
||||
displayValue(
|
||||
field.prop === 'settlementTypeName' ? settlementTypeName : form[field.prop]
|
||||
)
|
||||
}}</span></el-form-item
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="24"
|
||||
><el-form-item label="备注" prop="remark"
|
||||
><el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/><span v-else>{{ displayValue(form.remark) }}</span></el-form-item
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="settlement-adjustment-editor__section">
|
||||
<div class="settlement-adjustment-editor__section-head">
|
||||
<div class="dialog-section-title">调整费用</div>
|
||||
<el-button
|
||||
v-if="editable && form.formalSettlementId"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openFeeDialog"
|
||||
>添加费用</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table :data="details" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" /><el-table-column
|
||||
prop="feeType"
|
||||
label="费用类型"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeItem"
|
||||
label="费用项目"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/><el-table-column
|
||||
prop="originalAmountTax"
|
||||
label="原金额(含税)"
|
||||
min-width="145"
|
||||
align="center"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.originalAmountTax)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="调整金额(含税)" min-width="160" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-if="editable"
|
||||
v-model="row.adjustmentAmountTax"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@change="recalculate"
|
||||
/><span v-else :class="{ negative: Number(row.adjustmentAmountTax) < 0 }">{{
|
||||
formatMoney(row.adjustmentAmountTax)
|
||||
}}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="调整金额(不含税)" min-width="170" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-if="editable"
|
||||
v-model="row.adjustmentAmountNoTax"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/><span v-else>{{ formatMoney(row.adjustmentAmountNoTax) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="备注" min-width="180" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input v-if="editable" v-model="row.remark" maxlength="200" /><span v-else>{{
|
||||
displayValue(row.remark)
|
||||
}}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center"
|
||||
><template #default="{ $index }"
|
||||
><el-link
|
||||
type="danger"
|
||||
@click="
|
||||
details.splice($index, 1);
|
||||
recalculate();
|
||||
"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
<template #empty><el-empty description="请选择调整费用明细" /></template>
|
||||
</el-table>
|
||||
<div class="settlement-adjustment-editor__summary">
|
||||
调整金额合计:<strong :class="{ negative: Number(form.adjustmentAmount) < 0 }">{{
|
||||
formatMoney(form.adjustmentAmount)
|
||||
}}</strong
|
||||
>;调整后结算金额:<strong>{{ formatMoney(form.adjustedSettlementAmount) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="visible = false">取消</el-button
|
||||
><el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
|
||||
<el-dialog v-model="feeDialog.visible" title="选择调整费用" width="88%" append-to-body>
|
||||
<el-table
|
||||
ref="feeTable"
|
||||
:data="feeRows"
|
||||
border
|
||||
@selection-change="feeDialog.selected = $event"
|
||||
><el-table-column type="selection" width="52" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="64"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="documentNo"
|
||||
label="单据号"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeType"
|
||||
label="费用类型"
|
||||
min-width="140"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeItem"
|
||||
label="费用项目"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/><el-table-column
|
||||
prop="originalAmountTax"
|
||||
label="原金额(含税)"
|
||||
min-width="145"
|
||||
align="center"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.originalAmountTax)
|
||||
}}</template></el-table-column
|
||||
></el-table
|
||||
>
|
||||
<template #footer
|
||||
><el-button @click="feeDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmFees">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/settlement/settlementAdjustment';
|
||||
import {
|
||||
createSettlementAdjustmentForm,
|
||||
settlementAdjustmentFormFields,
|
||||
} from '@/option/settlement/settlementAdjustmentForm';
|
||||
export default {
|
||||
name: 'SettlementAdjustmentEditor',
|
||||
props: { modelValue: Boolean, recordId: [String, Number], readonly: Boolean },
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data: () => ({
|
||||
loading: false,
|
||||
saving: false,
|
||||
form: createSettlementAdjustmentForm(),
|
||||
fields: settlementAdjustmentFormFields,
|
||||
details: [],
|
||||
candidates: [],
|
||||
feeRows: [],
|
||||
rules: {
|
||||
formalSettlementId: [{ required: true, message: '请选择关联正式结算单', trigger: 'change' }],
|
||||
},
|
||||
feeDialog: { visible: false, selected: [] },
|
||||
}),
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看结算调整单' : this.recordId ? '编辑结算调整单' : '新增结算调整单';
|
||||
},
|
||||
settlementTypeName() {
|
||||
return this.form.settlementType === 'receivable'
|
||||
? '应收'
|
||||
: this.form.settlementType === 'payable'
|
||||
? '应付'
|
||||
: '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.form = createSettlementAdjustmentForm();
|
||||
this.details = [];
|
||||
this.candidates = [];
|
||||
this.feeRows = [];
|
||||
if (!this.recordId) {
|
||||
await this.loadFormalSettlements('');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await api.getDetail(this.recordId);
|
||||
this.form = { ...createSettlementAdjustmentForm(), ...data };
|
||||
this.details = (data.details || []).map(item => ({
|
||||
...item,
|
||||
adjustmentAmountTax: Number(item.adjustmentAmountTax || 0),
|
||||
adjustmentAmountNoTax:
|
||||
item.adjustmentAmountNoTax == null ? null : Number(item.adjustmentAmountNoTax),
|
||||
}));
|
||||
await this.loadFormalSettlements(this.form.formalSettlementNo || '');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
},
|
||||
async handleFormalChange(id) {
|
||||
const item = this.candidates.find(row => String(row.id) === String(id));
|
||||
if (!item) return;
|
||||
Object.assign(this.form, item, {
|
||||
formalSettlementId: id,
|
||||
formalSettlementNo: item.formalSettlementNo,
|
||||
originalSettlementAmount: Number(item.settlementAmount || 0),
|
||||
settlementTypeName: item.settlementTypeName,
|
||||
});
|
||||
this.details = [];
|
||||
this.recalculate();
|
||||
},
|
||||
async openFeeDialog() {
|
||||
const { data } = await api.getFormalDetails(this.form.formalSettlementId);
|
||||
const used = new Set(this.details.map(item => String(item.formalSettlementDetailFeeId)));
|
||||
this.feeRows = (data?.data || data || []).filter(
|
||||
item => !used.has(String(item.formalSettlementDetailFeeId))
|
||||
);
|
||||
this.feeDialog = { visible: true, selected: [] };
|
||||
},
|
||||
confirmFees() {
|
||||
this.feeDialog.selected.forEach(item =>
|
||||
this.details.push({
|
||||
...item,
|
||||
adjustmentAmountTax: 0,
|
||||
adjustmentAmountNoTax: null,
|
||||
remark: '',
|
||||
})
|
||||
);
|
||||
this.recalculate();
|
||||
this.feeDialog.visible = false;
|
||||
},
|
||||
recalculate() {
|
||||
const total = this.details.reduce(
|
||||
(sum, row) => sum + Number(row.adjustmentAmountTax || 0),
|
||||
0
|
||||
);
|
||||
this.form.adjustmentAmount = Number(total.toFixed(2));
|
||||
this.form.adjustedSettlementAmount = Number(
|
||||
(Number(this.form.originalSettlementAmount || 0) + total).toFixed(2)
|
||||
);
|
||||
},
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.details.length) return this.$message.warning('请至少添加一条调整费用');
|
||||
this.saving = true;
|
||||
try {
|
||||
await api.save({
|
||||
id: this.form.id,
|
||||
formalSettlementId: this.form.formalSettlementId,
|
||||
remark: this.form.remark,
|
||||
details: this.details.map(item => ({
|
||||
formalSettlementDetailId: item.formalSettlementDetailId,
|
||||
formalSettlementDetailFeeId: item.formalSettlementDetailFeeId,
|
||||
feeType: item.feeType,
|
||||
feeItem: item.feeItem,
|
||||
adjustmentAmountTax: item.adjustmentAmountTax,
|
||||
adjustmentAmountNoTax: item.adjustmentAmountNoTax,
|
||||
remark: item.remark,
|
||||
})),
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settlement-adjustment-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.settlement-adjustment-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.settlement-adjustment-editor__summary {
|
||||
padding: 12px 0;
|
||||
text-align: right;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.dialog-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog-section-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,832 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="98%" top="2vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="reconciliation-editor">
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="dialog-section-title">导入外部账单,与内部账单核对</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in formFields" :key="field.prop" :span="8">
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-input
|
||||
v-if="field.prop === 'formalSettlementNo' && editable"
|
||||
v-model="form.formalSettlementNo"
|
||||
readonly
|
||||
placeholder="请选择正式结算单"
|
||||
@click="openFormalDialog"
|
||||
>
|
||||
<template #append><el-button @click="openFormalDialog">选择</el-button></template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-else-if="field.prop === 'reconciliationMode' && editable"
|
||||
v-model="form.reconciliationMode"
|
||||
>
|
||||
<el-option label="整车总额对账" value="vehicle" />
|
||||
<el-option label="货物明细对账" value="cargo" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-else-if="field.type === 'date' && editable"
|
||||
v-model="form.reconciliationDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="field.money"
|
||||
v-model="form[field.prop]"
|
||||
:controls="false"
|
||||
:precision="2"
|
||||
disabled
|
||||
/>
|
||||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="dialog-section-title">内部账单</div>
|
||||
<div class="reconciliation-editor__stats">
|
||||
<div class="stat-card">
|
||||
<b>{{ form.internalBillCount || 0 }}</b
|
||||
><span>我方账单数</span
|
||||
><small
|
||||
>对方 {{ form.externalBillCount || 0 }} 条 / 差异
|
||||
{{ form.differenceCount || 0 }} 条</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<b>{{ formatNumber(form.internalQuantity) }}</b
|
||||
><span>我方货量</span
|
||||
><small
|
||||
>对方 {{ formatNumber(form.externalQuantity) }} / 差异
|
||||
{{ formatNumber(form.differenceQuantity) }}</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<b>{{ formatMoney(form.internalAmount) }}</b
|
||||
><span>我方结算金额</span
|
||||
><small
|
||||
>对方 {{ formatMoney(form.externalAmount) }} / 差异
|
||||
{{ formatMoney(form.differenceAmount) }}</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card stat-card--success">
|
||||
<b>{{ form.matchedCount || 0 }}</b
|
||||
><span>成功匹配(条)</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--danger">
|
||||
<b>{{ form.unmatchedCount || 0 }}</b
|
||||
><span>无法匹配(条)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter">
|
||||
<el-input v-model="internalQuery.documentNo" placeholder="单据号" clearable />
|
||||
<el-input v-model="internalQuery.vehicleNo" placeholder="车号" clearable />
|
||||
<el-input v-model="internalQuery.batchNo" placeholder="批次号" clearable />
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="货物名称" clearable />
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredInternalRows" border max-height="420">
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in internalColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="column.prop === 'matchResult'" :type="matchTagType(row.matchResult)">{{
|
||||
matchName(row.matchResult)
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-else-if="column.prop === 'updateResult'"
|
||||
:type="updateTagType(row.updateResult)"
|
||||
>{{ updateName(row.updateResult) }}</el-tag
|
||||
>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="reconciliation-editor__links">
|
||||
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult === 'matched'"
|
||||
type="primary"
|
||||
@click="handleUnmatch(row)"
|
||||
>取消匹配</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatch(row)"
|
||||
>人工匹配</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="reconciliation-editor__section-head">
|
||||
<div class="dialog-section-title">导入外部账单</div>
|
||||
<div class="reconciliation-editor__actions" v-if="editable">
|
||||
<el-button type="primary" plain @click="downloadTemplate('vehicle')"
|
||||
>下载整车对账模板</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="downloadTemplate('cargo')"
|
||||
>下载货物明细对账模板</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="chooseImport">导入</el-button>
|
||||
<el-button type="primary" @click="handleMatch">开始匹配内部账单</el-button>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
class="hidden-file"
|
||||
@change="handleImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="externalTab">
|
||||
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
||||
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
||||
</el-tabs>
|
||||
<el-table :data="visibleExternalRows" border max-height="420">
|
||||
<el-table-column
|
||||
v-for="column in externalColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)">{{
|
||||
matchName(row.matchStatus)
|
||||
}}</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><el-link
|
||||
v-if="editable && row.matchStatus !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatchByExternal(row)"
|
||||
>匹配</el-link
|
||||
></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<el-collapse>
|
||||
<el-collapse-item title="操作说明" name="help">
|
||||
<div class="reconciliation-editor__help">
|
||||
<p>1. 新增对账单时选择一张已审批通过的正式结算单,系统自动带出内部账单明细。</p>
|
||||
<p>
|
||||
2. 导入对方 Excel
|
||||
账单后点击“开始匹配内部账单”,系统按车号、货物、地址、批次、发货时间、运输量和金额匹配。
|
||||
</p>
|
||||
<p>3. 外部账单存在重复候选时会标记为疑似重复,需要人工选择唯一明细匹配。</p>
|
||||
<p>4. 只有所有内外部明细一一匹配且差异为 0,才允许按匹配结果更新账单或完成对账。</p>
|
||||
<p>
|
||||
5.
|
||||
已付金额大于外部匹配金额时禁止更新;整车模式一车多货会跳过自动更新,可通过“调整”逐货物修改。
|
||||
</p>
|
||||
<p>6. 按匹配结果更新成功后生成变更记录;完成对账后单据不可再编辑和删除。</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<template v-if="editable">
|
||||
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
||||
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate"
|
||||
>按匹配结果更新账单</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="actionLoading" @click="handleComplete"
|
||||
>完成对账</el-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
||||
<el-form :model="formalDialog.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="正式结算单号"
|
||||
><el-input v-model="formalDialog.query.keyword" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item><el-button @click="loadFormalOptions">查询</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
v-loading="formalDialog.loading"
|
||||
:data="formalDialog.rows"
|
||||
border
|
||||
@selection-change="formalDialog.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" :selectable="row => !row.isSelected" />
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column prop="formalSettlementNo" label="正式结算单号" min-width="150" />
|
||||
<el-table-column prop="contractNo" label="合同编号" min-width="130" />
|
||||
<el-table-column prop="contractName" label="合同名称" min-width="160" />
|
||||
<el-table-column prop="payerName" label="付款方" min-width="150" />
|
||||
<el-table-column prop="payeeName" label="收款方" min-width="150" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.settlementAmount, row.currency)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="reconciliation-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="formalDialog.page.current"
|
||||
v-model:page-size="formalDialog.page.size"
|
||||
:total="formalDialog.page.total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadFormalOptions"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="formalDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmFormal">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="adjustDialog.visible" title="结算明细调整" width="90%" append-to-body>
|
||||
<el-table :data="adjustDialog.rows" border>
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="120" />
|
||||
<el-table-column prop="transportQuantity" label="运输量" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="unitPrice" label="运输单价" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.unitPrice" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="freightAmount" label="运输费" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.freightAmount" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="140"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.settlementAmount" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
<el-form class="dialog-form" label-position="right" label-width="auto"
|
||||
><el-form-item label="调整原因" required
|
||||
><el-input
|
||||
v-model="adjustDialog.reason"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit /></el-form-item
|
||||
></el-form>
|
||||
<template #footer
|
||||
><el-button @click="adjustDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="adjustDialog.saving" @click="saveAdjust"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="manualDialog.visible" title="选择外部账单明细" width="86%" append-to-body>
|
||||
<el-table :data="unmatchedExternalRows" border @row-click="manualDialog.selected = $event">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column prop="externalLineNo" label="外部行号" width="90" />
|
||||
<el-table-column prop="vehicleNo" label="车牌号" width="110" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="transportQuantity" label="运输量" width="110" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" width="130"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.settlementAmount)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="选择" width="80"
|
||||
><template #default="{ row }"
|
||||
><el-radio v-model="manualDialog.selected" :label="row"> </el-radio></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<template #footer
|
||||
><el-button @click="manualDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmManualMatch">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
externalCargoColumns,
|
||||
externalVehicleColumns,
|
||||
} from '@/option/settlement/transportReconciliationTable';
|
||||
|
||||
export default {
|
||||
name: 'TransportReconciliationEditor',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
recordId: [String, Number],
|
||||
settlementType: { type: String, default: 'payable' },
|
||||
readonly: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
actionLoading: false,
|
||||
currentId: null,
|
||||
form: this.emptyForm(),
|
||||
formFields: transportReconciliationFormFields,
|
||||
internalColumns,
|
||||
internalDetails: [],
|
||||
externalDetails: [],
|
||||
internalQuery: { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' },
|
||||
internalFilterTick: 0,
|
||||
externalTab: 'all',
|
||||
formalDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
rows: [],
|
||||
selected: [],
|
||||
query: { keyword: '' },
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||
manualDialog: { visible: false, internal: null, selected: null },
|
||||
rules: {
|
||||
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
||||
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
||||
remark: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly
|
||||
? '查看运输对账单'
|
||||
: this.currentId
|
||||
? '编辑运输对账单'
|
||||
: '新增运输对账单';
|
||||
},
|
||||
filteredInternalRows() {
|
||||
void this.internalFilterTick;
|
||||
const query = this.internalQuery;
|
||||
return this.internalDetails.filter(
|
||||
row =>
|
||||
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
||||
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
||||
(!query.batchNo || String(row.batchNo || '').includes(query.batchNo)) &&
|
||||
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
||||
);
|
||||
},
|
||||
externalColumns() {
|
||||
return this.form.reconciliationMode === 'cargo'
|
||||
? externalCargoColumns
|
||||
: externalVehicleColumns;
|
||||
},
|
||||
duplicateRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
|
||||
);
|
||||
},
|
||||
visibleExternalRows() {
|
||||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||||
},
|
||||
unmatchedExternalRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.matchStatus !== 'matched' && !row.suspectedDuplicate
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emptyForm() {
|
||||
return {
|
||||
reconciliationNo: '',
|
||||
reconciliationMode: 'vehicle',
|
||||
formalSettlementId: null,
|
||||
formalSettlementNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractName: '',
|
||||
paidAmount: 0,
|
||||
reconcilerName: '',
|
||||
reconciliationDate: '',
|
||||
remark: '',
|
||||
internalBillCount: 0,
|
||||
externalBillCount: 0,
|
||||
differenceCount: 0,
|
||||
internalQuantity: 0,
|
||||
externalQuantity: 0,
|
||||
differenceQuantity: 0,
|
||||
internalAmount: 0,
|
||||
externalAmount: 0,
|
||||
differenceAmount: 0,
|
||||
matchedCount: 0,
|
||||
unmatchedCount: 0,
|
||||
};
|
||||
},
|
||||
async initialize() {
|
||||
this.currentId = this.recordId;
|
||||
this.form = this.emptyForm();
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalTab = 'all';
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
if (!this.currentId) {
|
||||
this.form.reconciliationMode = 'vehicle';
|
||||
this.form.reconcilerName = '当前用户';
|
||||
this.form.reconciliationDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadDetail();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadDetail() {
|
||||
const { data } = await api.getDetail(this.currentId);
|
||||
this.form = { ...this.emptyForm(), ...data };
|
||||
this.internalDetails = data.internalDetails || [];
|
||||
this.externalDetails = data.externalDetails || [];
|
||||
},
|
||||
openFormalDialog() {
|
||||
if (!this.editable) return;
|
||||
this.formalDialog.visible = true;
|
||||
this.formalDialog.page.current = 1;
|
||||
this.loadFormalOptions();
|
||||
},
|
||||
async loadFormalOptions() {
|
||||
this.formalDialog.loading = true;
|
||||
try {
|
||||
const { data } = await api.getFormalOptions(
|
||||
this.formalDialog.page.current,
|
||||
this.formalDialog.page.size,
|
||||
{ settlementType: this.settlementType, keyword: this.formalDialog.query.keyword }
|
||||
);
|
||||
this.formalDialog.rows = data.records || [];
|
||||
this.formalDialog.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.formalDialog.loading = false;
|
||||
}
|
||||
},
|
||||
confirmFormal() {
|
||||
if (this.formalDialog.selected.length !== 1)
|
||||
return this.$message.warning('请选择一张正式结算单');
|
||||
const selected = this.formalDialog.selected[0];
|
||||
this.form = {
|
||||
...this.form,
|
||||
...selected,
|
||||
formalSettlementId: selected.id,
|
||||
formalSettlementNo: selected.formalSettlementNo,
|
||||
reconciliationNo: this.form.reconciliationNo,
|
||||
};
|
||||
this.formalDialog.visible = false;
|
||||
},
|
||||
async handleSave() {
|
||||
const valid = await this.$refs.formRef.validate().catch(() => false);
|
||||
if (!valid || !this.form.formalSettlementId) return this.$message.warning('请选择正式结算单');
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.save({
|
||||
id: this.currentId,
|
||||
formalSettlementId: this.form.formalSettlementId,
|
||||
reconciliationMode: this.form.reconciliationMode,
|
||||
reconciliationDate: this.form.reconciliationDate,
|
||||
remark: this.form.remark,
|
||||
});
|
||||
this.currentId = data;
|
||||
await this.loadDetail();
|
||||
this.$message.success('草稿保存成功');
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async handleMatch() {
|
||||
if (!this.currentId) {
|
||||
await this.handleSave();
|
||||
if (!this.currentId) return;
|
||||
}
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.match(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('匹配完成');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.updateByMatch(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('账单更新完成');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
async handleComplete() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.complete(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('对账完成');
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
chooseImport() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
this.$refs.fileInput?.click();
|
||||
},
|
||||
async handleImport(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
if (!file) return;
|
||||
try {
|
||||
const response =
|
||||
this.form.reconciliationMode === 'cargo'
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||||
if (contentType.includes('spreadsheetml') || contentType.includes('ms-excel')) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = JSON.parse(await response.data.text());
|
||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
await this.loadDetail();
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
const response = await api.template(mode);
|
||||
downloadXls(
|
||||
response.data,
|
||||
`${mode === 'cargo' ? '货物明细对账模板' : '整车总额对账模板'}.xlsx`
|
||||
);
|
||||
},
|
||||
resetInternalQuery() {
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
this.internalFilterTick++;
|
||||
},
|
||||
openAdjust(row) {
|
||||
this.adjustDialog = {
|
||||
visible: true,
|
||||
saving: false,
|
||||
reason: '',
|
||||
rows: this.internalDetails
|
||||
.filter(item => item.formalSettlementDetailId === row.formalSettlementDetailId)
|
||||
.map(item => ({ ...item })),
|
||||
};
|
||||
},
|
||||
async saveAdjust() {
|
||||
if (!this.adjustDialog.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
this.adjustDialog.saving = true;
|
||||
try {
|
||||
for (const row of this.adjustDialog.rows)
|
||||
await api.adjust({
|
||||
...row,
|
||||
reconciliationId: this.currentId,
|
||||
updateMessage: this.adjustDialog.reason,
|
||||
});
|
||||
this.adjustDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('调整保存成功');
|
||||
} finally {
|
||||
this.adjustDialog.saving = false;
|
||||
}
|
||||
},
|
||||
async handleUnmatch(row) {
|
||||
await api.unmatch(row.id);
|
||||
await this.loadDetail();
|
||||
},
|
||||
openManualMatch(row) {
|
||||
this.manualDialog = { visible: true, internal: row, selected: null };
|
||||
},
|
||||
openManualMatchByExternal(row) {
|
||||
this.manualDialog = { visible: true, internal: null, selected: row };
|
||||
},
|
||||
async confirmManualMatch() {
|
||||
if (!this.manualDialog.selected) return this.$message.warning('请选择外部账单明细');
|
||||
if (!this.manualDialog.internal) return this.$message.warning('请从内部账单行发起人工匹配');
|
||||
await api.manualMatch({
|
||||
reconciliationId: this.currentId,
|
||||
internalId: this.manualDialog.internal.id,
|
||||
externalId: this.manualDialog.selected.id,
|
||||
});
|
||||
this.manualDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('人工匹配成功');
|
||||
},
|
||||
matchName(value) {
|
||||
return (
|
||||
{
|
||||
matched: '已匹配',
|
||||
suspected_duplicate: '疑似重复',
|
||||
partial: '部分匹配',
|
||||
unmatched: '未匹配',
|
||||
}[value] || '未匹配'
|
||||
);
|
||||
},
|
||||
matchTagType(value) {
|
||||
return value === 'matched'
|
||||
? 'success'
|
||||
: value === 'suspected_duplicate'
|
||||
? 'danger'
|
||||
: value === 'partial'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
},
|
||||
updateName(value) {
|
||||
return (
|
||||
{
|
||||
updated: '已更新',
|
||||
skipped_multi_cargo: '跳过更新',
|
||||
manually_adjusted: '手工调整',
|
||||
not_updated: '未更新',
|
||||
}[value] ||
|
||||
value ||
|
||||
'未更新'
|
||||
);
|
||||
},
|
||||
updateTagType(value) {
|
||||
return value === 'updated' || value === 'manually_adjusted'
|
||||
? 'success'
|
||||
: value === 'skipped_multi_cargo'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
formatNumber(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reconciliation-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.reconciliation-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__actions,
|
||||
.reconciliation-editor__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-editor__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
min-height: 104px;
|
||||
border: 1px solid #eff1f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
.stat-card b {
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
.stat-card span {
|
||||
color: #606266;
|
||||
}
|
||||
.stat-card small {
|
||||
color: #909399;
|
||||
}
|
||||
.stat-card--success b {
|
||||
color: #67c23a;
|
||||
}
|
||||
.stat-card--danger b {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.reconciliation-editor__help {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.dialog-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.hidden-file {
|
||||
display: none;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.reconciliation-editor__stats {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<basic-container class="formal-page">
|
||||
<section class="formal-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="formal-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<div class="formal-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="formal-page__table-panel">
|
||||
<el-tabs v-model="activeSettlementType" @tab-change="handleSettlementTypeChange">
|
||||
<el-tab-pane label="应付" name="payable" />
|
||||
<el-tab-pane label="应收" name="receivable" />
|
||||
</el-tabs>
|
||||
<div class="formal-page__toolbar">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_payment')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openPaymentDialog"
|
||||
>付款申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleSync"
|
||||
>同步金蝶</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_print')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handlePrint"
|
||||
>打印结算单</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="formal-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openView(row)">{{
|
||||
row[column.prop]
|
||||
}}</el-link>
|
||||
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)">{{
|
||||
displayValue(row[column.prop])
|
||||
}}</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||||
<span v-else-if="column.prop === 'invoiceStatusName'">{{
|
||||
invoiceName(row.invoiceStatus)
|
||||
}}</span>
|
||||
<span v-else-if="column.prop === 'paymentStatusName'">{{
|
||||
paymentName(row.paymentStatus)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><div class="formal-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_submit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeSyncStatus !== 'synced'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<formal-settlement-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
<el-dialog v-model="paymentDialog.visible" title="付款申请" width="520px" append-to-body>
|
||||
<el-form :model="paymentDialog.form" label-position="right" label-width="auto">
|
||||
<el-form-item label="结算单号">{{
|
||||
paymentDialog.row.formalSettlementNo || '-'
|
||||
}}</el-form-item>
|
||||
<el-form-item label="剩余可申请金额">{{
|
||||
formatMoney(paymentAvailable, paymentDialog.row.currency)
|
||||
}}</el-form-item>
|
||||
<el-form-item label="申请付款金额" required
|
||||
><el-input-number
|
||||
v-model="paymentDialog.form.appliedAmount"
|
||||
:min="0.01"
|
||||
:max="paymentAvailable"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/></el-form-item>
|
||||
<el-form-item label="备注"
|
||||
><el-input
|
||||
v-model="paymentDialog.form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer
|
||||
><el-button @click="paymentDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="paymentDialog.loading" @click="submitPayment"
|
||||
>提交</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
formalSettlementSearchFields,
|
||||
invoiceStatusOptions,
|
||||
paymentStatusOptions,
|
||||
} from '@/option/settlement/formalSettlementSearch';
|
||||
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
formalSettlementNo: '',
|
||||
preSettlementNo: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
invoiceStatus: '',
|
||||
paymentStatus: '',
|
||||
approvalStatus: '',
|
||||
kingdeeSyncStatus: '',
|
||||
createDateRange: [],
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlement',
|
||||
components: { FormalSettlementEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
activeSettlementType: 'payable',
|
||||
searchFields: formalSettlementSearchFields,
|
||||
columns: formalSettlementTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
paymentDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
row: {},
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
paymentAvailable() {
|
||||
return Math.max(
|
||||
0,
|
||||
Number(this.paymentDialog.row.settlementAmount || 0) -
|
||||
Number(this.paymentDialog.row.appliedPaymentAmount || 0)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = this.buildQueryParams();
|
||||
const response = await api.getList(this.page.current, this.page.size, params);
|
||||
const data = this.unwrapData(response);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.handleSearch();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: null, readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该正式结算草稿?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
await api.submit({ id: row.id });
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
'审核通过后正式结算生效,金额只能通过结算调整单处理,确认通过?',
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入驳回原因',
|
||||
});
|
||||
await api.returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废正式结算单', {
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
|
||||
});
|
||||
await api.voidBill({ id: row.id, reason: value });
|
||||
this.$message.success('已作废');
|
||||
this.loadTable();
|
||||
},
|
||||
selectedOne(action) {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$message.warning(`${action}需选择一条正式结算单`);
|
||||
return null;
|
||||
}
|
||||
return this.selection[0];
|
||||
},
|
||||
async handleSync() {
|
||||
const row = this.selectedOne('同步金蝶');
|
||||
if (!row) return;
|
||||
const response = await api.syncKingdee(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||
this.loadTable();
|
||||
},
|
||||
openPaymentDialog() {
|
||||
const row = this.selectedOne('付款申请');
|
||||
if (!row) return;
|
||||
if (row.approvalStatus !== 'approved' || row.settlementType !== 'payable')
|
||||
return this.$message.warning('仅审批通过的应付正式结算单允许发起付款申请');
|
||||
this.paymentDialog = {
|
||||
visible: true,
|
||||
loading: false,
|
||||
row,
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
};
|
||||
},
|
||||
async submitPayment() {
|
||||
if (Number(this.paymentDialog.form.appliedAmount || 0) <= 0)
|
||||
return this.$message.warning('请输入申请付款金额');
|
||||
this.paymentDialog.loading = true;
|
||||
try {
|
||||
const response = await api.applyPayment({
|
||||
id: this.paymentDialog.row.id,
|
||||
...this.paymentDialog.form,
|
||||
});
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`付款申请已生成:${data}`);
|
||||
this.paymentDialog.visible = false;
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.paymentDialog.loading = false;
|
||||
}
|
||||
},
|
||||
handlePrint() {
|
||||
const row = this.selectedOne('打印');
|
||||
if (!row) return;
|
||||
const win = window.open('', '_blank');
|
||||
if (!win) return this.$message.warning('浏览器阻止了打印窗口,请允许弹窗后重试');
|
||||
win.document.write(
|
||||
`<!doctype html><html><head><title>${
|
||||
row.formalSettlementNo
|
||||
}</title><style>body{font-family:Arial,"Microsoft YaHei",sans-serif;padding:32px;color:#222}h1{text-align:center}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px 28px;margin-top:30px}.item{border-bottom:1px solid #ddd;padding:8px 0}.actions{text-align:center;margin-top:36px}@media print{.actions{display:none}}</style></head><body><h1>正式结算单</h1><div class="grid"><div class="item">结算单号:${this.escapeHtml(
|
||||
row.formalSettlementNo
|
||||
)}</div><div class="item">预结算单号:${this.escapeHtml(
|
||||
row.preSettlementNos
|
||||
)}</div><div class="item">项目:${this.escapeHtml(
|
||||
row.projectName
|
||||
)}</div><div class="item">合同编号:${this.escapeHtml(
|
||||
row.contractNo
|
||||
)}</div><div class="item">合同名称:${this.escapeHtml(
|
||||
row.contractName
|
||||
)}</div><div class="item">所属组织:${this.escapeHtml(
|
||||
row.deptName
|
||||
)}</div><div class="item">付款方:${this.escapeHtml(
|
||||
row.payerName
|
||||
)}</div><div class="item">收款方:${this.escapeHtml(
|
||||
row.payeeName
|
||||
)}</div><div class="item">结算金额:${this.formatMoney(
|
||||
row.settlementAmount,
|
||||
row.currency
|
||||
)}</div></div><div class="actions"><button onclick="window.print()">打印</button></div></body></html>`
|
||||
);
|
||||
win.document.close();
|
||||
},
|
||||
async handleExport() {
|
||||
const response = await api.getList(1, 100000, this.buildQueryParams());
|
||||
const data = this.unwrapData(response);
|
||||
const rows = (data.records || []).map(item => ({
|
||||
结算单号: item.formalSettlementNo,
|
||||
预结算单号: item.preSettlementNos,
|
||||
来源: item.sourceType,
|
||||
付款方: item.payerName,
|
||||
收款方: item.payeeName,
|
||||
项目名称: item.projectName,
|
||||
所属组织: item.deptName,
|
||||
合同编号: item.contractNo,
|
||||
合同名称: item.contractName,
|
||||
原币结算金额: item.settlementAmount,
|
||||
本位币结算金额: item.localSettlementAmount,
|
||||
结算汇率: item.exchangeRate,
|
||||
发票状态: this.invoiceName(item.invoiceStatus),
|
||||
收付款状态: this.paymentName(item.paymentStatus),
|
||||
审核状态: item.approvalStatusName,
|
||||
金蝶单据号: item.kingdeeBillNo,
|
||||
创建人: item.createUserName,
|
||||
创建时间: item.createTime,
|
||||
}));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '正式结算单');
|
||||
XLSX.writeFile(workbook, `正式结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
buildQueryParams() {
|
||||
const params = { ...this.query };
|
||||
params.settlementType = this.activeSettlementType;
|
||||
const range = params.createDateRange || [];
|
||||
delete params.createDateRange;
|
||||
if (range.length === 2) {
|
||||
params.createStartDate = range[0];
|
||||
params.createEndDate = range[1];
|
||||
}
|
||||
return params;
|
||||
},
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
handleSettlementTypeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value || '-').replace(
|
||||
/[&<>"']/g,
|
||||
char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])
|
||||
);
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
statusType(status) {
|
||||
return (
|
||||
{ approved: 'success', reviewing: 'warning', returned: 'danger', voided: 'info' }[status] ||
|
||||
''
|
||||
);
|
||||
},
|
||||
invoiceName(value) {
|
||||
return invoiceStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
paymentName(value) {
|
||||
return paymentStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.formal-page__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.formal-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.formal-page__search :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.formal-page__search :deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.formal-page__search :deep(.el-input),
|
||||
.formal-page__search :deep(.el-select),
|
||||
.formal-page__search :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
.formal-page__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.formal-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.formal-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.formal-page__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formal-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
:deep(.formal-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.formal-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.formal-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,805 @@
|
||||
<template>
|
||||
<basic-container class="pre-settlement-page">
|
||||
<section class="pre-settlement-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="pre-settlement-page__search-grid">
|
||||
<template v-for="field in visibleSearchFields" :key="field.prop">
|
||||
<el-form-item :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<div class="pre-settlement-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="pre-settlement-page__table-panel">
|
||||
<div class="pre-settlement-page__toolbar">
|
||||
<div class="pre-settlement-page__toolbar-left">
|
||||
<el-button v-if="hasPermission('pre_settlement_add')" type="primary" @click="openCreate">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_advance')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openAdvanceDialog"
|
||||
>
|
||||
预付申请
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_formal')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleFormalSettlement"
|
||||
>
|
||||
尾款结算
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_print')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openPrintDialog"
|
||||
>
|
||||
打印结算单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pre-settlement-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
border
|
||||
class="pre-settlement-page__table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:fixed="column.fixed"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="column.link && row[column.prop] && hasPermission('pre_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<el-tag v-else-if="column.status" :type="statusTagType(row.approvalStatus)">
|
||||
{{ row[column.prop] || '-' }}
|
||||
</el-tag>
|
||||
<span v-else-if="column.money">
|
||||
{{ formatMoney(row[column.prop], moneyCurrency(row, column.prop)) }}
|
||||
</span>
|
||||
<span v-else-if="column.precision !== undefined">
|
||||
{{ formatNumber(row[column.prop], column.precision) }}
|
||||
</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="pre-settlement-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_view') && row.approvalStatus !== 'draft'"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
>
|
||||
流程
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
通过
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>
|
||||
驳回
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('pre_settlement_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
!row.formalSettlementNo
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>
|
||||
作废
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pre-settlement-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<pre-settlement-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="advanceDialog.visible" title="预付申请" width="520px" append-to-body>
|
||||
<el-form
|
||||
ref="advanceFormRef"
|
||||
:model="advanceForm"
|
||||
:rules="advanceRules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="预结算单号">
|
||||
<span>{{ advanceDialog.row.preSettlementNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算金额">
|
||||
<span>
|
||||
{{ formatMoney(advanceDialog.row.settlementAmount, advanceDialog.row.currency) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="已申请预付金额">
|
||||
<span>
|
||||
{{ formatMoney(advanceDialog.row.advanceAppliedAmount, advanceDialog.row.currency) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请预付金额" prop="appliedAmount">
|
||||
<el-input-number
|
||||
v-model="advanceForm.appliedAmount"
|
||||
:min="0"
|
||||
:max="advanceAvailableAmount"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="金蝶预付单号">
|
||||
<el-input
|
||||
v-model="advanceForm.kingdeeAdvanceNo"
|
||||
maxlength="100"
|
||||
placeholder="外部系统回写时可填写"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="advanceDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="advanceDialog.submitting" @click="submitAdvance">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="printDialog.visible" title="打印" width="620px" append-to-body>
|
||||
<div class="dialog-section-title">选择打印模板</div>
|
||||
<el-form label-position="right" label-width="auto" class="pre-settlement-page__print-form">
|
||||
<el-form-item label="项目">
|
||||
<span>{{ printDialog.row.projectName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板名称">
|
||||
<el-select v-model="printDialog.template" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in printDialog.templates"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="printDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="printDialog.loading" @click="handlePrintPreview">
|
||||
打印预览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="预结算单号">
|
||||
{{ flowDialog.row.preSettlementNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
applyAdvance,
|
||||
approve,
|
||||
exportList,
|
||||
formalSettlement,
|
||||
getDetail,
|
||||
getList,
|
||||
getPrintTemplates,
|
||||
remove,
|
||||
returnBill,
|
||||
voidBill,
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
|
||||
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import PreSettlementEditor from './components/pre-settlement-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
preSettlementNo: '',
|
||||
advanceNo: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractName: '',
|
||||
contractNo: '',
|
||||
payeeName: '',
|
||||
payerName: '',
|
||||
createDateRange: [],
|
||||
approvalStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'PreSettlement',
|
||||
components: { PreSettlementEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: preSettlementSearchFields,
|
||||
searchExpanded: false,
|
||||
tableColumns: preSettlementTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
},
|
||||
editor: {
|
||||
visible: false,
|
||||
id: '',
|
||||
readonly: false,
|
||||
},
|
||||
advanceDialog: {
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: {},
|
||||
},
|
||||
advanceForm: {
|
||||
appliedAmount: null,
|
||||
kingdeeAdvanceNo: '',
|
||||
},
|
||||
advanceRules: {
|
||||
appliedAmount: [
|
||||
{ required: true, message: '请输入申请预付金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!value || Number(value) <= 0) {
|
||||
callback(new Error('申请预付金额必须大于0'));
|
||||
return;
|
||||
}
|
||||
if (Number(value) > this.advanceAvailableAmount) {
|
||||
callback(new Error('申请预付金额不能超过剩余可申请金额'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
},
|
||||
printDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
row: {},
|
||||
templates: [],
|
||||
template: '',
|
||||
},
|
||||
flowDialog: {
|
||||
visible: false,
|
||||
row: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
advanceAvailableAmount() {
|
||||
return Math.max(
|
||||
Number(this.advanceDialog.row.settlementAmount || 0) -
|
||||
Number(this.advanceDialog.row.advanceAppliedAmount || 0),
|
||||
0
|
||||
);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await getList(this.page.current, this.page.size, this.buildQuery());
|
||||
const result = data?.data || {};
|
||||
this.rows = result.records || [];
|
||||
this.page.total = Number(result.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
buildQuery() {
|
||||
const range = this.query.createDateRange || [];
|
||||
return {
|
||||
...this.query,
|
||||
createDateRange: undefined,
|
||||
createStartDate: range[0],
|
||||
createEndDate: range[1],
|
||||
};
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
handleSelectionChange(rows) {
|
||||
this.selection = rows;
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: '', readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该预结算草稿?', '提示', { type: 'warning' });
|
||||
await remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
selectedOne(operationName) {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$message.warning(`${operationName}需选择一条预结算单`);
|
||||
return null;
|
||||
}
|
||||
return this.selection[0];
|
||||
},
|
||||
openAdvanceDialog() {
|
||||
const row = this.selectedOne('预付申请');
|
||||
if (!row) return;
|
||||
if (
|
||||
row.approvalStatus !== 'approved' ||
|
||||
row.settlementType !== 'payable' ||
|
||||
row.formalSettlementNo
|
||||
) {
|
||||
this.$message.warning('仅审批通过、未转正式结算的应付预结算单可发起预付');
|
||||
return;
|
||||
}
|
||||
this.advanceDialog.row = row;
|
||||
this.advanceDialog.visible = true;
|
||||
this.advanceForm = { appliedAmount: null, kingdeeAdvanceNo: '' };
|
||||
this.$nextTick(() => this.$refs.advanceFormRef?.clearValidate());
|
||||
},
|
||||
async submitAdvance() {
|
||||
await this.$refs.advanceFormRef?.validate();
|
||||
this.advanceDialog.submitting = true;
|
||||
try {
|
||||
await applyAdvance({
|
||||
preSettlementId: this.advanceDialog.row.id,
|
||||
appliedAmount: this.advanceForm.appliedAmount,
|
||||
kingdeeAdvanceNo: this.advanceForm.kingdeeAdvanceNo,
|
||||
});
|
||||
this.$message.success('预付申请提交成功');
|
||||
this.advanceDialog.visible = false;
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.advanceDialog.submitting = false;
|
||||
}
|
||||
},
|
||||
async handleFormalSettlement() {
|
||||
const row = this.selectedOne('尾款结算');
|
||||
if (!row) return;
|
||||
if (
|
||||
row.approvalStatus !== 'approved' ||
|
||||
row.settlementType !== 'payable' ||
|
||||
row.formalSettlementNo
|
||||
) {
|
||||
this.$message.warning('仅审批通过、未转正式结算的应付预结算单可发起尾款结算');
|
||||
return;
|
||||
}
|
||||
await this.$confirm('转正式结算后无法发起预付,确认继续?', '提示', {
|
||||
type: 'warning',
|
||||
});
|
||||
const { data } = await formalSettlement(row.id);
|
||||
this.$message.success(`正式结算单已生成:${data?.data || ''}`);
|
||||
this.loadTable();
|
||||
},
|
||||
async openPrintDialog() {
|
||||
const row = this.selectedOne('打印结算单');
|
||||
if (!row) return;
|
||||
if (row.approvalStatus === 'voided') {
|
||||
this.$message.warning('已作废的预结算单不能打印');
|
||||
return;
|
||||
}
|
||||
this.printDialog.row = row;
|
||||
this.printDialog.visible = true;
|
||||
this.printDialog.loading = true;
|
||||
try {
|
||||
const { data } = await getPrintTemplates(row.id);
|
||||
this.printDialog.templates = data?.data || [];
|
||||
this.printDialog.template = this.printDialog.templates[0]?.value || '';
|
||||
} finally {
|
||||
this.printDialog.loading = false;
|
||||
}
|
||||
},
|
||||
async handlePrintPreview() {
|
||||
if (!this.printDialog.template) {
|
||||
this.$message.warning('请选择打印模板');
|
||||
return;
|
||||
}
|
||||
const previewWindow = window.open('', '_blank');
|
||||
if (!previewWindow) {
|
||||
this.$message.warning('浏览器阻止了打印预览窗口,请允许弹出窗口后重试');
|
||||
return;
|
||||
}
|
||||
this.printDialog.loading = true;
|
||||
try {
|
||||
const { data } = await getDetail(this.printDialog.row.id);
|
||||
const detail = data?.data || {};
|
||||
previewWindow.document.open();
|
||||
previewWindow.document.write(this.buildPrintHtml(detail));
|
||||
previewWindow.document.close();
|
||||
this.printDialog.visible = false;
|
||||
} catch (error) {
|
||||
previewWindow.close();
|
||||
throw error;
|
||||
} finally {
|
||||
this.printDialog.loading = false;
|
||||
}
|
||||
},
|
||||
buildPrintHtml(detail) {
|
||||
const summaryRows = (detail.summaryFees || [])
|
||||
.map(
|
||||
(row, index) =>
|
||||
`<tr><td>${index + 1}</td><td>${this.escapeHtml(row.feeType)}</td><td>${this.escapeHtml(
|
||||
row.feeItem
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.originalAmount, detail.currency)
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.adjustAmount, detail.currency)
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.settlementAmount, detail.currency)
|
||||
)}</td></tr>`
|
||||
)
|
||||
.join('');
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><title>${this.escapeHtml(
|
||||
detail.preSettlementNo || '预结算单'
|
||||
)}</title><style>body{font-family:Arial,"Microsoft YaHei",sans-serif;color:#222;padding:28px}h1{text-align:center;font-size:24px}.meta{display:grid;grid-template-columns:repeat(3,1fr);gap:14px 24px;margin:24px 0}.meta div{border-bottom:1px solid #ddd;padding:7px 0}table{width:100%;border-collapse:collapse;margin-top:16px}th,td{border:1px solid #bbb;padding:9px;text-align:center;font-size:13px}th{background:#f5f5f5}.actions{text-align:center;margin-top:24px}@media print{.actions{display:none}}</style></head><body><h1>预结算单</h1><div class="meta"><div>预结算单号:${this.escapeHtml(
|
||||
detail.preSettlementNo
|
||||
)}</div><div>项目名称:${this.escapeHtml(
|
||||
detail.projectName
|
||||
)}</div><div>合同编号:${this.escapeHtml(
|
||||
detail.contractNo
|
||||
)}</div><div>合同名称:${this.escapeHtml(
|
||||
detail.contractName
|
||||
)}</div><div>收款方:${this.escapeHtml(detail.payeeName)}</div><div>付款方:${this.escapeHtml(
|
||||
detail.payerName
|
||||
)}</div><div>结算类型:${
|
||||
detail.settlementType === 'receivable' ? '应收' : '应付'
|
||||
}</div><div>结算金额:${this.escapeHtml(
|
||||
this.formatMoney(detail.settlementAmount, detail.currency)
|
||||
)}</div><div>本位币合计:${this.escapeHtml(
|
||||
this.formatMoney(detail.localSettlementAmount, detail.localCurrency)
|
||||
)}</div><div>汇率日期:${this.escapeHtml(
|
||||
detail.exchangeRateDate
|
||||
)}</div><div>结算汇率:${this.escapeHtml(
|
||||
detail.exchangeRate
|
||||
)}</div><div>创建人:${this.escapeHtml(
|
||||
detail.createUserName
|
||||
)}</div></div><h2>结算合计</h2><table><thead><tr><th>序号</th><th>费用类型</th><th>费用项</th><th>原金额</th><th>调整金额</th><th>结算金额</th></tr></thead><tbody>${summaryRows}</tbody></table><div class="actions"><button onclick="window.print()">打印 / 另存为 PDF</button></div></body></html>`;
|
||||
},
|
||||
async handleExport() {
|
||||
const { data } = await exportList(this.buildQuery());
|
||||
downloadFile(data, `预结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
openFlow(row) {
|
||||
this.flowDialog.row = row;
|
||||
this.flowDialog.visible = true;
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该预结算单?', '提示', { type: 'warning' });
|
||||
await approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入驳回原因',
|
||||
});
|
||||
await returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废预结算单', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
|
||||
});
|
||||
await voidBill({ id: row.id, reason: value });
|
||||
this.$message.success('作废成功');
|
||||
this.loadTable();
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const amount = Number(value);
|
||||
if (!Number.isFinite(amount)) return '-';
|
||||
return `${amount.toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
formatNumber(value, precision = 2) {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(precision) : '-';
|
||||
},
|
||||
moneyCurrency(row, prop) {
|
||||
return prop === 'localSettlementAmount' ? row.localCurrency : row.currency;
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === undefined || value === null || value === '' ? '-' : value;
|
||||
},
|
||||
statusTagType(status) {
|
||||
return {
|
||||
draft: 'info',
|
||||
reviewing: 'warning',
|
||||
approved: 'success',
|
||||
returned: 'danger',
|
||||
voided: 'info',
|
||||
}[status];
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value ?? '-')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pre-settlement-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__toolbar-left,
|
||||
&__toolbar-right,
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__print-form {
|
||||
margin-top: 20px;
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.pre-settlement-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.pre-settlement-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page .el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,9 @@
|
||||
<div class="settlement-detail-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch" />
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -31,8 +33,8 @@
|
||||
<div class="settlement-detail-page__toolbar">
|
||||
<div class="settlement-detail-page__toolbar-left">
|
||||
<el-button type="primary" @click="openGenerateDialog">生成费用</el-button>
|
||||
<el-button type="primary" @click="openUpdateFeeDialog">更新费用</el-button>
|
||||
<el-button type="primary" @click="openTransferDialog">批量转结算</el-button>
|
||||
<el-button type="primary" plain @click="openUpdateFeeDialog">更新费用</el-button>
|
||||
<el-button type="primary" plain @click="openTransferDialog">批量转结算</el-button>
|
||||
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
<div class="settlement-detail-page__toolbar-right">
|
||||
@@ -55,7 +57,7 @@
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
v-for="column in displayTableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
@@ -71,16 +73,16 @@
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||
<span v-else>{{ formatColumnValue(row, column) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="settlement-detail-page__actions">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openUpdateFeeDialog(row)">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openAdjustDialog(row)">
|
||||
调整
|
||||
</el-link>
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="closeRow(row)">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="danger" @click="closeRow(row)">
|
||||
关闭
|
||||
</el-link>
|
||||
<el-link v-else type="primary" disabled>-</el-link>
|
||||
@@ -122,7 +124,7 @@
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ formatCell(row[column.prop]) }}</template>
|
||||
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
@@ -154,6 +156,134 @@
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="adjustDialog.visible"
|
||||
title="调整费用"
|
||||
width="96%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="settlement-detail-page__adjust-toolbar">
|
||||
<el-button type="primary" plain @click="addManualAdjustRow">新增费用项</el-button>
|
||||
</div>
|
||||
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column label="手工费用项目" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeItemName"
|
||||
clearable
|
||||
placeholder="请输入收费或扣费项目"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收费/扣费" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeType"
|
||||
placeholder="请选择"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
>
|
||||
<el-option label="收费" value="charge" />
|
||||
<el-option label="扣费" value="deduct" />
|
||||
</el-select>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手工金额" width="150" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="row.manualFee"
|
||||
v-model="row.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.manualFee && !row.id"
|
||||
type="danger"
|
||||
@click="removeManualAdjustRow(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="column in adjustFeeColumns"
|
||||
:key="column.prop || column.feeItemName"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="column.prop === 'transportQuantityText' && !row.manualFee"
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'mileage' && !row.manualFee"
|
||||
v-model="row.mileage"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'freightAmount' && !row.manualFee"
|
||||
v-model="row.freightAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, 'freight')"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.dynamic && !row.manualFee"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, column.feeItemName)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'remark'"
|
||||
v-model="row.remark"
|
||||
clearable
|
||||
maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else-if="column.prop === 'adjustAmountText'">
|
||||
{{ fixedTwoDecimals(row.adjustAmount) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'afterAmountText'">
|
||||
{{ fixedTwoDecimals(row.afterAmount) }}
|
||||
</span>
|
||||
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="adjustDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="updateFeeDialog.visible" title="更新费用" width="620px" append-to-body>
|
||||
<el-form
|
||||
ref="updateFeeFormRef"
|
||||
@@ -169,9 +299,10 @@
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择合同"
|
||||
@change="handleUpdateContractChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
v-for="item in updateContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:value="item.id"
|
||||
@@ -179,7 +310,13 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同计费方案" prop="billingPlanId">
|
||||
<el-select v-model="updateFeeForm.billingPlanId" clearable filterable placeholder="请选择">
|
||||
<el-select
|
||||
v-model="updateFeeForm.billingPlanId"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!updateFeeForm.contractId"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in billingPlanOptions"
|
||||
:key="item.id"
|
||||
@@ -190,10 +327,10 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button @click="updateFeeDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -201,7 +338,10 @@
|
||||
<div class="settlement-detail-page__transfer-form">
|
||||
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
|
||||
<el-form-item label="转结算类型">
|
||||
<el-radio-group v-model="transferForm.settlementBillType">
|
||||
<el-radio-group
|
||||
v-model="transferForm.settlementBillType"
|
||||
@change="loadTransferCandidates"
|
||||
>
|
||||
<el-radio label="pre">预结算单</el-radio>
|
||||
<el-radio label="formal">正式结算单</el-radio>
|
||||
</el-radio-group>
|
||||
@@ -246,13 +386,15 @@
|
||||
:min-width="column.minWidth"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button @click="transferDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -323,7 +465,7 @@
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary">
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||
<span v-else>{{ formatColumnValue(row, column) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -339,8 +481,8 @@
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||
<el-button @click="generateDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -369,11 +511,11 @@
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="generateDialog.visible = true">上一步</el-button>
|
||||
<el-button type="primary" :loading="previewDialog.submitting" @click="submitGenerateFee">
|
||||
确认
|
||||
提交
|
||||
</el-button>
|
||||
<el-button @click="previewDialog.visible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
@@ -395,6 +537,7 @@ import {
|
||||
import * as api from '@/api/settlement/receivable-payable-detail';
|
||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
|
||||
export default {
|
||||
@@ -412,6 +555,7 @@ export default {
|
||||
Setting,
|
||||
searchFields,
|
||||
tableColumns,
|
||||
tableFeeItemNames: [],
|
||||
changeRecordColumns,
|
||||
transferSearchFields,
|
||||
generateWaybillColumns,
|
||||
@@ -424,6 +568,9 @@ export default {
|
||||
detailDialog: { visible: false, title: '费用明细', activeTab: 'fee', row: null, loading: false },
|
||||
feeRows: [],
|
||||
dynamicFeeColumns: [],
|
||||
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
|
||||
adjustRows: [],
|
||||
adjustDynamicFeeColumns: [],
|
||||
changeRows: [],
|
||||
changePage: { current: 1, size: 10, total: 0 },
|
||||
changeDialog: { loading: false },
|
||||
@@ -433,6 +580,8 @@ export default {
|
||||
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
|
||||
},
|
||||
contractOptions: [],
|
||||
updateContractOptions: [],
|
||||
transportTypeOptions: [],
|
||||
billingPlanOptions: [],
|
||||
transferDialog: { visible: false, loading: false, submitting: false },
|
||||
transferQuery: {},
|
||||
@@ -456,22 +605,67 @@ export default {
|
||||
return '应收应付';
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 7);
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
feeDetailColumns() {
|
||||
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||
},
|
||||
displayTableColumns() {
|
||||
const columns = [...this.tableColumns];
|
||||
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
|
||||
const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({
|
||||
label: name,
|
||||
prop: `tableFeeItem${index}`,
|
||||
feeItemName: name,
|
||||
dynamic: true,
|
||||
minWidth: 130,
|
||||
align: 'right',
|
||||
}));
|
||||
if (totalIndex < 0) return [...columns, ...dynamicColumns];
|
||||
columns.splice(totalIndex, 0, ...dynamicColumns);
|
||||
return columns;
|
||||
},
|
||||
previewColumns() {
|
||||
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||
},
|
||||
adjustFeeColumns() {
|
||||
return [
|
||||
...feeDetailBaseColumns,
|
||||
...this.adjustDynamicFeeColumns,
|
||||
...feeDetailTailColumns,
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadContracts();
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
async loadTransportTypeOptions() {
|
||||
const res = await getDictionary({ code: 'transport_type' });
|
||||
const records = res.data?.data || [];
|
||||
this.transportTypeOptions = records.map(item => ({
|
||||
label: item.dictValue || item.label || item.name,
|
||||
value: item.dictKey || item.value || item.dictValue || item.name,
|
||||
}));
|
||||
},
|
||||
transportTypeLabel(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return (
|
||||
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
|
||||
);
|
||||
},
|
||||
formatColumnValue(row, column) {
|
||||
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
|
||||
if (column.dynamic) {
|
||||
return this.money(this.normalizeFeeItems(row.feeItems)[column.feeItemName], row.currency || 'RMB');
|
||||
}
|
||||
return this.formatDetailCell(row, column.prop);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
this.tableFeeItemNames = [];
|
||||
try {
|
||||
const params = this.buildRequestParams(
|
||||
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||
@@ -479,6 +673,7 @@ export default {
|
||||
const res = await api.getList(this.page.current, this.page.size, params);
|
||||
const data = this.unwrapPage(res);
|
||||
this.rows = (data.records || []).map(this.decorateRow);
|
||||
this.tableFeeItemNames = this.collectFeeItemNames(this.rows);
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -511,6 +706,132 @@ export default {
|
||||
this.detailDialog = { ...this.detailDialog, visible: true, row, title: row.documentNo, activeTab: 'fee' };
|
||||
await this.loadFeeDetail();
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.adjustDialog = { ...this.adjustDialog, visible: true, row, loading: true };
|
||||
try {
|
||||
const res = await api.getFeeDetail(row.id);
|
||||
const data = res.data?.data || res.data || res || {};
|
||||
this.adjustDynamicFeeColumns = (data.feeItemNames || []).map(name => ({
|
||||
label: name,
|
||||
feeItemName: name,
|
||||
dynamic: true,
|
||||
minWidth: 150,
|
||||
align: 'right',
|
||||
}));
|
||||
this.adjustRows = (data.records || []).map(item => {
|
||||
const feeItems = {};
|
||||
(data.feeItemNames || []).forEach(name => {
|
||||
feeItems[name] = Number(item.feeItems?.[name] || 0);
|
||||
});
|
||||
const adjusted = {
|
||||
...item,
|
||||
transportQuantity: Number(item.transportQuantity || 0),
|
||||
mileage:
|
||||
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
|
||||
? null
|
||||
: Number(item.mileage),
|
||||
freightAmount: Number(item.freightAmount || 0),
|
||||
originalAmount: Number(item.originalAmount || 0),
|
||||
feeItems,
|
||||
manualFee: item.billingFactor === '手工调整',
|
||||
feeItemName:
|
||||
item.billingFactor === '手工调整' ? Object.keys(feeItems)[0] || '' : '',
|
||||
feeType: item.billingType === '手工扣费' ? 'deduct' : 'charge',
|
||||
amount:
|
||||
item.billingFactor === '手工调整'
|
||||
? Math.abs(Number(item.afterAmount || 0))
|
||||
: 0,
|
||||
};
|
||||
if (adjusted.manualFee) this.recalculateManualAdjustRow(adjusted);
|
||||
else this.recalculateAdjustRow(adjusted);
|
||||
return adjusted;
|
||||
});
|
||||
} finally {
|
||||
this.adjustDialog.loading = false;
|
||||
}
|
||||
},
|
||||
addManualAdjustRow() {
|
||||
this.adjustRows.push({
|
||||
id: null,
|
||||
manualFee: true,
|
||||
feeItemName: '',
|
||||
feeType: 'charge',
|
||||
amount: 0,
|
||||
transportQuantity: null,
|
||||
mileage: null,
|
||||
freightAmount: 0,
|
||||
originalAmount: 0,
|
||||
feeItems: {},
|
||||
remark: '',
|
||||
});
|
||||
},
|
||||
removeManualAdjustRow(row) {
|
||||
const index = this.adjustRows.indexOf(row);
|
||||
if (index >= 0) this.adjustRows.splice(index, 1);
|
||||
},
|
||||
recalculateManualAdjustRow(row) {
|
||||
const amount = Number(row.amount || 0);
|
||||
row.afterAmount = Number((row.feeType === 'deduct' ? -amount : amount).toFixed(2));
|
||||
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
|
||||
},
|
||||
fixedTwoDecimals(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
recalculateAdjustRow(row, changedField) {
|
||||
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
|
||||
row.freightAmount = Number(row.feeItems[changedField] || 0);
|
||||
}
|
||||
if (changedField === 'freight') {
|
||||
const freightItem = Object.keys(row.feeItems).find(this.isFreightFeeItem);
|
||||
if (freightItem) row.feeItems[freightItem] = Number(row.freightAmount || 0);
|
||||
}
|
||||
const feeItemTotal = Object.values(row.feeItems).reduce(
|
||||
(total, value) => total + Number(value || 0),
|
||||
0
|
||||
);
|
||||
const hasFreightItem = Object.keys(row.feeItems).some(this.isFreightFeeItem);
|
||||
row.afterAmount = Number((hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2));
|
||||
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
|
||||
},
|
||||
isFreightFeeItem(name) {
|
||||
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
||||
},
|
||||
async saveAdjustFee() {
|
||||
if (!this.adjustRows.length || !this.adjustDialog.row) {
|
||||
this.$message.warning('没有可调整的费用明细');
|
||||
return;
|
||||
}
|
||||
const invalidManualRow = this.adjustRows.find(
|
||||
row => row.manualFee && (!String(row.feeItemName || '').trim() || Number(row.amount || 0) <= 0)
|
||||
);
|
||||
if (invalidManualRow) {
|
||||
this.$message.warning('请完整填写手工费用项目和金额');
|
||||
return;
|
||||
}
|
||||
this.adjustDialog.submitting = true;
|
||||
try {
|
||||
await api.adjustFee({
|
||||
detailId: this.adjustDialog.row.id,
|
||||
rows: this.adjustRows.map(row => ({
|
||||
id: row.id,
|
||||
transportQuantity: row.transportQuantity,
|
||||
mileage: row.mileage,
|
||||
freightAmount: row.freightAmount,
|
||||
feeItems: row.feeItems,
|
||||
manualFee: row.manualFee === true,
|
||||
feeItemName: row.feeItemName,
|
||||
feeType: row.feeType,
|
||||
amount: row.amount,
|
||||
remark: row.remark,
|
||||
})),
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.adjustDialog.visible = false;
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.adjustDialog.submitting = false;
|
||||
}
|
||||
},
|
||||
async loadFeeDetail() {
|
||||
if (!this.detailDialog.row) return;
|
||||
this.detailDialog.loading = true;
|
||||
@@ -555,20 +876,36 @@ export default {
|
||||
this.changePage.current = 1;
|
||||
this.loadChangeRecords();
|
||||
},
|
||||
openUpdateFeeDialog(row) {
|
||||
async openUpdateFeeDialog(row) {
|
||||
await this.loadUpdateFeeContracts();
|
||||
this.updateFeeDialog = { ...this.updateFeeDialog, visible: true, row: row || null };
|
||||
this.updateFeeForm = {
|
||||
ids: row ? [row.id] : this.selection.map(item => item.id),
|
||||
settlementType: this.settlementType || undefined,
|
||||
contractId: row?.contractId || '',
|
||||
billingPlanId: '',
|
||||
};
|
||||
this.syncBillingPlanOptions(this.updateFeeForm.contractId);
|
||||
this.handleUpdateContractChange(this.updateFeeForm.contractId);
|
||||
},
|
||||
async loadUpdateFeeContracts() {
|
||||
const res = await api.getUpdateFeeContracts({
|
||||
settlementType: this.settlementType || undefined,
|
||||
});
|
||||
this.updateContractOptions = res.data?.data || [];
|
||||
},
|
||||
handleUpdateContractChange(contractId) {
|
||||
this.updateFeeForm.billingPlanId = '';
|
||||
if (!contractId) {
|
||||
this.billingPlanOptions = [];
|
||||
return;
|
||||
}
|
||||
const options = this.syncBillingPlanOptions(contractId);
|
||||
const selected = options.find(item => item.defaultPlan) || options[0];
|
||||
this.updateFeeForm.billingPlanId = selected?.id || '';
|
||||
},
|
||||
async confirmUpdateFee() {
|
||||
await this.$refs.updateFeeFormRef.validate();
|
||||
await this.$confirm(
|
||||
'更新费用将使用最新的合同计费规则重新生成费用明细,确认更新费用?',
|
||||
'将使用选定计费方案,更新该合同下所有待结算明细的费用,确认继续?',
|
||||
'确认提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
@@ -602,20 +939,73 @@ export default {
|
||||
},
|
||||
async loadTransferCandidates() {
|
||||
this.transferDialog.loading = true;
|
||||
this.transferSelection = [];
|
||||
try {
|
||||
const params = this.buildRequestParams(
|
||||
this.normalizeQuery(this.transferQuery, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||
this.normalizeQuery(
|
||||
this.transferQuery,
|
||||
'generateDateRange',
|
||||
'generateStartDate',
|
||||
'generateEndDate'
|
||||
)
|
||||
);
|
||||
const res = await api.getTransferCandidates(1, 50, {
|
||||
...params,
|
||||
settlementStatus: 'pending',
|
||||
settlementType: this.settlementType || undefined,
|
||||
settlementBillType: this.transferForm.settlementBillType,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.transferRows = (data.records || []).map(this.decorateRow);
|
||||
this.transferRows = (data.records || [])
|
||||
.filter(row => this.isTransferCandidate(row))
|
||||
.map(this.decorateRow);
|
||||
} finally {
|
||||
this.transferDialog.loading = false;
|
||||
}
|
||||
},
|
||||
isTransferCandidate(row) {
|
||||
const hasValue = value => {
|
||||
if (Array.isArray(value)) return value.length > 0;
|
||||
return value !== null && value !== undefined && String(value).trim() !== '';
|
||||
};
|
||||
const normalizeSettlementType = value => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === '应收') return 'receivable';
|
||||
if (normalized === '应付') return 'payable';
|
||||
return normalized;
|
||||
};
|
||||
const rowSettlementType = normalizeSettlementType(
|
||||
row.settlementType || row.settlementTypeName
|
||||
);
|
||||
const expectedSettlementType = normalizeSettlementType(this.settlementType);
|
||||
const matchesSettlementType =
|
||||
!expectedSettlementType || rowSettlementType === expectedSettlementType;
|
||||
const status = String(row.settlementStatus || '').trim().toLowerCase();
|
||||
const isClosed =
|
||||
[row.closed, row.isClosed, row.closeFlag, row.closedFlag].some(
|
||||
value => value === true || String(value).trim().toLowerCase() === 'true'
|
||||
) ||
|
||||
['closed', 'close', '已关闭'].includes(status);
|
||||
const hasPreSettlement = [
|
||||
row.preSettlementId,
|
||||
row.preSettlementIds,
|
||||
row.preSettlementNo,
|
||||
row.preSettlementNos,
|
||||
].some(hasValue);
|
||||
const hasFormalSettlement = [
|
||||
row.formalSettlementId,
|
||||
row.formalSettlementIds,
|
||||
row.formalSettlementNo,
|
||||
row.formalSettlementNos,
|
||||
].some(hasValue);
|
||||
return (
|
||||
matchesSettlementType &&
|
||||
['pending', '待结算'].includes(status) &&
|
||||
!isClosed &&
|
||||
!hasPreSettlement &&
|
||||
!hasFormalSettlement
|
||||
);
|
||||
},
|
||||
confirmTransferSelection() {
|
||||
if (!this.transferSelection.length) {
|
||||
this.$message.warning('请选择需要转结算的明细');
|
||||
@@ -763,14 +1153,16 @@ export default {
|
||||
return this.settlementType ? { ...params, settlementType: this.settlementType } : params;
|
||||
},
|
||||
syncBillingPlanOptions(contractId) {
|
||||
const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
|
||||
const contracts = [...this.updateContractOptions, ...this.contractOptions];
|
||||
const contract = contracts.find(item => String(item.id) === String(contractId));
|
||||
const plans = this.parseJson(contract?.billingPlanJson);
|
||||
this.billingPlanOptions = plans.length
|
||||
? plans.map((item, index) => ({
|
||||
id: item.id || item.planId || item.name || `plan-${index}`,
|
||||
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
|
||||
}))
|
||||
: [{ id: 'default', name: '默认计费方案' }];
|
||||
this.billingPlanOptions = plans.map((item, index) => ({
|
||||
id: item.id || item.planId || item.name || `plan-${index}`,
|
||||
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
|
||||
defaultPlan:
|
||||
item.defaultPlan === true || String(item.defaultPlan).toLowerCase() === 'true',
|
||||
}));
|
||||
return this.billingPlanOptions;
|
||||
},
|
||||
normalizeQuery(source, rangeProp, startProp, endProp) {
|
||||
const params = { ...source };
|
||||
@@ -796,10 +1188,41 @@ export default {
|
||||
'-',
|
||||
};
|
||||
},
|
||||
collectFeeItemNames(rows) {
|
||||
const names = [];
|
||||
(rows || []).forEach(row => {
|
||||
Object.keys(this.normalizeFeeItems(row.feeItems)).forEach(name => {
|
||||
if (!names.includes(name)) names.push(name);
|
||||
});
|
||||
});
|
||||
return names;
|
||||
},
|
||||
normalizeFeeItems(value) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
if (typeof value !== 'string' || !value.trim()) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
money(value, currency) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return `${Number(value).toFixed(2)} ${currency}`;
|
||||
},
|
||||
formatDetailCell(row, prop) {
|
||||
if (
|
||||
prop === 'mileage' &&
|
||||
(row?.[prop] === null ||
|
||||
row?.[prop] === undefined ||
|
||||
row?.[prop] === '' ||
|
||||
Number(row[prop]) === -1)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
return this.formatCell(row?.[prop]);
|
||||
},
|
||||
formatCell(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
@@ -840,7 +1263,7 @@ export default {
|
||||
|
||||
.settlement-detail-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
|
||||
@@ -863,10 +1286,6 @@ export default {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settlement-detail-page__table-panel {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.settlement-detail-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -885,6 +1304,7 @@ export default {
|
||||
.settlement-detail-page__table,
|
||||
.settlement-detail-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
|
||||
:deep(th.el-table__cell) {
|
||||
background: #f5f7fa;
|
||||
@@ -924,13 +1344,13 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
@media screen and (max-width: 1200px) {
|
||||
.settlement-detail-page__search-grid {
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
@media screen and (max-width: 760px) {
|
||||
.settlement-detail-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<basic-container class="settlement-adjustment-page">
|
||||
<section class="settlement-adjustment-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="settlement-adjustment-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
><el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<div class="settlement-adjustment-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="settlement-adjustment-page__table-panel">
|
||||
<div class="settlement-adjustment-page__toolbar">
|
||||
<el-button
|
||||
v-if="hasPermission('settlement_adjustment_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
><div class="settlement-adjustment-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="64"
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
><template #default="{ row }"
|
||||
><el-link
|
||||
v-if="column.link && row[column.prop]"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>{{ row[column.prop] }}</el-link
|
||||
><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)">{{
|
||||
row[column.prop] || '-'
|
||||
}}</el-tag
|
||||
><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center"
|
||||
><template #default="{ row }"
|
||||
><div class="settlement-adjustment-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('settlement_adjustment_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
><el-link
|
||||
v-if="hasPermission('settlement_adjustment_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_delete') && row.approvalStatus === 'draft'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
><el-link
|
||||
v-if="hasPermission('settlement_adjustment_submit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_repush') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeSyncStatus === 'synced'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleRepush(row)"
|
||||
>重新推送(高危)</el-link
|
||||
>
|
||||
</div></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="settlement-adjustment-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<settlement-adjustment-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/settlement/settlementAdjustment';
|
||||
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
|
||||
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
|
||||
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
adjustmentNo: '',
|
||||
createDateRange: [],
|
||||
customerName: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
formalSettlementNo: '',
|
||||
settlementType: '',
|
||||
approvalStatus: '',
|
||||
});
|
||||
export default {
|
||||
name: 'SettlementAdjustment',
|
||||
components: { SettlementAdjustmentEditor },
|
||||
data: () => ({
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: settlementAdjustmentSearchFields,
|
||||
searchExpanded: false,
|
||||
tableColumns: settlementAdjustmentTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: '', readonly: false },
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const range = this.query.createDateRange || [];
|
||||
const { data } = await api.getList(this.page.current, this.page.size, {
|
||||
...this.query,
|
||||
createDateRange: undefined,
|
||||
createStartDate: range[0],
|
||||
createEndDate: range[1],
|
||||
});
|
||||
const result = data?.data || {};
|
||||
this.rows = result.records || [];
|
||||
this.page.total = Number(result.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: '', readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该结算调整草稿?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('确认提交审批?', '提示');
|
||||
await api.submit(row.id);
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('审批通过后将更新关联正式结算单,确认继续?', '提示');
|
||||
await api.approve(row.id);
|
||||
if (row.kingdeeSyncStatus === 'synced')
|
||||
this.$message.warning(
|
||||
'调整已生效,本地已更新;原结算单已推送金蝶,请财务先手工冲销旧应付单后再重新推送。'
|
||||
);
|
||||
else this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '驳回', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: v => (v && v.length <= 200) || '请输入200字以内原因',
|
||||
});
|
||||
await api.returnBill(row.id, value);
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleRepush(row) {
|
||||
await this.$confirm(
|
||||
'请确认金蝶旧应付单已由财务手工冲销,继续将生成全新财务应付单。',
|
||||
'高危操作确认',
|
||||
{ type: 'warning', confirmButtonText: '确认重新推送' }
|
||||
);
|
||||
const { data } = await api.repush(row.id);
|
||||
this.$message.success(`已生成新金蝶应付单:${data?.data || data || '-'}`);
|
||||
this.loadTable();
|
||||
},
|
||||
statusType(status) {
|
||||
return (
|
||||
{ approved: 'success', returned: 'danger', reviewing: 'warning', draft: 'info' }[status] ||
|
||||
'info'
|
||||
);
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settlement-adjustment-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
&__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
.settlement-adjustment-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 760px) {
|
||||
.settlement-adjustment-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
:deep(.settlement-adjustment-page .el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
:deep(.settlement-adjustment-page .el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<basic-container class="reconciliation-page">
|
||||
<section class="reconciliation-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="reconciliation-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-select
|
||||
v-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="reconciliation-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-page__table-panel">
|
||||
<el-tabs v-model="settlementType" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="应付" name="payable" />
|
||||
<el-tab-pane label="应收" name="receivable" />
|
||||
</el-tabs>
|
||||
<div class="reconciliation-page__toolbar">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="hasPermission('transport_reconciliation_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
<div class="reconciliation-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link" type="primary" @click="openView(row)">{{
|
||||
row[column.prop] || '-'
|
||||
}}</el-link>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||||
<el-tag
|
||||
v-else-if="column.prop === 'reconciliationStatusName'"
|
||||
:type="row.reconciliationStatus === 'completed' ? 'success' : 'warning'"
|
||||
>{{ row[column.prop] || '-' }}</el-tag
|
||||
>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="reconciliation-page__links">
|
||||
<el-link type="primary" @click="openView(row)">查看</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_delete') && isEditable(row)"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_complete') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleComplete(row)"
|
||||
>确认</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="reconciliation-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<transport-reconciliation-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:settlement-type="settlementType"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
import { transportReconciliationSearchFields } from '@/option/settlement/transportReconciliationSearch';
|
||||
import { transportReconciliationTableColumns } from '@/option/settlement/transportReconciliationTable';
|
||||
import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
reconciliationNo: '',
|
||||
preSettlementNos: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
matchStatus: '',
|
||||
reconciliationStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'TransportReconciliation',
|
||||
components: { TransportReconciliationEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchFields: transportReconciliationSearchFields,
|
||||
columns: transportReconciliationTableColumns,
|
||||
settlementType: 'payable',
|
||||
loading: false,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await api.getList(this.page.current, this.page.size, {
|
||||
...this.query,
|
||||
settlementType: this.settlementType,
|
||||
});
|
||||
this.rows = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.handleSearch();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleTabChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: null, readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm(`确定删除对账单“${row.reconciliationNo}”吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
});
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleComplete(row) {
|
||||
await this.$confirm('完成后对账单将不可修改,是否继续?', '完成对账', { type: 'warning' });
|
||||
await api.complete(row.id);
|
||||
this.$message.success('对账单确认完成');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleExport() {
|
||||
const { data } = await api.getList(1, 100000, {
|
||||
...this.query,
|
||||
settlementType: this.settlementType,
|
||||
});
|
||||
const exportRows = (data.records || []).map(item => ({
|
||||
对账单号: item.reconciliationNo,
|
||||
付款方: item.payerName,
|
||||
收款方: item.payeeName,
|
||||
项目名称: item.projectName,
|
||||
所属组织: item.deptName,
|
||||
合同编号: item.contractNo,
|
||||
合同名称: item.contractName,
|
||||
结算金额: Number(item.settlementAmount || 0).toFixed(2),
|
||||
对账模式: item.reconciliationModeName,
|
||||
账单总数: item.externalBillCount,
|
||||
匹配数: item.matchedCount,
|
||||
对账状态: item.reconciliationStatusName,
|
||||
创建人: item.createUserName,
|
||||
创建时间: item.createTime,
|
||||
}));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '运输对账');
|
||||
XLSX.writeFile(workbook, `运输对账${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
isEditable(row) {
|
||||
return row.reconciliationStatus === 'unfinished';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reconciliation-page__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.reconciliation-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-input),
|
||||
.reconciliation-page__search :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
.reconciliation-page__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.reconciliation-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.reconciliation-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
.reconciliation-page__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
.reconciliation-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
:deep(.reconciliation-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
.reconciliation-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 760px) {
|
||||
.reconciliation-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user