Files
tms-erp-web/src/views/business/common-cargo.vue
T
2026-08-11 09:31:42 +08:00

682 lines
20 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="common-cargo-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"
icon="el-icon-plus"
v-if="hasPermission('common_cargo_add')"
@click="$refs.crud.rowAdd()"
>新建常用货物
</el-button>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="config.importUrl && hasPermission('common_cargo_import')"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.templateUrl && hasPermission('common_cargo_template')"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.exportUrl && hasPermission('common_cargo_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('common_cargo_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #firstCargoTypeName-form>
<el-select
v-model="form.firstCargoTypeCode"
clearable
filterable
remote
:remote-method="loadFirstCargoTypeOptions"
:loading="firstCargoTypeLoading"
:disabled="dialogReadonly || dialogType === 'edit'"
placeholder="请选择一级货物类型"
style="width: 100%"
@change="handleFirstCargoTypeChange"
>
<el-option
v-for="item in firstCargoTypeOptions"
:key="item.cargoCode"
:label="item.cargoName"
:value="item.cargoCode"
/>
</el-select>
</template>
<template #secondCargoTypeName-form>
<el-select
v-model="form.secondCargoTypeCode"
clearable
filterable
remote
:remote-method="loadSecondCargoTypeOptions"
:loading="secondCargoTypeLoading"
:disabled="dialogReadonly || dialogType === 'edit' || !form.firstCargoTypeCode"
:placeholder="form.firstCargoTypeCode ? '请选择二级货物类型' : '请先选择一级货物类型'"
style="width: 100%"
@change="handleSecondCargoTypeChange"
>
<el-option
v-for="item in secondCargoTypeOptions"
:key="item.cargoCode"
:label="item.cargoName"
:value="item.cargoCode"
/>
</el-select>
</template>
<template #cargoCode-form>
<el-input
:model-value="getCargoCodeSuffix()"
clearable
:maxlength="getCargoCodeMaxLength()"
show-word-limit
placeholder="请输入"
:disabled="dialogReadonly || dialogType === 'edit'"
@input="handleCargoCodeInput"
>
<template v-if="getCargoCodePrefix()" #prepend>
{{ getCargoCodePrefix() }}
</template>
</el-input>
</template>
<template #cargoValue-form>
<div class="common-cargo-page__value-unit">
<el-input
v-model="form.cargoValue"
clearable
placeholder="请输入"
:disabled="dialogReadonly"
@input="handleCargoValueInput"
/>
<el-select
v-model="form.priceUnit"
clearable
:disabled="dialogReadonly"
placeholder="请选择"
>
<el-option
v-for="item in priceUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</template>
<template #menu="{ row, index }">
<el-link
type="primary"
v-if="hasPermission('common_cargo_view')"
@click="$refs.crud.rowView(row, index)"
>
查看
</el-link>
<el-link
type="primary"
v-if="hasPermission('common_cargo_edit') && !row.readonly"
@click="$refs.crud.rowEdit(row, index)"
>
编辑
</el-link>
<el-link
type="danger"
v-if="hasPermission('common_cargo_delete') && !row.readonly"
@click="rowDel(row)"
>
删除
</el-link>
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOptionConfig" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
</basic-container>
</template>
<script>
import * as api from '@/api/business/common-cargo';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { config, excelOption, option } from '@/option/business/common-cargo';
import { priceUnitOptions } from '@/option/business/common';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
export default {
data() {
return {
api,
config,
option,
priceUnitOptions,
form: {},
query: {},
loading: true,
data: [],
dialogReadonly: false,
dialogType: 'add',
excelBox: false,
excelForm: {},
excelOptionConfig: excelOption,
firstCargoTypeLoading: false,
secondCargoTypeLoading: false,
firstCargoTypeOptions: [],
secondCargoTypeOptions: [],
deptOptions: [],
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.hasPermission('common_cargo_add'),
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.initDeptTree();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
getRecords(res) {
const data = res && res.data && res.data.data;
if (Array.isArray(data)) return data;
return (data && data.records) || [];
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.deptOptions;
}
});
},
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;
},
normalizeCargoTypeOptions(list = []) {
return list
.map(item => ({
...item,
cargoCode: String(item.cargoCode || ''),
cargoName: item.cargoName || '',
}))
.filter(item => item.cargoCode && item.cargoName);
},
ensureCargoTypeOption(listName, optionItem) {
if (!optionItem || !optionItem.cargoCode) return;
const exists = this[listName].some(item => item.cargoCode === optionItem.cargoCode);
if (!exists) {
this[listName] = [optionItem, ...this[listName]];
}
},
loadFirstCargoTypeOptions(keyword = '') {
this.firstCargoTypeLoading = true;
getCargoTypeList(1, 30, {
typeLevel: 1,
cargoName: String(keyword || '').trim(),
})
.then(res => {
this.firstCargoTypeOptions = this.normalizeCargoTypeOptions(this.getRecords(res));
})
.finally(() => {
this.firstCargoTypeLoading = false;
});
},
loadSecondCargoTypeOptions(keyword = '') {
if (!this.form.firstCargoTypeCode) {
this.secondCargoTypeOptions = [];
return;
}
this.secondCargoTypeLoading = true;
getCargoTypeList(1, 30, {
typeLevel: 2,
parentCargoCode: this.form.firstCargoTypeCode,
cargoName: String(keyword || '').trim(),
})
.then(res => {
this.secondCargoTypeOptions = this.normalizeCargoTypeOptions(this.getRecords(res));
})
.finally(() => {
this.secondCargoTypeLoading = false;
});
},
handleFirstCargoTypeChange(value) {
const first = this.firstCargoTypeOptions.find(item => item.cargoCode === value);
this.form.firstCargoTypeName = first ? first.cargoName : '';
this.form.firstCargoTypeCode = first ? first.cargoCode : '';
this.form.secondCargoTypeName = '';
this.form.secondCargoTypeCode = '';
this.secondCargoTypeOptions = [];
this.syncCargoCodePrefix();
if (first) {
this.loadSecondCargoTypeOptions();
}
},
handleSecondCargoTypeChange(value) {
const second = this.secondCargoTypeOptions.find(item => item.cargoCode === value);
this.form.secondCargoTypeName = second ? second.cargoName : '';
this.form.secondCargoTypeCode = second ? second.cargoCode : '';
this.form.cargoCode = second ? String(second.cargoCode).slice(0, 4) : '';
this.syncCargoCodePrefix();
},
getCargoCodePrefix() {
return this.form.secondCargoTypeCode
? String(this.form.secondCargoTypeCode).slice(0, 4)
: '';
},
getCargoCodeSuffix() {
const prefix = this.getCargoCodePrefix();
const cargoCode = String(this.form.cargoCode || '');
return prefix && cargoCode.startsWith(prefix) ? cargoCode.slice(prefix.length) : cargoCode;
},
getCargoCodeMaxLength() {
return 20 - this.getCargoCodePrefix().length;
},
syncCargoCodePrefix() {
const prefix = this.getCargoCodePrefix();
const current = String(this.form.cargoCode || '').replace(/\D/g, '');
if (!prefix) {
this.form.cargoCode = current;
return;
}
const suffix = current.startsWith(prefix) ? current.slice(prefix.length) : current;
this.form.cargoCode = `${prefix}${suffix}`.slice(0, 20);
},
handleCargoCodeInput(value) {
const prefix = this.getCargoCodePrefix();
const suffix = String(value || '')
.replace(/\D/g, '')
.slice(0, this.getCargoCodeMaxLength());
this.form.cargoCode = `${prefix}${suffix}`;
},
handleCargoValueInput(value) {
const raw = String(value || '').replace(/[^\d.]/g, '');
const parts = raw.split('.');
const integer = parts[0] || '';
const decimal = parts.slice(1).join('').slice(0, 2);
this.form.cargoValue = parts.length > 1 ? `${integer}.${decimal}` : integer;
},
normalizeRow(row) {
const submitRow = { ...row };
Object.keys(submitRow).forEach(key => {
if (typeof submitRow[key] === 'string') {
submitRow[key] = submitRow[key].trim();
}
});
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
submitRow.cargoValue = String(submitRow.cargoValue).trim();
}
return submitRow;
},
validateRow(row) {
if (!row.firstCargoTypeName || !row.firstCargoTypeCode) {
this.$message.warning('请选择一级货物类型');
return false;
}
if (!row.secondCargoTypeName || !row.secondCargoTypeCode) {
this.$message.warning('请选择二级货物类型');
return false;
}
if (!row.cargoName) {
this.$message.warning('请输入货物名称');
return false;
}
if (row.cargoName.length > 100) {
this.$message.warning('货物名称不能超过100个字符');
return false;
}
const cargoCode = String(row.cargoCode || '');
const prefix = String(row.secondCargoTypeCode || '').slice(0, 4);
if (!cargoCode || cargoCode === prefix) {
this.$message.warning('请输入货物编号');
return false;
}
if (!cargoCode.startsWith(prefix)) {
this.$message.warning('货物编号需拼接二级货物类型编号在前面');
return false;
}
if (cargoCode.length > 20) {
this.$message.warning('货物编号不能超过20个字符');
return false;
}
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
this.$message.warning('货值只能输入数字,最多保留2位小数');
return false;
}
if (row.remark && row.remark.length > 200) {
this.$message.warning('备注不能超过200个字符');
return false;
}
return true;
},
stopSubmitLoading(loading) {
if (typeof loading === 'function') {
loading();
}
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => api.remove(row.id))
.then(res => {
this.afterRemove(res.data.data);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
const selection = this.selectionList.filter(item => !item.readonly);
if (!selection.length) {
this.$message.warning('所选数据均为只读数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => api.remove(selection.map(item => item.id).join(',')))
.then(res => {
this.afterRemove(res.data.data);
});
},
afterRemove(result = {}) {
this.onLoad(this.page);
const successCount = result.successCount || 0;
const skippedCount = result.skippedCount || 0;
if (skippedCount) {
this.$message.warning(`成功删除${successCount}条,因业务引用跳过${skippedCount}条`);
} else {
this.$message({ type: 'success', message: '删除成功!' });
}
this.selectionClear();
},
beforeOpen(done, type) {
this.dialogReadonly = type === 'view';
this.dialogType = type || 'add';
if (type === 'add') {
this.form = {
priceUnit: '元/吨',
...this.form,
};
this.loadFirstCargoTypeOptions();
done();
return;
}
if (['edit', 'view'].includes(type)) {
api.getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
this.ensureCargoTypeOption('firstCargoTypeOptions', {
cargoName: this.form.firstCargoTypeName,
cargoCode: this.form.firstCargoTypeCode,
});
this.ensureCargoTypeOption('secondCargoTypeOptions', {
cargoName: this.form.secondCargoTypeName,
cargoCode: this.form.secondCargoTypeCode,
});
if (this.form.firstCargoTypeCode) {
this.loadSecondCargoTypeOptions();
}
done();
});
return;
}
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
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 = [];
if (this.$refs.crud) {
this.$refs.crud.toggleSelection();
}
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
.then(res => {
const result = res.data.data;
this.page.total = result.total;
this.data = result.records;
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
buildExportParams() {
return {
...this.query,
allDept: 0,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
handleExport() {
this.$confirm(`是否导出${config.exportName}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(config.exportUrl, this.buildExportParams(), { feedback: true })
.then(res => {
downloadXls(
res.data,
`${config.exportName}${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
})
.finally(() => {
NProgress.done();
});
});
},
handleTemplate() {
NProgress.start();
exportBlob(
config.templateUrl,
{
[this.website.tokenHeader]: getToken(),
},
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `${config.title}模板.xlsx`);
})
.finally(() => {
NProgress.done();
});
},
handleImport() {
const column = this.excelOptionConfig.column.find(item => item.prop === 'excelFile');
column.action = config.importUrl;
openImportDialog(this, config.title, () => this.onLoad(this.page, this.query));
},
},
};
</script>
<style lang="scss" scoped>
.common-cargo-page {
&__value-unit {
display: flex;
align-items: center;
width: 100%;
.el-input {
flex: 1;
min-width: 0;
}
.el-select {
flex: 0 0 112px;
margin-left: -1px;
}
:deep(.el-input__wrapper) {
border-radius: 4px 0 0 4px;
}
.el-select :deep(.el-input__wrapper) {
border-radius: 0 4px 4px 0;
}
}
}
</style>