Files
tms-erp-web/src/views/base/fee-item.vue
T

589 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="fee-item-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button type="primary" plain v-if="permission.fee_item_export" @click="handleExport">
<el-icon class="el-icon--left"><Download /></el-icon>批量导出
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #feeCategory="{ row }">
{{ formatFeeCategory(row.feeCategory) }}
</template>
<template #feeCategory-form>
<el-select
v-model="form.feeCategory"
class="fee-item-form-control"
clearable
filterable
placeholder="请选择费用类型"
@change="handleFeeCategoryChange"
>
<el-option
v-for="item in feeCategoryOptions"
:key="item.id || item.dictKey"
:label="`${item.dictValue}/${item.dictKey}`"
:value="item.dictKey"
/>
</el-select>
</template>
<template #taxRate="{ row }">{{ formatTaxRate(row.taxRate) }}</template>
<template #taxRate-form>
<el-input
v-model="form.taxRate"
inputmode="decimal"
maxlength="6"
clearable
placeholder="请输入税率"
@input="handleTaxRateInput"
>
<template #append>%</template>
</el-input>
</template>
<template #englishName-form>
<el-input
v-model="form.englishName"
:maxlength="feeItemCodeMaxlength"
class="fee-item-form-control"
clearable
placeholder="请输入费用项代码"
@change="handleFeeItemCodeChange"
>
<template #prepend>{{ feeCategoryPrefix || '费用类型' }}</template>
</el-input>
</template>
<template #menu="{ row, index }">
<el-link
type="primary"
v-if="permission.fee_item_edit"
@click="$refs.crud.rowEdit(row, index)"
>
编辑
</el-link>
<el-link type="primary" v-if="permission.fee_item_status" @click="handleStatus(row)">
{{ row.status === 1 ? '停用' : '启用' }}
</el-link>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/fee-item';
import { exportBlob } from '@/api/common';
import { getDictionaryAll } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { Download } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
export default {
components: { Download },
data() {
return {
form: {},
query: {},
loading: true,
data: [],
dialogType: 'add',
feeCategoryOptions: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 180,
column: [
{
label: '费用类型',
prop: 'feeCategory',
type: 'select',
search: true,
searchOrder: 3,
filterable: true,
formslot: true,
slot: true,
span: 12,
dicData: [],
props: {
label: 'displayLabel',
value: 'dictKey',
},
minWidth: 120,
rules: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
},
{
label: '费用项代码',
prop: 'englishName',
formslot: true,
span: 12,
minWidth: 160,
maxlength: 100,
rules: [{ required: true, message: '请输入费用项代码', trigger: 'blur' }],
},
{
label: '税率',
prop: 'taxRate',
type: 'input',
formslot: true,
slot: true,
minWidth: 100,
span: 12,
rules: [
{ required: true, message: '请输入税率', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const text = String(value ?? '').trim();
const rate = Number(text);
if (!/^\d+(\.\d{1,2})?$/.test(text) || rate < 0 || rate > 100) {
callback(new Error('税率必须是0到100之间且最多保留2位小数的数字'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '费用项',
prop: 'name',
minWidth: 140,
search: true,
searchOrder: 4,
span: 12,
maxlength: 50,
rules: [{ required: true, message: '请输入费用项', trigger: 'blur' }],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 2,
span: 24,
minWidth: 200,
overHidden: true,
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '组织',
prop: 'createDeptName',
minWidth: 160,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '组织',
prop: 'createDept',
type: 'select',
search: true,
searchOrder: 1,
hide: true,
display: false,
filterable: true,
dicData: [],
props: {
label: 'label',
value: 'value',
},
},
{
label: '更新人',
prop: 'updateUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
display: false,
},
{
label: '更新时间',
prop: 'updateTime',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '创建时间',
prop: 'createTime',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
searchOrder: 2,
slot: true,
dataType: 'number',
dicData: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
addDisplay: false,
editDisplay: false,
value: 1,
minWidth: 110,
},
],
},
};
},
created() {
this.initFeeCategoryOptions();
this.initDeptTree();
},
computed: {
...mapGetters(['permission', 'userInfo']),
feeCategoryPrefix() {
return this.getFeeCategoryPrefix(this.form.feeCategory);
},
feeItemCodeMaxlength() {
return Math.max(1, 100 - this.feeCategoryPrefix.length);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
permissionList() {
return {
addBtn: this.validData(this.permission.fee_item_add, false),
};
},
},
methods: {
initFeeCategoryOptions() {
getDictionaryAll().then(res => {
const options = (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 => ({
...item,
displayLabel: `${item.dictValue}/${item.dictKey}`,
}));
this.feeCategoryOptions = options;
const column = this.findColumn(this.option.column, 'feeCategory');
column.dicData = options;
});
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
const column = this.findColumn(this.option.column, 'createDept');
column.dicData = this.flattenDept(res.data.data || []);
});
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
}
});
return result;
},
getFeeCategoryPrefix(value) {
const feeCategory = String(value || '').trim();
const option = this.feeCategoryOptions.find(
item => String(item.dictKey || '') === feeCategory
);
return String((option && option.dictKey) || feeCategory);
},
formatFeeCategory(value) {
const feeCategory = String(value || '').trim();
if (!feeCategory) return '';
const option = this.feeCategoryOptions.find(
item =>
String(item.dictKey || '') === feeCategory || String(item.dictValue || '') === feeCategory
);
return option ? `${option.dictValue}/${option.dictKey}` : feeCategory;
},
formatTaxRate(value) {
return value === undefined || value === null || value === '' ? '' : `${value}%`;
},
getFeeItemCodeSuffix(code, feeCategory) {
const value = String(code || '').trim();
const prefix = this.getFeeCategoryPrefix(feeCategory);
if (prefix && value.startsWith(prefix)) {
return value.slice(prefix.length);
}
return value;
},
handleFeeCategoryChange(value) {
this.form.feeCategory = value;
this.form.englishName = this.getFeeItemCodeSuffix(this.form.englishName, value);
},
handleFeeItemCodeChange(value) {
this.form.englishName = this.getFeeItemCodeSuffix(value, this.form.feeCategory);
},
normalizeRow(row) {
const values = { ...row };
const prefix = this.getFeeCategoryPrefix(values.feeCategory);
const code = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
values.feeCategory = String(values.feeCategory || '').trim();
values.name = String(values.name || '').trim();
values.taxRate = Number(values.taxRate);
values.englishName = prefix ? `${prefix}${code}` : code;
values.remark = String(values.remark || '').trim() || undefined;
if (!values.status) values.status = 1;
return values;
},
normalizeFormForEdit(row) {
const values = { ...row };
values.feeCategory = String(values.feeCategory || '').trim();
values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
return values;
},
validateUnique(row) {
const rowId = String(row.id || '');
const hasDuplicate = (params, prop) => {
if (!row[prop]) {
return Promise.resolve(false);
}
return getList(1, 100, params).then(res => {
const records = res.data.data.records || [];
return records.some(
item =>
String(item[prop] || '').trim() === String(row[prop]).trim() &&
String(item.id || '') !== rowId
);
});
};
return Promise.all([
hasDuplicate({ name: row.name }, 'name'),
hasDuplicate({ englishName: row.englishName }, 'englishName'),
]).then(([nameExists, codeExists]) => {
if (nameExists) {
return Promise.reject(new Error('该费用项已存在'));
}
if (codeExists) {
return Promise.reject(new Error('该费用项代码已存在'));
}
return Promise.resolve();
});
},
handleSubmitError(error, loading) {
const uniqueMessages = ['该费用项已存在', '该费用项代码已存在'];
if (uniqueMessages.includes(error.message)) {
this.$message.warning(error.message);
}
window.console.log(error);
loading();
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
})
.catch(error => this.handleSubmitError(error, loading));
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
})
.catch(error => this.handleSubmitError(error, loading));
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleStatus(row) {
const nextStatus = row.status === 1 ? 2 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
this.$confirm(`是否确认${actionName}该条数据?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => changeStatus(row.id, nextStatus))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
beforeOpen(done, type) {
this.dialogType = type || 'add';
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = this.normalizeFormForEdit(res.data.data || {});
done();
});
return;
}
this.form.status = 1;
this.form.englishName = '';
done();
},
handleTaxRateInput(value) {
let normalized = String(value || '').replace(/[^\d.]/g, '');
normalized = normalized.replace(/(\..*)\./g, '$1');
if (normalized.startsWith('.')) normalized = `0${normalized}`;
const [integerPart, decimalPart] = normalized.split('.');
normalized =
decimalPart === undefined ? integerPart : `${integerPart}.${decimalPart.slice(0, 2)}`;
this.form.taxRate = normalized;
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
buildExportParams() {
return {
...this.query,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
handleExport() {
this.$confirm('是否导出费用项数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob('/blade-system/fee-item/export-fee-item', this.buildExportParams(), {
feedback: true,
})
.then(res => {
downloadXls(res.data, `费用项${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.selectionClear();
this.loading = false;
});
},
},
};
</script>
<style scoped>
.fee-item-page :deep(.avue-crud__dialog .avue-form__row),
.fee-item-page :deep(.avue-crud__dialog .el-row) {
margin-left: -36px;
margin-right: -36px;
}
.fee-item-page :deep(.avue-crud__dialog .el-col) {
padding-left: 36px;
padding-right: 36px;
}
.fee-item-form-control {
width: 100%;
}
</style>