Files
tms-erp-web/src/views/settlement/settlement-adjustment.vue
T

451 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="settlement-adjustment-page">
<section v-show="searchVisible" class="settlement-adjustment-page__search">
<el-form :model="query" label-position="right" label-width="88px" @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-cascader
v-else-if="field.type === 'cascader'"
v-model="query[field.prop]"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
clearable
filterable
style="width: 100%"
placeholder="请选择"
/>
<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
>
<settlement-table-tools
:columns="columnSettingOptions"
v-model:visible-column-keys="visibleColumnKeys"
v-model:table-size="tableSize"
v-model:search-visible="searchVisible"
@refresh="loadTable"
/>
</div>
<el-table
v-loading="loading"
:data="rows"
:size="tableSize"
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 visibleTableColumns"
: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="handleTableLink(row, column)"
>{{ row[column.prop] }}</el-link
><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">{{
row[column.prop] || '-'
}}</el-tag
><el-tag
v-else-if="column.kingdee"
:type="kingdeeStatusType(row.kingdeeSyncStatus)"
class="status-text"
>{{ kingdeeSyncName(row.kingdeeSyncStatus) }}</el-tag
><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span
><span v-else>{{ 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'"
:type="row.kingdeeSyncStatus === 'synced' ? 'danger' : 'primary'"
@click="handleKingdee(row)"
>{{ row.kingdeeSyncStatus === 'synced' ? '重新推送(高危)' : '推送金蝶' }}</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 } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/settlement/settlementAdjustment';
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
import organizationSearch from '@/mixins/organization-search';
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
import { kingdeeSyncStatusOptions } from '@/option/settlement/formalSettlementSearch';
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
import SettlementTableTools from './components/settlement-table-tools.vue';
import { isAdministrator } from '@/utils/auth';
const emptyQuery = () => ({
adjustmentNo: '',
createDateRange: [],
customerName: '',
projectName: '',
deptName: [],
formalSettlementNo: '',
settlementType: '',
approvalStatus: '',
});
export default {
name: 'SettlementAdjustment',
components: { SettlementAdjustmentEditor, SettlementTableTools },
mixins: [organizationSearch],
data: () => ({
ArrowDown,
ArrowUp,
loading: false,
query: emptyQuery(),
searchFields: settlementAdjustmentSearchFields,
searchExpanded: false,
searchVisible: true,
tableSize: 'default',
tableColumns: settlementAdjustmentTableColumns,
visibleColumnKeys: settlementAdjustmentTableColumns.map(column => column.prop),
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);
},
columnSettingOptions() {
return this.tableColumns.map(column => ({
prop: column.prop,
label: column.label,
}));
},
visibleTableColumns() {
return this.tableColumns.filter(column => this.visibleColumnKeys.includes(column.prop));
},
isAdmin() {
return isAdministrator(this.userInfo);
},
},
created() {
this.loadOrganizationOptions();
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, {
// 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'),
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 };
},
// 单据号→本单查看弹窗;结算单号→跳正式结算列表页并自动打开该单详情(页面支持 ?detailNo=)
handleTableLink(row, column) {
if (column.prop === 'formalSettlementNo') {
this.openSettlementDetail(row.formalSettlementNo);
return;
}
this.openView(row);
},
openSettlementDetail(detailNo) {
if (!detailNo) {
this.$message.info('当前记录未关联结算单详情');
return;
}
this.$router.push({ path: '/settlement/formal-settlement', query: { detailNo } });
},
isEditable(row) {
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('审批通过后将更新关联正式结算单,确认继续?', '提示');
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
if (this.loading) return;
this.loading = true;
try {
const res = await api.approve(row.id);
// data=false:自动同步金蝶失败;data=true 且 msg 带已推送场景的手工冲销提示
if (res.data.data === true || res.data.data === false) this.$message.warning(res.data.msg);
else this.$message.success(res.data.msg || '审批通过');
await this.loadTable();
} finally {
this.loading = false;
}
},
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 handleKingdee(row) {
if (row.kingdeeSyncStatus === 'synced') return this.handleRepush(row);
return this.handleSyncKingdee(row);
},
async handleSyncKingdee(row) {
const res = await api.syncKingdee(row.id);
this.$message.success(`已推送金蝶:${res.data.data || '-'}`);
this.loadTable();
},
async handleRepush(row) {
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'
);
},
kingdeeStatusType(status) {
return { synced: 'success', failed: 'danger' }[status] || 'info';
},
kingdeeSyncName(value) {
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
},
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.04);
}
&__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;
}
&__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>