Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-07-29 00:53:16 +08:00
39 changed files with 9180 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" :crud-excel-option="excelOption" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/common-cargo';
import { config, excelOption, option } from '@/option/business/common-cargo';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
excelOption,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/common-route';
import { config, option } from '@/option/business/common-route';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,673 @@
<template>
<basic-container class="business-crud-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"
plain
v-if="canCreate"
@click="$refs.crud.rowAdd()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="config.importUrl && hasPermission(`${config.permission}_import`) && !readonlyScope"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.templateUrl && hasPermission(`${config.permission}_template`)"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="success"
icon="el-icon-check"
plain
v-if="canBatchComplete"
@click="handleBatchComplete"
>批量完成
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission(`${config.permission}_delete`) && !readonlyScope"
@click="handleDelete"
>批量删除
</el-button>
<el-switch
v-if="config.enableAllDept && canViewAllDept"
v-model="allDept"
class="business-crud-page__scope"
active-text="全部组织"
inactive-text="当前组织"
:active-value="1"
:inactive-value="0"
@change="handleScopeChange"
/>
</template>
<template #status="{ row }">
<el-tag :type="statusTagType(row.status)">
{{ displayStatus(row, 'status') }}
</el-tag>
</template>
<template #businessStatus="{ row }">
<el-tag :type="statusTagType(row.businessStatus)">
{{ displayStatus(row, 'businessStatus') }}
</el-tag>
</template>
<template #approvalStatus="{ row }">
<el-tag :type="statusTagType(row.approvalStatus)">
{{ displayStatus(row, 'approvalStatus') }}
</el-tag>
</template>
<template #contractStage="{ row }">
<el-tag :type="stageTagType(row.contractStage)">
{{ displayStatus(row, 'contractStage') }}
</el-tag>
</template>
<template #menu="{ row, index }">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="hasPermission(`${config.permission}_view`)"
@click="$refs.crud.rowView(row, index)"
>
查看
</el-button>
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="canEdit(row)"
@click="$refs.crud.rowEdit(row, index)"
>
编辑
</el-button>
<el-button type="primary" text icon="el-icon-copy-document" v-if="canCopy(row)" @click="handleCopy(row)">
复制
</el-button>
<el-button type="primary" text icon="el-icon-video-play" v-if="canEnable(row)" @click="handleAction('enable', row)">
启用
</el-button>
<el-button type="primary" text icon="el-icon-video-pause" v-if="canDisable(row)" @click="handleAction('disable', row)">
停用
</el-button>
<el-button type="primary" text icon="el-icon-refresh" v-if="canReassign(row)" @click="handleAction('reassign', row)">
重新派单
</el-button>
<el-button type="primary" text icon="el-icon-close" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消
</el-button>
<el-button type="primary" text icon="el-icon-check" v-if="canComplete(row)" @click="handleAction('complete', row)">
完成
</el-button>
<el-button
v-for="operation in customOperations(row)"
:key="operation.action"
:type="operation.type || 'primary'"
text
:icon="operation.icon || 'el-icon-position'"
@click="handleCustomOperation(operation, row)"
>
{{ operation.label }}
</el-button>
<el-button type="primary" text icon="el-icon-delete" v-if="canDelete(row)" @click="rowDel(row)">
删除
</el-button>
</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 { exportBlob } from '@/api/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 {
name: 'BusinessCrudPage',
props: {
api: {
type: Object,
required: true,
},
config: {
type: Object,
required: true,
},
crudOption: {
type: Object,
required: true,
},
crudExcelOption: {
type: Object,
default: () => ({ column: [] }),
},
},
data() {
return {
form: {},
query: {},
loading: true,
data: [],
allDept: 0,
dialogReadonly: false,
option: this.cloneOption(this.crudOption),
excelBox: false,
excelForm: {},
excelOptionConfig: this.cloneOption(this.crudExcelOption),
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.canCreate,
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority) ? authority.includes('admin') : String(authority || '').includes('admin');
},
canViewAllDept() {
return this.isAdmin;
},
readonlyScope() {
return this.config.enableAllDept && this.allDept === 1;
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
},
canBatchComplete() {
return (
this.hasAction('batchComplete') &&
this.hasPermission(`${this.config.permission}_batch_complete`) &&
!this.readonlyScope
);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
methods: {
cloneOption(option) {
return {
...option,
column: (option.column || []).map(column => ({ ...column })),
};
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
hasAction(action) {
return (this.config.actions || []).includes(action);
},
statusValue(row) {
return row[this.config.statusProp || 'status'];
},
inStatus(row, statusList) {
if (!statusList || !statusList.length) {
return true;
}
return statusList.includes(this.statusValue(row));
},
canEdit(row) {
return (
this.hasPermission(`${this.config.permission}_edit`) &&
!this.readonlyScope &&
!row.readonly &&
this.inStatus(row, this.config.editStatus)
);
},
canDelete(row) {
return (
this.hasPermission(`${this.config.permission}_delete`) &&
!this.readonlyScope &&
!row.readonly &&
this.inStatus(row, this.config.deleteStatus)
);
},
canCopy(row) {
return this.hasAction('copy') && this.hasPermission(`${this.config.permission}_copy`) && !this.readonlyScope && !row.readonly;
},
canEnable(row) {
return (
this.hasAction('enable') &&
this.hasPermission(`${this.config.permission}_enable`) &&
!this.readonlyScope &&
!row.readonly &&
[0, 2].includes(row.status)
);
},
canDisable(row) {
return (
this.hasAction('disable') &&
this.hasPermission(`${this.config.permission}_disable`) &&
!this.readonlyScope &&
!row.readonly &&
row.status === 1
);
},
canCancel(row) {
if (!this.hasAction('cancel') || !this.hasPermission(`${this.config.permission}_cancel`) || this.readonlyScope || row.readonly) {
return false;
}
const status = this.statusValue(row);
return this.config.permission === 'transport_plan'
? ['waiting_dispatch', 'dispatching'].includes(status)
: ['pending', 'processing'].includes(status);
},
canComplete(row) {
if (!this.hasAction('complete') || !this.hasPermission(`${this.config.permission}_complete`) || this.readonlyScope || row.readonly) {
return false;
}
const status = this.statusValue(row);
return this.config.permission === 'transport_plan' ? status === 'dispatching' : status === 'processing';
},
canReassign(row) {
return (
this.hasAction('reassign') &&
this.hasPermission(`${this.config.permission}_reassign`) &&
!this.readonlyScope &&
!row.readonly &&
this.statusValue(row) === 'pending'
);
},
customOperations(row) {
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
},
canOperation(operation, row) {
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) {
return false;
}
const prop = operation.statusProp || this.config.statusProp || 'status';
const statusList = operation.status || [];
if (statusList.length && !statusList.includes(row[prop])) {
return false;
}
if (operation.stage && !operation.stage.includes(row.contractStage)) {
return false;
}
return true;
},
displayStatus(row, prop) {
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
if (textProp && row[textProp]) {
return row[textProp];
}
const column = this.findColumn(this.option.column, prop);
const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
return item ? item.label : row[prop] || '未知';
},
statusTagType(status) {
if ([1, 'pending', 'waiting_dispatch', 'processing', 'dispatching', 'approved', 'change_approved'].includes(status)) {
return 'success';
}
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
return 'info';
}
if (['rejected', 'change_rejected'].includes(status)) {
return 'danger';
}
if (status === 'completed') {
return 'primary';
}
return 'warning';
},
stageTagType(stage) {
if (stage === 'formal') {
return 'success';
}
if (stage === 'temporary') {
return 'warning';
}
if (stage === 'terminated') {
return 'danger';
}
return 'info';
},
normalizeRow(row) {
const submitRow = { ...row };
Object.keys(submitRow).forEach(key => {
if (typeof submitRow[key] === 'string') {
submitRow[key] = submitRow[key].trim();
}
});
return submitRow;
},
stopSubmitLoading(loading) {
if (typeof loading === 'function') {
loading();
}
},
rowSave(row, done, loading) {
this.api.submit(this.normalizeRow(row)).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
rowUpdate(row, index, done, loading) {
this.api.submit(this.normalizeRow(row)).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(() => this.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 => this.canDelete(item));
if (!selection.length) {
this.$message.warning('所选数据当前状态不可删除');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.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';
if (type === 'add') {
this.form = {
...(this.config.defaultForm || {}),
...this.form,
};
done();
return;
}
if (['edit', 'view'].includes(type)) {
this.api.getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
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);
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
}
this.page.currentPage = 1;
this.onLoad(this.page);
},
onLoad(page, params = {}) {
this.loading = true;
this.api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
.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: this.allDept,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
handleExport() {
this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(this.config.exportUrl, this.buildExportParams())
.then(res => {
downloadXls(res.data, `${this.config.exportName || this.config.title}${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
handleTemplate() {
NProgress.start();
exportBlob(this.config.templateUrl, {
[this.website.tokenHeader]: getToken(),
})
.then(res => {
downloadXls(res.data, `${this.config.title}模板.xlsx`);
})
.finally(() => {
NProgress.done();
});
},
handleImport() {
const column = this.findColumn(this.excelOptionConfig.column, 'excelFile');
column.action = this.config.importUrl;
openImportDialog(this, this.config.title, () => this.onLoad(this.page, this.query));
},
handleCopy(row) {
this.$confirm(`确定复制该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api.copy(row.id))
.then(() => {
this.$message.success('复制成功');
this.onLoad(this.page, this.query);
});
},
handleAction(action, row) {
const actionName = {
enable: '启用',
disable: '停用',
cancel: '取消',
complete: '完成',
reassign: '重新派单',
}[action];
this.$confirm(`确定${actionName}${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api[action](row.id))
.then(() => {
this.$message.success(`${actionName}成功`);
this.onLoad(this.page, this.query);
});
},
handleCustomOperation(operation, row) {
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(() => {
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
});
};
if (operation.prompt) {
this.$prompt(operation.prompt, operation.label, {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S+/,
inputErrorMessage: operation.promptError || '请输入内容',
}).then(({ value }) => run(value));
return;
}
this.$confirm(operation.confirm || `确定${operation.label}${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => run());
},
handleBatchComplete() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定完成选中的运单?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api.batchComplete(this.ids))
.then(res => {
const result = res.data.data || {};
this.$message.success(`成功完成${result.successCount || 0}条,跳过${result.skippedCount || 0}`);
this.onLoad(this.page, this.query);
});
},
},
};
</script>
<style lang="scss" scoped>
.business-crud-page {
&__scope {
margin-left: 12px;
vertical-align: middle;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/contract-manage';
import { config, option } from '@/option/business/contract-manage';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/process-config';
import { config, option } from '@/option/business/process-config';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/project-apply';
import { config, option } from '@/option/business/project-apply';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/shipping-template';
import { config, option } from '@/option/business/shipping-template';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/temporary-credit-limit';
import { config, option } from '@/option/business/temporary-credit-limit';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/transport-plan';
import { config, option } from '@/option/business/transport-plan';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import * as api from '@/api/business/waybill-manage';
import { config, option } from '@/option/business/waybill-manage';
export default {
components: { BusinessCrudPage },
data() {
return {
api,
config,
option,
};
},
};
</script>