feat: 开票项目支持关联费用项目拖动排序与级联选择
This commit is contained in:
+304
-91
@@ -8,9 +8,6 @@
|
||||
:data="data"
|
||||
:table-loading="loading"
|
||||
:permission="permissionList"
|
||||
:before-open="beforeOpen"
|
||||
@row-save="rowSave"
|
||||
@row-update="rowUpdate"
|
||||
@row-del="rowDel"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@@ -20,11 +17,21 @@
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
v-if="permission.invoice_item_add"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
@click="openDialog()"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
v-if="permission.invoice_item_edit"
|
||||
type="primary"
|
||||
@click="$refs.crud.rowEdit(scope.row, scope.index)"
|
||||
@click="openDialog(scope.row)"
|
||||
>
|
||||
修改
|
||||
</el-link>
|
||||
@@ -37,24 +44,125 @@
|
||||
</el-link>
|
||||
</template>
|
||||
<template #defaultTaxRate="scope">{{ scope.row.defaultTaxRate }}%</template>
|
||||
<template #defaultTaxRate-form>
|
||||
<el-input
|
||||
v-model="form.defaultTaxRate"
|
||||
inputmode="decimal"
|
||||
maxlength="6"
|
||||
clearable
|
||||
placeholder="请输入默认税率"
|
||||
@input="handleTaxRateInput"
|
||||
>
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</avue-crud>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="900px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="dialogForm" :model="dialog.form" label-width="130px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货物或服务简称" required prop="shortName">
|
||||
<el-input v-model="dialog.form.shortName" maxlength="100" clearable placeholder="请输入货物或服务简称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="税收分类编码" required prop="taxClassificationCode">
|
||||
<el-input v-model="dialog.form.taxClassificationCode" maxlength="30" clearable placeholder="请输入税收分类编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品和服务分类名称" required prop="categoryName">
|
||||
<el-input v-model="dialog.form.categoryName" maxlength="200" clearable placeholder="请输入商品和服务分类名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="默认税率" required prop="defaultTaxRate">
|
||||
<el-input
|
||||
v-model="dialog.form.defaultTaxRate"
|
||||
inputmode="decimal"
|
||||
maxlength="6"
|
||||
clearable
|
||||
placeholder="请输入默认税率"
|
||||
@input="handleTaxRateInput"
|
||||
>
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="关联费用项目">
|
||||
<div class="fee-item-box">
|
||||
<span class="fee-item-tip">可拖动排序,发票冲抵费用项目时,按设置的顺序冲抵费用</span>
|
||||
<el-button type="primary" size="small" @click="addFeeItem">添加</el-button>
|
||||
<table class="fee-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="36"></th>
|
||||
<th width="60">序号</th>
|
||||
<th>费用类型</th>
|
||||
<th>费用项目</th>
|
||||
<th width="60">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in dialog.form.feeItems"
|
||||
:key="index"
|
||||
draggable="true"
|
||||
@dragstart="onFeeDragStart(index, $event)"
|
||||
@dragover.prevent
|
||||
@drop="onFeeDrop(index, $event)"
|
||||
@dragend="onFeeDragEnd"
|
||||
>
|
||||
<td class="fee-item-drag" :class="{ 'is-dragging': feeDragIndex === index }">≡</td>
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>
|
||||
<el-select v-model="item.feeCategory" placeholder="请选择" clearable filterable @change="() => onFeeCategoryChange(item)">
|
||||
<el-option
|
||||
v-for="category in feeCategoryOptions"
|
||||
:key="category.value"
|
||||
:label="category.label"
|
||||
:value="category.value"
|
||||
/>
|
||||
</el-select>
|
||||
</td>
|
||||
<td>
|
||||
<el-select
|
||||
v-model="item.feeItemId"
|
||||
placeholder="请先选择费用类型"
|
||||
:disabled="!item.feeCategory"
|
||||
clearable
|
||||
filterable
|
||||
@change="onFeeItemChange(item)"
|
||||
>
|
||||
<el-option
|
||||
v-for="fee in feeItemsOfCategory(item.feeCategory)"
|
||||
:key="fee.id"
|
||||
:label="fee.name"
|
||||
:value="fee.id"
|
||||
/>
|
||||
</el-select>
|
||||
</td>
|
||||
<td>
|
||||
<el-link type="danger" @click="removeFeeItem(index)">删除</el-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!dialog.form.feeItems.length">
|
||||
<td colspan="5" class="fee-item-empty">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="dialog.saving" @click="saveDialog">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getList, getDetail, submit, remove } from '@/api/base/invoice-item';
|
||||
import { getList as getFeeItemList } from '@/api/base/fee-item';
|
||||
import { getDictionaryAll } from '@/api/system/dictbiz';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
@@ -65,6 +173,16 @@ export default {
|
||||
loading: true,
|
||||
query: {},
|
||||
selectionList: [],
|
||||
feeItemOptions: [],
|
||||
// 费用类型字典选项(fee_category 字典:dictKey-编码A01,dictValue-名称运输)
|
||||
feeCategoryDictOptions: [],
|
||||
feeDragIndex: null,
|
||||
dialog: {
|
||||
visible: false,
|
||||
title: '新增',
|
||||
saving: false,
|
||||
form: this.emptyForm(),
|
||||
},
|
||||
page: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 30, 50, 100, 200],
|
||||
@@ -81,13 +199,11 @@ export default {
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
selection: false,
|
||||
addBtn: true,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
menuWidth: 140,
|
||||
dialogWidth: 900,
|
||||
dialogClickModal: false,
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
searchShow: true,
|
||||
@@ -100,49 +216,22 @@ export default {
|
||||
label: '货物或服务简称',
|
||||
prop: 'shortName',
|
||||
minWidth: 160,
|
||||
span: 12,
|
||||
maxlength: 100,
|
||||
rules: [{ required: true, message: '请输入货物或服务简称', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '税收分类编码',
|
||||
prop: 'taxClassificationCode',
|
||||
minWidth: 190,
|
||||
span: 12,
|
||||
maxlength: 30,
|
||||
rules: [{ required: true, message: '请输入税收分类编码', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '商品和服务分类名称',
|
||||
prop: 'categoryName',
|
||||
minWidth: 220,
|
||||
span: 12,
|
||||
maxlength: 200,
|
||||
rules: [{ required: true, message: '请输入商品和服务分类名称', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '默认税率',
|
||||
prop: 'defaultTaxRate',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
slot: true,
|
||||
minWidth: 120,
|
||||
span: 12,
|
||||
rules: [
|
||||
{ required: true, message: '请输入默认税率', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const text = String(value ?? '');
|
||||
const rate = Number(text);
|
||||
if (!/^\d+(\.\d+)?$/.test(text) || rate < 0 || rate > 100) {
|
||||
callback(new Error('默认税率必须是0到100之间的数字'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
slot: true,
|
||||
},
|
||||
{
|
||||
label: '税收项目',
|
||||
@@ -193,27 +282,131 @@ export default {
|
||||
delBtn: false,
|
||||
};
|
||||
},
|
||||
// 费用类型下拉选项:优先字典(显示"名称/编码"),无字典数据时兜底费用项目库提取
|
||||
feeCategoryOptions() {
|
||||
if (this.feeCategoryDictOptions.length) return this.feeCategoryDictOptions;
|
||||
return [...new Set(this.feeItemOptions.map(fee => fee.feeCategory).filter(Boolean))]
|
||||
.map(category => ({ label: category, value: category }));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
rowSave(row, done, loading) {
|
||||
submit(this.normalizeTaxRateRow(row)).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
done();
|
||||
},
|
||||
() => loading()
|
||||
);
|
||||
emptyForm() {
|
||||
return {
|
||||
id: null,
|
||||
shortName: '',
|
||||
taxClassificationCode: '',
|
||||
categoryName: '',
|
||||
defaultTaxRate: 0,
|
||||
feeItems: [],
|
||||
};
|
||||
},
|
||||
rowUpdate(row, index, done, loading) {
|
||||
submit(this.normalizeTaxRateRow(row)).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
openDialog(row) {
|
||||
this.dialog.form = this.emptyForm();
|
||||
this.dialog.title = row ? '编辑' : '新增';
|
||||
this.dialog.visible = true;
|
||||
this.loadFeeItemOptions();
|
||||
this.initFeeCategoryOptions();
|
||||
if (row) {
|
||||
// 编辑回填:后端 detail 返回关联费用项目(按排序号升序)
|
||||
getDetail(row.id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
this.dialog.form = {
|
||||
...this.emptyForm(),
|
||||
...detail,
|
||||
defaultTaxRate: detail.defaultTaxRate ?? 0,
|
||||
feeItems: (detail.feeItems || []).map(item => ({
|
||||
feeItemId: item.feeItemId,
|
||||
feeItemName: item.feeItemName,
|
||||
feeCategory: item.feeCategory,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
loadFeeItemOptions() {
|
||||
if (this.feeItemOptions.length) return;
|
||||
getFeeItemList(1, 1000, { status: 1 }).then(res => {
|
||||
this.feeItemOptions = (res.data.data?.records) || [];
|
||||
});
|
||||
},
|
||||
// 费用类型字典:code=fee_category(dictKey-编码如A01,dictValue-名称如运输),label 显示"名称/编码"与费用项目页一致
|
||||
initFeeCategoryOptions() {
|
||||
if (this.feeCategoryDictOptions.length) return;
|
||||
getDictionaryAll().then(res => {
|
||||
this.feeCategoryDictOptions = (res.data.data || [])
|
||||
.filter(item => item.code === 'fee_category' && Number(item.parentId) > 0)
|
||||
.sort((first, second) => Number(first.sort || 0) - Number(second.sort || 0))
|
||||
.map(item => ({
|
||||
label: `${item.dictValue}/${item.dictKey}`,
|
||||
value: String(item.dictKey),
|
||||
}));
|
||||
});
|
||||
},
|
||||
// 级联:该费用类型下的费用项目选项
|
||||
feeItemsOfCategory(feeCategory) {
|
||||
if (!feeCategory) return [];
|
||||
return this.feeItemOptions.filter(fee => fee.feeCategory === feeCategory);
|
||||
},
|
||||
// 先选费用类型:清空原费用项目(原项目可能不属于新类型)
|
||||
onFeeCategoryChange(item) {
|
||||
item.feeItemId = null;
|
||||
item.feeItemName = '';
|
||||
},
|
||||
// 后选费用项目:带出名称(费用类型已由上级确定)
|
||||
onFeeItemChange(item) {
|
||||
const fee = this.feeItemsOfCategory(item.feeCategory).find(option => option.id === item.feeItemId);
|
||||
item.feeItemName = fee ? fee.name : '';
|
||||
},
|
||||
addFeeItem() {
|
||||
this.dialog.form.feeItems.push({ feeItemId: null, feeItemName: '', feeCategory: '' });
|
||||
},
|
||||
removeFeeItem(index) {
|
||||
this.dialog.form.feeItems.splice(index, 1);
|
||||
},
|
||||
onFeeDragStart(index, event) {
|
||||
this.feeDragIndex = index;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
onFeeDrop(index) {
|
||||
const from = this.feeDragIndex;
|
||||
if (from === null || from === index) return;
|
||||
const items = this.dialog.form.feeItems;
|
||||
items.splice(index, 0, items.splice(from, 1)[0]);
|
||||
this.feeDragIndex = null;
|
||||
},
|
||||
onFeeDragEnd() {
|
||||
this.feeDragIndex = null;
|
||||
},
|
||||
saveDialog() {
|
||||
const formItem = this.dialog.form;
|
||||
if (!formItem.shortName) return this.$message.warning('请输入货物或服务简称');
|
||||
if (!formItem.taxClassificationCode) return this.$message.warning('请输入税收分类编码');
|
||||
if (!formItem.categoryName) return this.$message.warning('请输入商品和服务分类名称');
|
||||
const rate = Number(formItem.defaultTaxRate);
|
||||
if (!/^\d+(\.\d+)?$/.test(String(formItem.defaultTaxRate ?? '')) || rate < 0 || rate > 100) {
|
||||
return this.$message.warning('默认税率必须是0到100之间的数字');
|
||||
}
|
||||
if (formItem.feeItems.some(item => !item.feeItemId)) {
|
||||
return this.$message.warning('请选择关联费用项目');
|
||||
}
|
||||
this.dialog.saving = true;
|
||||
submit({
|
||||
...formItem,
|
||||
defaultTaxRate: rate,
|
||||
feeItems: formItem.feeItems.map(item => ({
|
||||
feeItemId: item.feeItemId,
|
||||
feeItemName: item.feeItemName,
|
||||
feeCategory: item.feeCategory,
|
||||
})),
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('操作成功');
|
||||
done();
|
||||
},
|
||||
() => loading()
|
||||
);
|
||||
this.dialog.visible = false;
|
||||
this.onLoad(this.page);
|
||||
})
|
||||
.finally(() => {
|
||||
this.dialog.saving = false;
|
||||
});
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定将选择数据删除?', '提示', { type: 'warning' })
|
||||
@@ -223,27 +416,11 @@ export default {
|
||||
this.$message.success('操作成功');
|
||||
});
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
this.form = res.data.data || {};
|
||||
done();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.form = { defaultTaxRate: 0 };
|
||||
done();
|
||||
},
|
||||
handleTaxRateInput(value) {
|
||||
let normalized = String(value || '').replace(/[^\d.]/g, '');
|
||||
normalized = normalized.replace(/(\..*)\./g, '$1');
|
||||
if (normalized.startsWith('.')) normalized = `0${normalized}`;
|
||||
this.form.defaultTaxRate = normalized;
|
||||
},
|
||||
normalizeTaxRateRow(row) {
|
||||
const values = { ...row };
|
||||
values.defaultTaxRate = Number(values.defaultTaxRate);
|
||||
return values;
|
||||
this.dialog.form.defaultTaxRate = normalized;
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = {
|
||||
@@ -292,17 +469,53 @@ export default {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.invoice-item-page :deep(.avue-crud__dialog .el-form-item) {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.invoice-item-page :deep(.avue-crud__dialog .el-form-item__error) {
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.invoice-item-page :deep(.avue-crud__menu .el-link) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.fee-item-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fee-item-tip {
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fee-item-table {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.fee-item-table th,
|
||||
.fee-item-table td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fee-item-table th {
|
||||
background: #f8f8f9;
|
||||
color: #515a6e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fee-item-drag {
|
||||
cursor: move;
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fee-item-drag.is-dragging {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.fee-item-empty {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user