修复付款申请 + 收款/付款结算单搜索栏 #16

Merged
b2894lxlx merged 3 commits from fix/20261110-修复结算单页面-web into master 2026-11-10 16:16:05 +08:00
7 changed files with 413 additions and 298 deletions
+7 -119
View File
@@ -1,14 +1,11 @@
/** /**
* 付款申请列表(avue-crud)配置 * 付款申请列表(avue-crud)配置
* ------------------------------------------------------------------ * ------------------------------------------------------------------
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 / * 列表页与其它模块统一使用 avue-crud,头部按钮(刷新 / 列显隐 / 表格卡片切换)
* 表格卡片切换)由 avue-crud 自带,不做二次封装。 * 由 avue-crud 自带,不做二次封装。
* *
* 搜索项与表格列 label 不一致的(如表格列「收款方」/ 搜索项「客户名称」), * 搜索栏与应收/应付明细等页面统一为页面级手写搜索区(见 payment-application.vue),
* 单独定义一个 hide 的搜索列,并用 searchProp 映射到后端字段。 * 此处只保留表格列配置。
*
* ⚠️ avue 的 searchOrder 为降序排序,值越大越靠前(见 header-search2.js:
* `list.sort((a, b) => (b.searchOrder || 0) - (a.searchOrder || 0))`)。
*/ */
const textFormatter = (row, value) => const textFormatter = (row, value) =>
@@ -16,46 +13,10 @@ const textFormatter = (row, value) =>
const moneyFormatter = (row, value) => Number(value || 0).toFixed(2); const moneyFormatter = (row, value) => Number(value || 0).toFixed(2);
// 仅用于搜索、不在表格中出现的列 export const paymentApplicationOption = () => ({
const searchOnlyColumn = ({ label, prop, searchOrder, searchProp, dicData }) => {
const column = {
label,
prop,
search: true,
searchOrder,
searchPlaceholder: '请输入',
hide: true,
showColumn: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
};
if (searchProp) column.searchProp = searchProp;
if (dicData) {
column.type = 'select';
column.searchPlaceholder = '请选择';
column.dicData = dicData;
}
return column;
};
export const paymentApplicationOption = ({
paymentTypeOptions = [],
approvalStatusOptions = [],
kingdeeStatusOptions = [],
} = {}) => ({
height: 'auto', height: 'auto',
calcHeight: 32, calcHeight: 32,
tip: false, tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
searchShow: true,
searchIcon: true,
searchLabelWidth: 88,
// 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行
searchMenuSpan: 4,
searchIndex: 4,
searchMenuPosition: 'right',
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
@@ -73,84 +34,11 @@ export const paymentApplicationOption = ({
menuFixed: 'right', menuFixed: 'right',
dialogClickModal: false, dialogClickModal: false,
column: [ column: [
// ---------- 搜索项(前 4 项默认展示,其余折叠;searchOrder 越大越靠前) ----------
{
label: '单据号',
prop: 'paymentNo',
slot: true,
search: true,
searchSpan: 5,
searchOrder: 9,
searchPlaceholder: '请输入',
minWidth: 150,
},
{
label: '日期',
prop: 'applyDateRange',
type: 'daterange',
searchType: 'daterange',
search: true,
searchSpan: 5,
searchOrder: 8,
hide: true,
showColumn: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
...searchOnlyColumn({
label: '客户名称',
prop: 'payeeNameSearch',
searchOrder: 7,
searchProp: 'payeeName',
}),
searchSpan: 5,
},
{
...searchOnlyColumn({
label: '项目名称',
prop: 'projectNameSearch',
searchOrder: 6,
searchProp: 'projectName',
}),
searchSpan: 5,
},
searchOnlyColumn({
label: '关联结算单',
prop: 'settlementNo',
searchOrder: 4,
}),
searchOnlyColumn({
label: '付款类型',
prop: 'paymentType',
searchOrder: 3,
dicData: paymentTypeOptions,
}),
searchOnlyColumn({
label: '审核状态',
prop: 'approvalStatus',
searchOrder: 2,
dicData: approvalStatusOptions,
}),
searchOnlyColumn({
label: '金蝶单据状态',
prop: 'kingdeeStatus',
searchOrder: 1,
dicData: kingdeeStatusOptions,
}),
// ---------- 表格列 ---------- // ---------- 表格列 ----------
{ label: '单据号', prop: 'paymentNo', slot: true, minWidth: 150 },
{ label: '付款类型', prop: 'paymentTypeName', minWidth: 110, formatter: textFormatter }, { label: '付款类型', prop: 'paymentTypeName', minWidth: 110, formatter: textFormatter },
{ label: '所属项目', prop: 'projectName', minWidth: 140, formatter: textFormatter }, { label: '所属项目', prop: 'projectName', minWidth: 140, formatter: textFormatter },
{ { label: '所属组织', prop: 'deptName', minWidth: 120, formatter: textFormatter },
label: '所属组织',
prop: 'deptName',
search: true,
searchOrder: 5,
searchslot: true,
minWidth: 120,
formatter: textFormatter,
},
{ label: '合同编号', prop: 'contractNo', minWidth: 130, formatter: textFormatter }, { label: '合同编号', prop: 'contractNo', minWidth: 130, formatter: textFormatter },
{ label: '合同名称', prop: 'contractName', minWidth: 140, formatter: textFormatter }, { label: '合同名称', prop: 'contractName', minWidth: 140, formatter: textFormatter },
{ label: '付款方', prop: 'payerName', minWidth: 130, formatter: textFormatter }, { label: '付款方', prop: 'payerName', minWidth: 130, formatter: textFormatter },
+20 -105
View File
@@ -1,21 +1,18 @@
/** /**
* 正式结算单列表(avue-crud)配置 * 正式结算单列表(avue-crud)配置
* ------------------------------------------------------------------ * ------------------------------------------------------------------
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 / * 列表页与其它模块统一使用 avue-crud,头部按钮(刷新 / 列显隐 / 显示方式)
* 显示方式)由 avue-crud 自带,不做二次封装。 * 由 avue-crud 自带,不做二次封装。
*
* 搜索栏与应收/应付明细页统一为页面级手写搜索区(见 formal-settlement.vue),
* 此处只保留表格列配置。
* *
* 表格列复用 formalSettlementTable.js 的 formalSettlementTableColumns(与弹窗内 * 表格列复用 formalSettlementTable.js 的 formalSettlementTableColumns(与弹窗内
* 明细表共用同一份列定义),此处只补充 avue 需要的展示/搜索属性,避免两处维护。 * 明细表共用同一份列定义),此处只补充 avue 需要的展示属性,避免两处维护。
*
* ⚠️ searchOrder 为降序排序,值越大越靠前。
* ⚠️ 仅用于搜索的列必须 hide + showColumn: false,否则会混进「列显隐」抽屉。
* ⚠️ column 数组顺序 = 表格列顺序,搜索专用列统一放在数组最前面。
*/ */
import { formatSealTypes } from '@/utils/settlement-seal'; import { formatSealTypes } from '@/utils/settlement-seal';
import { import {
approvalStatusOptions,
invoiceStatusOptions,
kingdeeSyncStatusOptions, kingdeeSyncStatusOptions,
kingdeeWriteOffStatusOptions, kingdeeWriteOffStatusOptions,
paymentStatusOptions, paymentStatusOptions,
@@ -34,47 +31,9 @@ const moneyFormatter = (row, value) =>
const pickLabel = (options, value) => const pickLabel = (options, value) =>
options.find(item => item.value === value)?.label || value || '-'; options.find(item => item.value === value)?.label || value || '-';
const searchOnlyColumn = ({ label, prop, searchOrder, type = 'input', dicData }) => { /** 表格列的 avue 增量配置:插槽、覆盖宽度等 */
const column = {
label,
prop,
search: true,
searchOrder,
searchSpan: 5,
hide: true,
showColumn: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
};
if (type === 'select') {
column.type = 'select';
column.searchPlaceholder = '请选择';
column.dicData = dicData || [];
} else if (type === 'daterange') {
column.type = 'daterange';
column.searchType = 'daterange';
column.searchPlaceholder = '请选择';
} else {
column.searchPlaceholder = '请输入';
}
return column;
};
/** 表格列的 avue 增量配置:搜索项、插槽、覆盖宽度等 */
const COLUMN_EXTRA = { const COLUMN_EXTRA = {
formalSettlementNo: { formalSettlementNo: { slot: true },
slot: true,
search: true,
searchSpan: 5,
searchOrder: 12,
searchPlaceholder: '请输入',
},
payerName: { search: true, searchSpan: 5, searchOrder: 7, searchPlaceholder: '请输入' },
payeeName: { search: true, searchSpan: 5, searchOrder: 6, searchPlaceholder: '请输入' },
projectName: { search: true, searchSpan: 5, searchOrder: 11, searchPlaceholder: '请输入' },
deptName: { search: true, searchslot: true, searchSpan: 5, searchOrder: 10 },
contractNo: { search: true, searchSpan: 5, searchOrder: 9, searchPlaceholder: '请输入' },
sealTypes: { formatter: sealTypesFormatter }, sealTypes: { formatter: sealTypesFormatter },
invoiceStatusName: { slot: true }, invoiceStatusName: { slot: true },
paymentStatusName: { slot: true }, paymentStatusName: { slot: true },
@@ -82,27 +41,27 @@ const COLUMN_EXTRA = {
adjustFlag: { adjustFlag: {
formatter: row => row.adjustStatusName || (row.originFormalSettlementNo ? '调整后单' : '-'), formatter: row => row.adjustStatusName || (row.originFormalSettlementNo ? '调整后单' : '-'),
}, },
kingdeeSyncStatus: { kingdeeSyncStatus: { slot: true },
slot: true,
search: true,
type: 'select',
dicData: kingdeeSyncStatusOptions,
searchSpan: 5,
searchOrder: 2,
searchPlaceholder: '请选择',
},
kingdeeLockStatus: { slot: true }, kingdeeLockStatus: { slot: true },
kingdeeWriteOffStatus: { slot: true }, kingdeeWriteOffStatus: { slot: true },
kingdeeAssignStatus: { slot: true }, kingdeeAssignStatus: { slot: true },
createTime: { width: 180 }, createTime: { width: 180 },
}; };
/** 收款/付款结算单按结算类型展示各自标签 */
const PAYMENT_FIELD_LABELS = {
paidAmount: { payable: '已付合计(含预付)', receivable: '已收合计(含预付)' },
paymentStatusName: { payable: '付款状态', receivable: '收款状态' },
};
// 已指定应付仅付款结算单(先票后款 A302-6)有,收款结算单不展示 // 已指定应付仅付款结算单(先票后款 A302-6)有,收款结算单不展示
const buildTableColumns = settlementType => const buildTableColumns = settlementType =>
formalSettlementTableColumns formalSettlementTableColumns
.filter(column => !(settlementType === 'receivable' && column.prop === 'kingdeeAssignStatus')) .filter(column => !(settlementType === 'receivable' && column.prop === 'kingdeeAssignStatus'))
.map(column => { .map(column => {
const extra = { ...(COLUMN_EXTRA[column.prop] || {}) }; const extra = { ...(COLUMN_EXTRA[column.prop] || {}) };
const label = PAYMENT_FIELD_LABELS[column.prop]?.[settlementType];
if (label) extra.label = label;
if (column.money) { if (column.money) {
extra.formatter = moneyFormatter; extra.formatter = moneyFormatter;
extra.align = 'right'; extra.align = 'right';
@@ -118,15 +77,6 @@ export const formalSettlementOption = (settlementType = '') => ({
height: 'auto', height: 'auto',
calcHeight: 32, calcHeight: 32,
tip: false, tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
searchShow: true,
searchIcon: true,
searchLabelWidth: 100,
// 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行
searchMenuSpan: 4,
searchIndex: 4,
searchMenuPosition: 'right',
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
@@ -141,45 +91,10 @@ export const formalSettlementOption = (settlementType = '') => ({
editBtn: false, editBtn: false,
menuWidth: 320, menuWidth: 320,
menuFixed: 'right', menuFixed: 'right',
column: [ column: [...buildTableColumns(settlementType)],
// ---------- 仅搜索列 ----------
searchOnlyColumn({
label: '预付结算单号',
prop: 'preSettlementNo',
searchOrder: 8,
}),
searchOnlyColumn({
label: '发票状态',
prop: 'invoiceStatus',
searchOrder: 5,
type: 'select',
dicData: invoiceStatusOptions,
}),
searchOnlyColumn({
label: '收付款状态',
prop: 'paymentStatus',
searchOrder: 4,
type: 'select',
dicData: paymentStatusOptions,
}),
searchOnlyColumn({
label: '审核状态',
prop: 'approvalStatus',
searchOrder: 3,
type: 'select',
dicData: approvalStatusOptions,
}),
searchOnlyColumn({
label: '生成日期',
prop: 'createDateRange',
searchOrder: 1,
type: 'daterange',
}),
// ---------- 表格列(部分兼搜索项) ----------
...buildTableColumns(settlementType),
],
}); });
export const kingdeeSyncName = value => pickLabel(kingdeeSyncStatusOptions, value); export const kingdeeSyncName = value => pickLabel(kingdeeSyncStatusOptions, value);
export const kingdeeWriteOffName = value => pickLabel(kingdeeWriteOffStatusOptions, value); export const kingdeeWriteOffName = value => pickLabel(kingdeeWriteOffStatusOptions, value);
export const paymentStatusName = value => pickLabel(paymentStatusOptions, value); export const paymentStatusName = (value, settlementType) =>
pickLabel(paymentStatusOptions(settlementType), value);
+18 -37
View File
@@ -6,27 +6,28 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' }, { label: '已作废', value: 'voided' },
]; ];
export const invoiceStatusOptions = [ /** 发票状态选项按结算类型区分:付款结算单=收票方向,收款结算单=开票方向 */
{ label: '未开/收票', value: 'unreceived' }, export const invoiceStatusOptions = (settlementType = '') => {
{ label: '部分开/收票', value: 'partial' }, const received = settlementType === 'payable';
{ label: '已开/收票', value: 'completed' }, return [
{ label: received ? '未收票' : '未开票', value: 'unreceived' },
{ label: received ? '部分收票' : '部分开票', value: 'partial' },
{ label: received ? '已收票' : '已开票', value: 'completed' },
]; ];
export const invoiceStatusName = (value, settlementType) => {
const payable = settlementType === 'payable';
const labels = {
unreceived: payable ? '未收票' : '未开票',
partial: payable ? '部分收票' : '部分开票',
completed: payable ? '已收票' : '已开票',
};
return labels[value] || value || '-';
}; };
export const paymentStatusOptions = [ export const invoiceStatusName = (value, settlementType) =>
{ label: '未收/付款', value: 'unpaid' }, invoiceStatusOptions(settlementType).find(item => item.value === value)?.label || value || '-';
{ label: '部分收/付款', value: 'partial' },
{ label: '已收/付款', value: 'paid' }, /** 收付款状态选项按结算类型区分:付款结算单=付款方向,收款结算单=收款方向 */
export const paymentStatusOptions = (settlementType = '') => {
const suffix = settlementType === 'payable' ? '付款' : '收款';
return [
{ label: `未${suffix}`, value: 'unpaid' },
{ label: `部分${suffix}`, value: 'partial' },
{ label: `已${suffix}`, value: 'paid' },
]; ];
};
export const kingdeeSyncStatusOptions = [ export const kingdeeSyncStatusOptions = [
{ label: '未同步', value: 'unsynced' }, { label: '未同步', value: 'unsynced' },
@@ -38,23 +39,3 @@ export const kingdeeWriteOffStatusOptions = [
{ label: '未冲销', value: 'unwrittenoff' }, { label: '未冲销', value: 'unwrittenoff' },
{ label: '已冲销', value: 'writtenoff' }, { label: '已冲销', value: 'writtenoff' },
]; ];
export const formalSettlementSearchFields = [
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '项目', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '付款方', prop: 'payerName', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' },
{ label: '发票状态', prop: 'invoiceStatus', type: 'select', options: invoiceStatusOptions },
{ label: '收付款状态', prop: 'paymentStatus', type: 'select', options: paymentStatusOptions },
{ label: '审核状态', prop: 'approvalStatus', type: 'select', options: approvalStatusOptions },
{
label: '金蝶单据状态',
prop: 'kingdeeSyncStatus',
type: 'select',
options: kingdeeSyncStatusOptions,
},
{ label: '生成日期', prop: 'createDateRange', type: 'daterange' },
];
@@ -1103,6 +1103,8 @@ export default {
); );
this.$nextTick(() => { this.$nextTick(() => {
this.amountSyncing = false; this.amountSyncing = false;
// 程序化赋值不触发校验,需手动重校验以清除/刷新残留错误
this.$refs.formRef?.validateField?.('appliedAmount').catch(() => {});
}); });
}, },
'form.appliedAmount'(value) { 'form.appliedAmount'(value) {
@@ -1132,6 +1134,7 @@ export default {
); );
this.$nextTick(() => { this.$nextTick(() => {
this.amountSyncing = false; this.amountSyncing = false;
this.$refs.formRef?.validateField?.('appliedAmount').catch(() => {});
}); });
}, },
}, },
+149 -17
View File
@@ -1,13 +1,61 @@
<template> <template>
<basic-container class="payment-page"> <basic-container class="payment-page">
<section class="payment-search">
<el-form :model="searchForm" label-position="right" label-width="88px" @submit.prevent>
<div class="payment-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="searchForm[field.prop]"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="年/月/日"
end-placeholder="年/月/日"
/>
<el-select
v-else-if="field.type === 'select'"
v-model="searchForm[field.prop]"
clearable
placeholder="请选择"
>
<el-option
v-for="item in field.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="searchForm[field.prop]"
:options="organizationSearchTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择 所属组织"
/>
<el-input v-else v-model="searchForm[field.prop]" clearable placeholder="请输入" />
</el-form-item>
</template>
<div class="payment-search__actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
{{ searchExpanded ? '收起' : '展开' }}
</el-button>
</div>
</div>
</el-form>
</section>
<avue-crud <avue-crud
ref="crud" ref="crud"
:option="option" :option="option"
:table-loading="loading" :table-loading="loading"
:data="rows" :data="rows"
v-model:page="page" v-model:page="page"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange" @selection-change="selectionChange"
@current-change="currentChange" @current-change="currentChange"
@size-change="sizeChange" @size-change="sizeChange"
@@ -174,6 +222,7 @@
</template> </template>
<script> <script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication'; import * as api from '@/api/payment/paymentApplication';
import { paymentApplicationOption } from '@/option/payment/paymentApplication'; import { paymentApplicationOption } from '@/option/payment/paymentApplication';
@@ -190,6 +239,8 @@ export default {
mixins: [organizationSearch], mixins: [organizationSearch],
data() { data() {
return { return {
ArrowDown,
ArrowUp,
rows: [], rows: [],
selection: [], selection: [],
loading: false, loading: false,
@@ -197,6 +248,9 @@ export default {
syncLoading: false, syncLoading: false,
resultSyncing: false, resultSyncing: false,
query: {}, query: {},
// 手写搜索区(与应收/应付明细同款):模型与展开状态
searchForm: {},
searchExpanded: false,
option: {}, option: {},
page: { page: {
currentPage: 1, currentPage: 1,
@@ -209,13 +263,37 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission']), ...mapGetters(['permission']),
// 搜索字段与应收/应付明细同结构;下拉选项沿用 api 常量
searchFields() {
return [
{ label: '单据号', prop: 'paymentNo', type: 'input' },
{ label: '日期', prop: 'applyDateRange', type: 'daterange' },
{ label: '客户名称', prop: 'payeeName', type: 'input' },
{ label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '关联结算单', prop: 'settlementNo', type: 'input' },
{ label: '付款类型', prop: 'paymentType', type: 'select', options: api.paymentTypeOptions },
{
label: '审核状态',
prop: 'approvalStatus',
type: 'select',
options: api.approvalStatusOptions,
},
{
label: '金蝶单据状态',
prop: 'kingdeeStatus',
type: 'select',
options: api.kingdeeStatusOptions,
},
];
},
// 默认折叠只展示一行 4 个条件
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
}, },
created() { created() {
this.option = paymentApplicationOption({ this.option = paymentApplicationOption();
paymentTypeOptions: api.paymentTypeOptions,
approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
});
this.loadOrganizationOptions(); this.loadOrganizationOptions();
}, },
mounted() { mounted() {
@@ -271,26 +349,30 @@ export default {
this.$refs.crud?.toggleSelection?.(); this.$refs.crud?.toggleSelection?.();
}); });
}, },
searchChange(params, done) { // 搜索提交:组织级联路径转名称、日期区间拆成起止参数(与原 avue searchChange 同规则)
// 所属组织级联返回 id 路径数组,转成部门名称给后端 buildSearchParams() {
const params = { ...this.searchForm };
this.normalizeOrganizationSearch(params, 'deptName'); this.normalizeOrganizationSearch(params, 'deptName');
// 日期区间拆成后端需要的起止字段
const range = params.applyDateRange || []; const range = params.applyDateRange || [];
delete params.applyDateRange; delete params.applyDateRange;
this.query = { return { ...params, applyStartDate: range[0] || '', applyEndDate: range[1] || '' };
...params, },
applyStartDate: range[0] || '', handleSearch() {
applyEndDate: range[1] || '', this.query = this.buildSearchParams();
};
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page);
done();
}, },
searchReset() { handleReset() {
this.searchForm = {};
this.query = {}; this.query = {};
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page);
}, },
toggleSearch() {
this.searchExpanded = !this.searchExpanded;
// 搜索区在 avue 外部,高度变化 avue 感知不到,手动触发表格高度重算,保证分页在一屏内
this.$nextTick(() => this.$refs.crud?.getTableHeight?.());
},
currentChange(currentPage) { currentChange(currentPage) {
this.page.currentPage = currentPage; this.page.currentPage = currentPage;
}, },
@@ -544,6 +626,56 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 搜索区与应收/应付明细页(settlement-detail-page__search)同款样式
.payment-search {
margin-bottom: 8px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
.payment-search__grid {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 8px 24px;
align-items: start;
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor) {
width: 100%;
}
}
.payment-search__actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
@media screen and (max-width: 1200px) {
.payment-search__grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
}
}
@media screen and (max-width: 760px) {
.payment-search__grid {
grid-template-columns: 1fr;
}
}
.payment-page :deep(.el-table) { .payment-page :deep(.el-table) {
--el-table-border-color: #eff1f7; --el-table-border-color: #eff1f7;
} }
@@ -5,8 +5,6 @@
:table-loading="loading" :table-loading="loading"
:data="rows" :data="rows"
v-model:page="crudPage" v-model:page="crudPage"
@search-change="(params, done) => $emit('search-change', params, done)"
@search-reset="$emit('search-reset')"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@refresh-change="$emit('refresh')" @refresh-change="$emit('refresh')"
@on-load="page => $emit('load', page)" @on-load="page => $emit('load', page)"
@@ -61,7 +59,9 @@
}}</span> }}</span>
</template> </template>
<template #paymentStatusName="{ row }">{{ paymentName(row.paymentStatus) }}</template> <template #paymentStatusName="{ row }">{{
paymentName(row.paymentStatus, row.settlementType)
}}</template>
<template #approvalStatusName="{ row }"> <template #approvalStatusName="{ row }">
<el-tag :type="statusType(row.approvalStatus)" class="status-text">{{ <el-tag :type="statusType(row.approvalStatus)" class="status-text">{{
@@ -154,8 +154,6 @@ export default {
'load', 'load',
'action', 'action',
'update:selection', 'update:selection',
'search-change',
'search-reset',
], ],
data() { data() {
return { return {
@@ -181,6 +179,10 @@ export default {
}, },
}, },
methods: { methods: {
/** 搜索区展开/收起后由父级调用:avue 表格是固定像素高度,需手动触发了重新计算,否则分页会被推出视口 */
recalcTableHeight() {
this.$nextTick(() => this.$refs.crud?.getTableHeight?.());
},
handleSelectionChange(selection) { handleSelectionChange(selection) {
this.selection = selection; this.selection = selection;
this.$emit('update:selection', selection); this.$emit('update:selection', selection);
@@ -236,8 +238,8 @@ export default {
invoiceName(value, settlementType) { invoiceName(value, settlementType) {
return invoiceStatusName(value, settlementType); return invoiceStatusName(value, settlementType);
}, },
paymentName(value) { paymentName(value, settlementType) {
return paymentStatusName(value); return paymentStatusName(value, settlementType);
}, },
kingdeeSyncName(value) { kingdeeSyncName(value) {
return kingdeeSyncName(value); return kingdeeSyncName(value);
+205 -11
View File
@@ -1,5 +1,55 @@
<template> <template>
<basic-container class="formal-page"> <basic-container class="formal-page">
<section class="formal-search">
<el-form :model="searchForm" label-position="right" label-width="88px" @submit.prevent>
<div class="formal-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="searchForm[field.prop]"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="年/月/日"
end-placeholder="年/月/日"
/>
<el-select
v-else-if="field.type === 'select'"
v-model="searchForm[field.prop]"
clearable
placeholder="请选择"
>
<el-option
v-for="item in field.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="searchForm[field.prop]"
:options="organizationSearchTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择 所属组织"
/>
<el-input v-else v-model="searchForm[field.prop]" clearable placeholder="请输入" />
</el-form-item>
</template>
<div class="formal-search__actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
{{ searchExpanded ? '收起' : '展开' }}
</el-button>
</div>
</div>
</el-form>
</section>
<el-tabs <el-tabs
v-if="!lockedSettlementType" v-if="!lockedSettlementType"
type="border-card" type="border-card"
@@ -10,6 +60,7 @@
<formal-settlement-table-panel <formal-settlement-table-panel
v-if="activeSettlementType === 'payable'" v-if="activeSettlementType === 'payable'"
key="payable" key="payable"
ref="panel"
v-bind="tablePanelProps" v-bind="tablePanelProps"
v-on="tablePanelListeners" v-on="tablePanelListeners"
/> />
@@ -18,6 +69,7 @@
<formal-settlement-table-panel <formal-settlement-table-panel
v-if="activeSettlementType === 'receivable'" v-if="activeSettlementType === 'receivable'"
key="receivable" key="receivable"
ref="panel"
v-bind="tablePanelProps" v-bind="tablePanelProps"
v-on="tablePanelListeners" v-on="tablePanelListeners"
/> />
@@ -26,6 +78,7 @@
<formal-settlement-table-panel <formal-settlement-table-panel
v-else v-else
key="locked" key="locked"
ref="panel"
v-bind="tablePanelProps" v-bind="tablePanelProps"
v-on="tablePanelListeners" v-on="tablePanelListeners"
/> />
@@ -222,6 +275,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { createSettlementTransfer } from '@/utils/settlement-transfer'; import { createSettlementTransfer } from '@/utils/settlement-transfer';
import { import {
hasSettlementPermission, hasSettlementPermission,
@@ -234,6 +288,13 @@ import * as api from '@/api/settlement/formalSettlement';
import * as paymentApi from '@/api/payment/paymentApplication'; import * as paymentApi from '@/api/payment/paymentApplication';
import * as invoiceReceiptApi from '@/api/payment/invoiceReceipt'; import * as invoiceReceiptApi from '@/api/payment/invoiceReceipt';
import { formalSettlementOption, kingdeeSyncName } from '@/option/settlement/formalSettlement'; import { formalSettlementOption, kingdeeSyncName } from '@/option/settlement/formalSettlement';
import {
approvalStatusOptions,
invoiceStatusOptions,
kingdeeSyncStatusOptions,
paymentStatusOptions,
} from '@/option/settlement/formalSettlementSearch';
import { getDictionary } from '@/api/system/dictbiz';
import organizationSearch from '@/mixins/organization-search'; import organizationSearch from '@/mixins/organization-search';
import FormalSettlementEditor from './components/formal-settlement-editor.vue'; import FormalSettlementEditor from './components/formal-settlement-editor.vue';
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue'; import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
@@ -256,7 +317,16 @@ export default {
}, },
data() { data() {
return { return {
ArrowDown,
ArrowUp,
query: {}, query: {},
// 手写搜索区(与应收/应付明细同款):模型与展开状态
searchForm: {},
searchExpanded: false,
// 发票状态/收付款状态下拉选项:来自数据字典(按结算类型区分收/付方向),
// 字典未配置时回退前端默认文案(见 loadStatusDicts)
invoiceStatusDict: [],
paymentStatusDict: [],
activeSettlementType: 'payable', activeSettlementType: 'payable',
rows: [], rows: [],
selection: [], selection: [],
@@ -289,6 +359,47 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
// 搜索字段与应收/应付明细同结构;收付款/发票状态选项随结算类型区分
searchFields() {
return [
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
{ label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '付款方', prop: 'payerName', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' },
{
label: '发票状态',
prop: 'invoiceStatus',
type: 'select',
options: this.invoiceStatusDict,
},
{
label: this.activeSettlementType === 'payable' ? '付款状态' : '收款状态',
prop: 'paymentStatus',
type: 'select',
options: this.paymentStatusDict,
},
{
label: '审核状态',
prop: 'approvalStatus',
type: 'select',
options: approvalStatusOptions,
},
{
label: '金蝶单据状态',
prop: 'kingdeeSyncStatus',
type: 'select',
options: kingdeeSyncStatusOptions,
},
{ label: '生成日期', prop: 'createDateRange', type: 'daterange' },
];
},
// 默认折叠只展示一行 4 个条件
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
// 列随结算类型切换重建:收款结算单不展示「已指定应付」 // 列随结算类型切换重建:收款结算单不展示「已指定应付」
option() { option() {
return formalSettlementOption(this.activeSettlementType); return formalSettlementOption(this.activeSettlementType);
@@ -330,8 +441,6 @@ export default {
refresh: () => this.onLoad(this.page), refresh: () => this.onLoad(this.page),
load: this.onLoad, load: this.onLoad,
action: this.onAction, action: this.onAction,
'search-change': this.searchChange,
'search-reset': this.searchReset,
'update:selection': selection => { 'update:selection': selection => {
this.selection = selection; this.selection = selection;
}, },
@@ -353,6 +462,8 @@ export default {
activeSettlementType() { activeSettlementType() {
// 切换结算类型后由新挂载的 avue-crud 触发 on-load,此处只重置分页,避免重复请求 // 切换结算类型后由新挂载的 avue-crud 触发 on-load,此处只重置分页,避免重复请求
this.page.currentPage = 1; this.page.currentPage = 1;
// 收/付方向的字典文案不同,切 tab 后重拉
this.loadStatusDicts();
}, },
'$route.query.detailNo'(detailNo) { '$route.query.detailNo'(detailNo) {
if (detailNo) this.openRouteDetail(detailNo); if (detailNo) this.openRouteDetail(detailNo);
@@ -360,6 +471,7 @@ export default {
}, },
created() { created() {
this.loadOrganizationOptions(); this.loadOrganizationOptions();
this.loadStatusDicts();
}, },
mounted() { mounted() {
this.isPageActive = true; this.isPageActive = true;
@@ -402,25 +514,57 @@ export default {
this.loading = false; this.loading = false;
} }
}, },
searchChange(params, done) { /**
// 所属组织级联返回 id 路径,转成组织名称传给后端 * 发票状态/收付款状态下拉选项从字典加载(dictKey=后端状态值,dictValue=文案)。
* 收/付两个方向文案不同,按结算类型区分字典编码:
* invoice_status_payable / invoice_status_receivable
* payment_status_payable / payment_status_receivable
* 字典未配置或为空时回退前端默认文案,保证页面可用。
*/
loadStatusDicts() {
const type = this.activeSettlementType;
const fallback = {
invoice: invoiceStatusOptions(type),
payment: paymentStatusOptions(type),
};
const pick = res =>
(res?.data?.data || [])
.map(item => ({ label: item.dictValue, value: String(item.dictKey) }))
.filter(item => item.label && item.value);
Promise.all([
getDictionary({ code: `invoice_status_${type}` }).catch(() => null),
getDictionary({ code: `payment_status_${type}` }).catch(() => null),
]).then(([invoiceRes, paymentRes]) => {
const invoice = pick(invoiceRes);
const payment = pick(paymentRes);
this.invoiceStatusDict = invoice.length ? invoice : fallback.invoice;
this.paymentStatusDict = payment.length ? payment : fallback.payment;
});
},
// 搜索提交:组织级联路径转名称、日期区间拆成起止参数(与原 avue searchChange 同规则)
buildSearchParams() {
const params = { ...this.searchForm };
this.normalizeOrganizationSearch(params, 'deptName'); this.normalizeOrganizationSearch(params, 'deptName');
const range = params.createDateRange || []; const range = params.createDateRange || [];
delete params.createDateRange; delete params.createDateRange;
this.query = { return { ...params, createStartDate: range[0] || '', createEndDate: range[1] || '' };
...params, },
createStartDate: range[0] || '', handleSearch() {
createEndDate: range[1] || '', this.query = this.buildSearchParams();
};
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page);
done();
}, },
searchReset() { handleReset() {
this.searchForm = {};
this.query = {}; this.query = {};
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page);
}, },
toggleSearch() {
this.searchExpanded = !this.searchExpanded;
// 搜索区在 avue 外部,高度变化 avue 感知不到,需手动触发表格高度重算,保证分页在一屏内
this.$refs.panel?.recalcTableHeight();
},
onAction({ type, row }) { onAction({ type, row }) {
const handlers = { const handlers = {
view: () => this.openView(row), view: () => this.openView(row),
@@ -877,6 +1021,56 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 搜索区与应收/应付明细页(settlement-detail-page__search)同款样式
.formal-search {
margin-bottom: 8px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
.formal-search__grid {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 8px 24px;
align-items: start;
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor) {
width: 100%;
}
}
.formal-search__actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
@media screen and (max-width: 1200px) {
.formal-search__grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
}
}
@media screen and (max-width: 760px) {
.formal-search__grid {
grid-template-columns: 1fr;
}
}
.formal-page { .formal-page {
:deep(.el-table) { :deep(.el-table) {
--el-table-border-color: #eff1f7; --el-table-border-color: #eff1f7;