1、完善项目

2、完善合同
3、完善费用项
4、司机管理对接OCR
5、完善运输计划
6、完善临时额度
This commit is contained in:
2026-08-04 12:17:49 +08:00
parent 81c98f6ad8
commit f9f8221870
34 changed files with 7887 additions and 983 deletions

View File

@@ -129,7 +129,7 @@ import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { Plus, Upload, Download, Delete } from '@element-plus/icons-vue';
@@ -265,9 +265,7 @@ export default {
propsHttp: {
res: 'data',
},
headers: {
'Blade-Auth': `bearer ${getToken()}`,
},
headers: getUploadHeaders(),
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-system/cargo-type/import-cargo-type',
},

View File

@@ -1,5 +1,5 @@
<template>
<basic-container>
<basic-container class="fee-item-page">
<avue-crud
:option="option"
:table-loading="loading"
@@ -14,20 +14,20 @@
@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="danger"
icon="el-icon-delete"
plain
v-if="permission.fee_item_delete"
@click="handleDelete"
>批量删除
<el-button type="primary" plain v-if="permission.fee_item_import" @click="handleImport">
<el-icon class="el-icon--left"><Upload /></el-icon>批量导入
</el-button>
<el-button type="primary" plain v-if="permission.fee_item_template" @click="handleTemplate">
<el-icon class="el-icon--left"><Download /></el-icon>下载模板
</el-button>
<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 }">
@@ -35,6 +35,35 @@
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</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"
:value="item.dictKey"
/>
</el-select>
</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-button
type="primary"
@@ -44,41 +73,60 @@
>
编辑
</el-button>
<el-button
type="primary"
text
v-if="permission.fee_item_status"
@click="handleStatus(row)"
>
<el-button type="primary" text v-if="permission.fee_item_status" @click="handleStatus(row)">
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
</avue-crud>
<el-dialog title="费用项导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" 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 { getList, getDetail, submit, remove, changeStatus } from '@/api/base/fee-item';
import { exportBlob } from '@/api/common';
import { getDictionary } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { Upload, Download } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
export default {
components: { Upload, Download },
data() {
return {
form: {},
query: {},
loading: true,
data: [],
excelBox: false,
excelForm: {},
feeCategoryOptions: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 680,
dialogWidth: 980,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchShow: true,
searchMenuSpan: 24,
@@ -88,10 +136,11 @@ export default {
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
selection: false,
dialogClickModal: false,
menuWidth: 160,
column: [
@@ -100,8 +149,11 @@ export default {
prop: 'feeCategory',
type: 'select',
search: true,
searchOrder: 3,
filterable: true,
dicUrl: '/blade-system/dict/dictionary?code=fee_category',
formslot: true,
span: 12,
dicData: [],
props: {
label: 'dictValue',
value: 'dictKey',
@@ -109,35 +161,56 @@ export default {
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: 'name',
minWidth: 140,
search: true,
searchOrder: 4,
span: 12,
maxlength: 50,
rules: [{ required: true, message: '请输入费用项', trigger: 'blur' }],
},
{
label: '费用项代码',
prop: 'englishName',
label: '组织',
prop: 'createDeptName',
minWidth: 160,
search: true,
maxlength: 100,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
dicData: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
addDisplay: false,
editDisplay: false,
value: 1,
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: '更新时间',
@@ -161,32 +234,135 @@ export default {
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: 100,
},
],
},
excelOption: {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
headers: getUploadHeaders(),
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-system/fee-item/import-fee-item',
},
{
label: '模板下载',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
],
},
};
},
created() {
this.initFeeCategoryOptions();
this.initDeptTree();
},
computed: {
...mapGetters(['permission']),
...mapGetters(['permission', 'userInfo']),
feeCategoryPrefix() {
return this.getFeeCategoryPrefix(this.form.feeCategory);
},
feeItemCodeMaxlength() {
return Math.max(1, 100 - this.feeCategoryPrefix.length);
},
permissionList() {
return {
addBtn: this.validData(this.permission.fee_item_add, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
initFeeCategoryOptions() {
getDictionary({ code: 'fee_category' }).then(res => {
this.feeCategoryOptions = res.data.data || [];
const column = this.findColumn(this.option.column, 'feeCategory');
column.dicData = this.feeCategoryOptions;
});
},
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);
},
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) {
row.feeCategory = String(row.feeCategory || '').trim();
row.name = String(row.name || '').trim();
row.englishName = String(row.englishName || '').trim();
if (!row.status) row.status = 1;
return 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.englishName = prefix ? `${prefix}${code}` : code;
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;
},
rowSave(row, done, loading) {
submit(this.normalizeRow(row)).then(
@@ -226,23 +402,6 @@ export default {
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(this.ids))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$refs.crud.toggleSelection();
});
},
handleStatus(row) {
const nextStatus = row.status === 1 ? 2 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
@@ -260,12 +419,13 @@ export default {
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
this.form = this.normalizeFormForEdit(res.data.data || {});
done();
});
return;
}
this.form.status = 1;
this.form.englishName = '';
done();
},
searchReset() {
@@ -278,13 +438,6 @@ export default {
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
@@ -294,6 +447,42 @@ export default {
refreshChange() {
this.onLoad(this.page, this.query);
},
handleImport() {
openImportDialog(this, '费用项');
},
buildExportParams() {
return {
...this.query,
[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();
});
});
},
handleTemplate() {
exportBlob(
`/blade-system/fee-item/export-template?${this.website.tokenHeader}=${getToken()}`,
undefined,
{ feedback: true }
).then(res => {
downloadXls(res.data, '费用项模板.xlsx');
});
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
@@ -301,9 +490,25 @@ export default {
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</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>

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,15 @@
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="canMajorProjectSupplement"
@click="openProjectDialog('majorSupplement')"
>
重大项目补录
</el-button>
<el-button
type="primary"
icon="el-icon-plus"
@@ -44,16 +53,6 @@
>
批量删除
</el-button>
<el-switch
v-if="config.enableAllDept && canViewAllDept"
v-model="allDept"
class="project-apply-page__scope"
active-text="全部组织"
inactive-text="当前组织"
:active-value="1"
:inactive-value="0"
@change="handleScopeChange"
/>
</template>
<template #approvalStatus="{ row }">
@@ -71,12 +70,7 @@
>
查看
</el-button>
<el-button
type="primary"
text
v-if="canEdit(row)"
@click="openProjectDialog('edit', row)"
>
<el-button type="primary" text v-if="canEdit(row)" @click="openProjectDialog('edit', row)">
编辑
</el-button>
<el-button
@@ -88,14 +82,7 @@
>
{{ operation.label }}
</el-button>
<el-button
type="primary"
text
v-if="canDelete(row)"
@click="rowDel(row)"
>
删除
</el-button>
<el-button type="primary" text v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-button>
</template>
</avue-crud>
@@ -127,8 +114,25 @@
>
<div class="dialog-section-title">项目基本信息</div>
<div class="project-apply-form__grid">
<el-form-item v-if="isChangeDialog" label="变更类型" prop="changeType">
<el-select v-model="form.changeType" placeholder="请选择变更类型">
<el-option
v-for="item in changeTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<div v-if="isChangeDialog"></div>
<el-form-item label="项目类型" prop="projectType">
<el-select v-model="form.projectType" placeholder="请选择项目类型" clearable>
<el-select
v-model="form.projectType"
placeholder="请选择项目类型"
clearable
:disabled="isBasicInfoReadonly || isMajorProjectSupplementDialog"
@change="handleProjectTypeChange"
>
<el-option
v-for="item in projectTypeOptions"
:key="item.value"
@@ -141,10 +145,20 @@
<el-input v-model="form.projectCode" disabled placeholder="系统自动生成" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请填写项目名称" maxlength="50" />
<el-input
v-model="form.projectName"
placeholder="请填写项目名称"
maxlength="50"
:disabled="isBasicInfoReadonly"
/>
</el-form-item>
<el-form-item label="项目简称" prop="projectShortName">
<el-input v-model="form.projectShortName" placeholder="请填写项目简称" maxlength="20" />
<el-input
v-model="form.projectShortName"
placeholder="请填写项目简称"
maxlength="20"
:disabled="isBasicInfoReadonly"
/>
</el-form-item>
<el-form-item label="业务部门" prop="businessDeptId">
<el-select
@@ -152,6 +166,7 @@
placeholder="请选择业务部门"
filterable
clearable
:disabled="isBasicInfoReadonly"
@change="handleBusinessDeptChange"
>
<el-option
@@ -168,6 +183,7 @@
placeholder="请选择承办部门"
filterable
clearable
:disabled="isBasicInfoReadonly"
@change="handleUndertakeDeptChange"
>
<el-option
@@ -179,7 +195,12 @@
</el-select>
</el-form-item>
<el-form-item label="项目由来" prop="projectSource">
<el-select v-model="form.projectSource" placeholder="请选择项目由来" clearable>
<el-select
v-model="form.projectSource"
placeholder="请选择项目由来"
clearable
:disabled="isBasicInfoReadonly"
>
<el-option
v-for="item in projectSourceOptions"
:key="item.value"
@@ -189,7 +210,7 @@
</el-select>
</el-form-item>
<el-form-item label="项目由来说明" prop="sourceRemark">
<el-input v-model="form.sourceRemark" maxlength="300" />
<el-input v-model="form.sourceRemark" maxlength="300" :disabled="isBasicInfoReadonly" />
</el-form-item>
</div>
@@ -368,10 +389,7 @@
</el-form-item>
</div>
<div class="dialog-section-title project-apply-form__table-title">
客户信息
<span class="project-apply-form__tip">选择客户/承运商信息后,自动关联出客商信息</span>
</div>
<div class="dialog-section-title project-apply-form__table-title">客户信息</div>
<el-table :data="customerRows" border class="project-apply-form__table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="customer" label="客户" min-width="160" align="center" />
@@ -451,12 +469,7 @@
</div>
<div class="project-apply-form__material-head">
<div class="dialog-section-title project-apply-form__material-title">
项目材料
<span class="project-apply-form__material-warn">
项目立项报告、考察报告、立项成本测算表、企业资质板附件未上传
</span>
</div>
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
<el-button type="primary" :disabled="!attachmentRows.length" @click="handleBatchDownload"
>批量下载</el-button
>
@@ -466,21 +479,18 @@
<el-table-column label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
<el-option label="项目立项报告" value="项目立项报告" />
<el-option label="考察报告" value="考察报告" />
<el-option label="立项成本测算表" value="立项成本测算表" />
<el-option label="企业资质板附件" value="企业资质板附件" />
<el-option label="其他" value="其他" />
<el-option
v-for="item in projectAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
prop="name"
label="文件名"
min-width="220"
align="center"
show-overflow-tooltip
/>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
<el-input
@@ -519,13 +529,15 @@
@change="handleAttachmentChange"
/>
<div class="dialog-section-title project-apply-form__table-title">
<div v-if="!isChangeDialog" class="dialog-section-title project-apply-form__table-title">
变更记录
<span class="project-apply-form__tip"
>查看页面显示变更记录,变更提交后生成,可查看详情页面</span
>
</div>
<el-table :data="changeRows" border class="project-apply-form__table">
<el-table
v-if="!isChangeDialog"
:data="changeRows"
border
class="project-apply-form__table"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="changeDate" label="变更日期" min-width="140" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
@@ -553,19 +565,58 @@
</template>
</el-table-column>
</el-table>
<template v-if="isChangeDialog">
<div class="dialog-section-title">变更内容</div>
<el-form-item prop="changeContent" class="project-apply-form__change-textarea">
<el-input
v-model="form.changeContent"
type="textarea"
:rows="5"
placeholder="2000字以内"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<div class="dialog-section-title">变更原因</div>
<el-form-item prop="changeReason" class="project-apply-form__change-textarea">
<el-input
v-model="form.changeReason"
type="textarea"
:rows="5"
placeholder="2000字以内"
maxlength="2000"
show-word-limit
/>
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="project-apply-dialog__footer">
<el-button @click="projectBox = false">取消</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
:loading="submitLoading"
@click="submitProject"
>
提交
</el-button>
<div
class="project-apply-dialog__footer"
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
>
<template v-if="isChangeDialog">
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交
</el-button>
<el-button type="primary" :loading="submitLoading" @click="saveChangeProject">
保存
</el-button>
<el-button @click="projectBox = false">关闭</el-button>
</template>
<template v-else>
<el-button @click="projectBox = false">取消</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
:loading="submitLoading"
@click="submitProject"
>
提交
</el-button>
</template>
</div>
</template>
</el-dialog>
@@ -628,9 +679,11 @@ import {
getDetail as getCustomerArchiveDetail,
} from '@/api/vehicle/customer-archive';
import { getDeptTree } from '@/api/system/dept';
import { getDictionary } from '@/api/system/dict';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
import { getToken } from '@/utils/auth';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
@@ -640,7 +693,6 @@ import {
projectSourceOptions,
projectTypeOptions,
settlementModeOptions,
transportTypeOptions,
} from '@/option/business/common';
import { config, option } from '@/option/business/project-apply';
@@ -655,6 +707,7 @@ const emptyForm = () => ({
undertakeDeptName: '',
projectSource: '商务洽谈',
sourceRemark: '',
changeType: '',
customerNames: '',
carrierNames: '',
fundLimit: '',
@@ -685,8 +738,38 @@ const emptyForm = () => ({
profitRemark: '',
riskPoint: '',
emergencyPlan: '',
changeContent: '',
changeReason: '',
});
const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' },
];
const normalProjectAttachmentTypeOptions = [
'利润测算表',
'单一来源供应商申请表/三方比价表',
'合同模板',
'项目操作说明',
'项目分析报告',
'项目考察报告',
].map(item => ({ label: item, value: item }));
const majorProjectAttachmentTypeOptions = [
'会议纪要',
'评审表决票',
'利润测算表',
'单一来源供应商申请表/三方比价表',
'项目操作说明',
'项目分析报告',
'项目考察报告',
'合同模板',
'上游尽职调查报告',
'下游尽职调查报告',
'业务承诺函',
].map(item => ({ label: item, value: item }));
export default {
data() {
const validateAmount = (rule, value, callback) => {
@@ -719,14 +802,22 @@ export default {
}
callback();
};
const validateUser = (rule, value, callback) => {
if (value || this.form[rule.nameProp]) {
callback();
return;
}
callback(new Error(`请选择${rule.label}`));
};
return {
api,
config,
projectTypeOptions,
projectSourceOptions,
transportTypeOptions,
transportTypeOptions: [],
businessTypeOptions,
settlementModeOptions,
changeTypeOptions,
tableForm: {},
tableOption: this.buildTableOption(),
loading: true,
@@ -746,6 +837,7 @@ export default {
submitLoading: false,
form: emptyForm(),
formRules: {
changeType: [{ required: true, message: '请选择变更类型', trigger: 'change' }],
projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
@@ -784,8 +876,30 @@ export default {
projectScale: [{ label: '项目规模', validator: validateAmount, trigger: 'blur' }],
estimatedProfit: [{ label: '预估利润', validator: validateAmount, trigger: 'blur' }],
fundDemand: [{ label: '资金需求', validator: validateAmount, trigger: 'blur' }],
handlerUserId: [{ required: true, message: '请选择项目经办人', trigger: 'change' }],
principalUserId: [{ required: true, message: '请选择项目负责人', trigger: 'change' }],
handlerUserId: [
{
label: '项目经办人',
nameProp: 'handlerUserName',
validator: validateUser,
trigger: 'change',
},
],
principalUserId: [
{
label: '项目负责人',
nameProp: 'principalUserName',
validator: validateUser,
trigger: 'change',
},
],
changeContent: [
{ required: true, message: '请输入变更内容', trigger: 'blur' },
{ max: 2000, message: '变更内容不能超过2000个字符', trigger: 'blur' },
],
changeReason: [
{ required: true, message: '请输入变更原因', trigger: 'blur' },
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
],
},
deptOptions: [],
cargoTypeOptions: [],
@@ -852,29 +966,56 @@ export default {
canCreate() {
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
},
canMajorProjectSupplement() {
return this.hasPermission(`${this.config.permission}_supplement`) && !this.readonlyScope;
},
isMajorProjectSupplementDialog() {
return this.dialogType === 'majorSupplement';
},
isChangeDialog() {
return this.dialogType === 'change';
},
isBasicInfoReadonly() {
return this.dialogReadonly || this.isChangeDialog;
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
projectDialogTitle() {
if (this.isChangeDialog) {
return '项目变更';
}
if (this.isMajorProjectSupplementDialog) {
return '重大项目补录';
}
const text = { add: '新增', edit: '编辑', view: '查看' }[this.dialogType] || '新增';
return `${text}${this.config.title}`;
},
projectAttachmentTypeOptions() {
return this.form.projectType === '重大项目'
? majorProjectAttachmentTypeOptions
: normalProjectAttachmentTypeOptions;
},
},
created() {
this.loadDeptOptions();
this.loadCargoTypeOptions();
this.loadTransportTypeOptions();
},
methods: {
buildTableOption() {
return {
...option,
dialogType: 'dialog',
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
column: (option.column || []).map(column => ({ ...column, display: false })),
};
return applyTableMenuWidth(
{
...option,
dialogType: 'dialog',
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
column: (option.column || []).map(column => ({ ...column, display: false })),
},
4
);
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
@@ -1029,9 +1170,17 @@ export default {
this.selectionClear();
},
handleCustomOperation(operation, row) {
if (operation.action === 'startChange') {
this.openProjectDialog('change', row);
return;
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(() => {
this.api[operation.action](...args).then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
});
@@ -1084,13 +1233,20 @@ export default {
this.dialogType = type;
this.dialogReadonly = type === 'view';
this.projectBox = true;
if (type === 'add') {
this.applyProjectDetail(emptyForm());
if (type === 'add' || type === 'majorSupplement') {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '',
});
this.fillDefaultUsers();
return;
}
this.api.getDetail(row.id).then(res => {
this.applyProjectDetail(res.data.data || {});
const detail = res.data.data || {};
this.applyProjectDetail({
...detail,
changeType: type === 'change' ? this.resolveChangeType(detail) : detail.changeType,
});
});
},
resetProjectDialog() {
@@ -1113,16 +1269,23 @@ export default {
: [],
};
const situation = this.parseSituation(row.situationRemark);
const customerRows = this.parseJsonArray(row.customerJson);
const carrierRows = this.parseJsonArray(row.carrierJson);
this.form = {
...form,
...situation,
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
};
this.customerRows = this.parseJsonArray(row.customerJson);
this.carrierRows = this.parseJsonArray(row.carrierJson);
this.customerRows = customerRows;
this.carrierRows = carrierRows;
this.attachmentRows = this.parseJsonArray(row.attachmentsJson);
this.changeRows = this.buildChangeRows(row);
this.selectedCustomerId = this.customerRows[0]?.id || '';
this.selectedCarrierIds = this.carrierRows.map(item => item.id).filter(Boolean);
this.selectedCustomerId = this.getCustomerRowId(this.customerRows[0]);
this.selectedCarrierIds = this.carrierRows
.map(item => this.getCustomerRowId(item))
.filter(Boolean);
this.syncSelectedCustomerOptions();
},
fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || '';
@@ -1135,14 +1298,28 @@ export default {
this.form.principalUserName = userName;
}
},
resolveChangeType(row = {}) {
if (row.changeType) return row.changeType;
const node = row.currentNode || '';
if (String(node).includes('备案')) return '项目备案调整';
return '项目变更';
},
submitProject() {
this.$refs.projectForm.validate(valid => {
if (!valid) return;
if (!valid) {
this.handleValidateFail();
return;
}
if (!this.validateAttachmentFileTypes()) return;
this.submitLoading = true;
this.api
.submit(this.normalizeSubmitForm())
.then(
() => {
res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
return;
}
this.$message.success('操作成功!');
this.projectBox = false;
this.onLoad(this.page, this.query);
@@ -1156,7 +1333,39 @@ export default {
});
});
},
normalizeSubmitForm() {
saveChangeProject() {
this.submitChangeForm(false);
},
submitChangeProject() {
this.submitChangeForm(true);
},
submitChangeForm(needSubmit) {
this.$refs.projectForm.validate(valid => {
if (!valid) {
this.handleValidateFail();
return;
}
if (!this.validateAttachmentFileTypes()) return;
this.submitLoading = true;
const request = needSubmit ? this.api.submitChange : this.api.saveChange;
request(this.normalizeSubmitForm({ includeChangeType: true }))
.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(
res.data?.msg || `${needSubmit ? '提交' : '保存'}失败,请联系管理员`
);
return;
}
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
this.projectBox = false;
this.onLoad(this.page, this.query);
})
.finally(() => {
this.submitLoading = false;
});
});
},
normalizeSubmitForm(options = {}) {
const [businessStartDate, businessEndDate] = this.form.businessDateRange || [];
const submitRow = {
...this.form,
@@ -1173,6 +1382,9 @@ export default {
}),
};
delete submitRow.businessDateRange;
if (!options.includeChangeType) {
delete submitRow.changeType;
}
['projectIntro', 'profitRemark', 'riskPoint', 'emergencyPlan'].forEach(
key => delete submitRow[key]
);
@@ -1183,6 +1395,35 @@ export default {
});
return submitRow;
},
normalizeAttachmentFileTypes() {
const values = this.projectAttachmentTypeOptions.map(item => item.value);
this.attachmentRows = this.attachmentRows.map(row => ({
...row,
fileType: values.includes(row.fileType) ? row.fileType : '',
}));
},
validateAttachmentFileTypes() {
const values = this.projectAttachmentTypeOptions.map(item => item.value);
const invalidIndex = this.attachmentRows.findIndex(row => !values.includes(row.fileType));
if (invalidIndex === -1) return true;
this.$message.warning(`请选择第${invalidIndex + 1}行项目材料的附件类型`);
this.$nextTick(() => {
const field = this.$el.querySelector('.project-apply-form__material-head');
if (field && typeof field.scrollIntoView === 'function') {
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
return false;
},
handleValidateFail() {
this.$message.warning('请完善表单必填项后再提交');
this.$nextTick(() => {
const field = this.$el.querySelector('.is-error');
if (field && typeof field.scrollIntoView === 'function') {
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
},
parseSituation(value) {
if (!value) {
return { projectIntro: '', profitRemark: '', riskPoint: '', emergencyPlan: '' };
@@ -1212,15 +1453,63 @@ export default {
return [];
}
},
getCustomerRowId(row = {}) {
return row.id || row.customerId || row.carrierId || '';
},
getCustomerRowLabel(row = {}, type) {
return (
row[type === 'carrier' ? 'carrier' : 'customer'] ||
row.fullName ||
row.shortName ||
row.customerName ||
row.carrierName ||
row.customerCode ||
''
);
},
getCustomerRowNames(rows = [], type) {
return rows
.map(row => this.getCustomerRowLabel(row, type))
.filter(Boolean)
.join('、');
},
buildCustomerOptionsFromRows(rows = [], type) {
return rows
.map(row => {
const value = this.getCustomerRowId(row);
const label = this.getCustomerRowLabel(row, type);
return value && label ? { label, value, raw: row } : null;
})
.filter(Boolean);
},
mergeCustomerOptions(options = [], selectedOptions = []) {
const result = [...selectedOptions];
options.forEach(option => {
const exists = result.some(item => String(item.value) === String(option.value));
if (!exists) {
result.push(option);
}
});
return result;
},
syncSelectedCustomerOptions() {
this.customerOptions = this.mergeCustomerOptions(
this.customerOptions,
this.buildCustomerOptionsFromRows(this.customerRows, 'customer')
);
this.carrierOptions = this.mergeCustomerOptions(
this.carrierOptions,
this.buildCustomerOptionsFromRows(this.carrierRows, 'carrier')
);
},
buildChangeRows(row) {
if (!row || (!row.changeContent && !row.changeReason)) return [];
return [
{
changeDate: row.updateTime || row.createTime || '',
changeType:
row.approvalStatus && String(row.approvalStatus).includes('change')
? '项目变更'
: '项目调整备案',
row.changeType ||
(String(row.currentNode || '').includes('备案') ? '项目备案调整' : '项目变更'),
handler: row.handlerUserName || '',
content: row.changeContent || '',
reason: row.changeReason || '',
@@ -1239,6 +1528,10 @@ export default {
this.form.undertakeDeptName = dept ? dept.rawLabel : '';
this.$refs.projectForm?.validateField('undertakeDeptId');
},
handleProjectTypeChange() {
this.normalizeAttachmentFileTypes();
this.$refs.projectForm?.validateField('projectType');
},
loadDeptOptions() {
getDeptTree().then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
@@ -1259,13 +1552,21 @@ export default {
}, []);
},
loadCargoTypeOptions() {
getDictionary({ code: 'type_of_goods' }).then(res => {
getSystemDictionary({ code: 'type_of_goods' }).then(res => {
this.cargoTypeOptions = (res.data.data || []).map(item => ({
label: item.dictValue,
value: item.dictKey,
}));
});
},
loadTransportTypeOptions() {
getBizDictionary({ code: 'transport_type' }).then(res => {
this.transportTypeOptions = (res.data.data || []).map(item => ({
label: item.dictValue,
value: item.dictKey,
}));
});
},
loadCustomerOptions(type) {
const loadingKey = type === '客户' ? 'customerLoading' : 'carrierLoading';
const optionsKey = type === '客户' ? 'customerOptions' : 'carrierOptions';
@@ -1273,7 +1574,14 @@ export default {
getCustomerArchiveList(1, 100, { customerType: type, status: 1 })
.then(res => {
const records = (res.data.data && res.data.data.records) || [];
this[optionsKey] = records.map(item => this.mapCustomerOption(item));
const selectedOptions = this.buildCustomerOptionsFromRows(
type === '客户' ? this.customerRows : this.carrierRows,
type === '客户' ? 'customer' : 'carrier'
);
this[optionsKey] = this.mergeCustomerOptions(
records.map(item => this.mapCustomerOption(item)),
selectedOptions
);
})
.finally(() => {
this[loadingKey] = false;
@@ -1298,6 +1606,7 @@ export default {
this.customerRows = [row];
this.form.customerNames = row.customer;
this.form.customerJson = JSON.stringify(this.customerRows);
this.syncSelectedCustomerOptions();
this.$refs.projectForm?.validateField('customerNames');
});
},
@@ -1312,6 +1621,7 @@ export default {
this.carrierRows = rows;
this.form.carrierNames = rows.map(item => item.carrier).join('、');
this.form.carrierJson = JSON.stringify(this.carrierRows);
this.syncSelectedCustomerOptions();
this.$refs.projectForm?.validateField('carrierNames');
});
},
@@ -1353,9 +1663,10 @@ export default {
handleAttachmentChange(list) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const values = this.projectAttachmentTypeOptions.map(item => item.value);
this.attachmentRows = (list || []).map(item => ({
...item,
fileType: item.fileType || '其他',
fileType: values.includes(item.fileType) ? item.fileType : '',
description: item.description || '',
uploadUserName: item.uploadUserName || userName,
uploadTime: item.uploadTime || uploadTime,
@@ -1377,7 +1688,7 @@ export default {
this.$message.warning('附件地址为空');
return;
}
downloadFileByUrl(url, row.name || '附件');
downloadFileByUrl(url, row.originalName || row.name || '附件');
},
handleBatchDownload() {
this.attachmentRows.forEach(row => this.downloadAttachment(row));
@@ -1425,13 +1736,6 @@ export default {
</script>
<style lang="scss" scoped>
.project-apply-page {
&__scope {
margin-left: 12px;
vertical-align: middle;
}
}
.project-apply-form {
padding: 0 8px;
@@ -1523,6 +1827,15 @@ export default {
&__upload {
margin: -12px 0 24px;
}
&__change-textarea {
margin-bottom: 24px;
:deep(.el-form-item__content) {
width: 100%;
margin-left: 0 !important;
}
}
}
:global(.project-apply-dialog .el-dialog__body) {
@@ -1535,6 +1848,11 @@ export default {
display: flex;
justify-content: flex-end;
gap: 8px;
&--change {
justify-content: flex-start;
padding-left: 24px;
}
}
.project-apply-user-dialog {

View File

@@ -41,6 +41,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, reactive, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import { getList, remove, update, add, getNotice } from '@/api/desk/notice';
import { getUploadHeaders } from '@/utils/upload';
// 定义响应式数据
const form = ref({});
@@ -145,6 +146,7 @@ const option = reactive({
prop: 'content',
component: 'avue-ueditor',
action: '/blade-resource/oss/endpoint/put-file',
headers: getUploadHeaders(),
propsHttp: {
res: 'data',
url: 'link',

View File

@@ -41,6 +41,7 @@
import { getList, remove, update, add, getNotice } from '@/api/desk/notice';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import { getUploadHeaders } from '@/utils/upload';
export default {
data() {
@@ -146,6 +147,7 @@ export default {
prop: 'content',
component: 'avue-ueditor',
action: '/blade-resource/oss/endpoint/put-file',
headers: getUploadHeaders(),
propsHttp: {
res: 'data',
url: 'link',

View File

@@ -76,7 +76,7 @@
<script>
import { getList, getDetail, remove } from '@/api/resource/attach';
import { mapGetters } from 'vuex';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { UploadFilled } from '@element-plus/icons-vue';
import func from '@/utils/func';
@@ -98,10 +98,6 @@ export default {
uploading: false,
uploadPercent: 0,
selectionList: [],
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
option: {
height: 'auto',
calcHeight: 32,
@@ -194,6 +190,9 @@ export default {
};
},
computed: {
uploadHeaders() {
return getUploadHeaders();
},
func() {
return func;
},

View File

@@ -64,6 +64,7 @@ import { getList, getDetail, add, update, remove, enable } from '@/api/resource/
import { mapGetters } from 'vuex';
import func from '@/utils/func';
import { sensitive } from '@/utils/sensitive';
import { getUploadHeaders } from '@/utils/upload';
export default {
data() {
@@ -252,6 +253,7 @@ export default {
listType: 'picture-img',
dataType: 'string',
action: '/blade-resource/oss/endpoint/put-file',
headers: getUploadHeaders(),
propsHttp: {
res: 'data',
url: 'link',

View File

@@ -68,6 +68,7 @@ export default {
loading: true,
selectionList: [],
parentId: 0,
currentEditMenuId: '',
page: {
pageSize: 10,
currentPage: 1,
@@ -331,6 +332,7 @@ export default {
);
},
rowUpdate(row, index, done, loading) {
row.id = row.id || this.currentEditMenuId;
update(row).then(
() => {
this.$message({
@@ -419,8 +421,11 @@ export default {
this.form.isDisplay = 1;
}
if (['edit', 'view'].includes(type)) {
getMenu(this.form.id).then(res => {
const menuId = this.form.id;
this.currentEditMenuId = menuId;
getMenu(menuId).then(res => {
this.form = Object.assign(res.data.data, {
id: res.data.data.id || menuId,
hasChildren: this.form.hasChildren,
});
if (this.form.parentId === '0') {
@@ -432,6 +437,7 @@ export default {
},
beforeClose(done) {
this.parentId = '';
this.currentEditMenuId = '';
const column = this.findColumn(this.option.column, 'parentId');
column.value = '';
column.addDisabled = false;

View File

@@ -205,6 +205,7 @@ import { getDetail as packageDetail } from '@/api/system/tenantpackage';
import { mapGetters } from 'vuex';
import { getMenuTree } from '@/api/system/menu';
import { validatenull } from '@/utils/validate';
import { getUploadHeaders } from '@/utils/upload';
export default {
data() {
@@ -326,6 +327,7 @@ export default {
listType: 'picture-img',
dataType: 'string',
action: '/blade-resource/oss/endpoint/put-file',
headers: getUploadHeaders(),
propsHttp: {
res: 'data',
url: 'link',

View File

@@ -156,7 +156,7 @@ import {
Delete,
} from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
export default {
components: {
@@ -192,10 +192,6 @@ export default {
sensitiveManager: null,
showAvatarPreview: false,
avatarPreviewList: [],
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
};
},
created() {
@@ -208,6 +204,11 @@ export default {
this.loadUserInfo();
}
},
computed: {
uploadHeaders() {
return getUploadHeaders();
},
},
methods: {
// Tab切换处理
handleTabChange(name) {

View File

@@ -100,12 +100,7 @@
>
查看
</el-button>
<el-button
type="primary"
text
v-if="hasPermission('driver_edit')"
@click="openDriver(row)"
>
<el-button type="primary" text v-if="hasPermission('driver_edit')" @click="openDriver(row)">
修改
</el-button>
<el-button
@@ -222,7 +217,7 @@
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('idCardFront', url)"
@success="url => handleIdCardUploadSuccess('idCardFront', url)"
/>
</el-col>
<el-col :span="8">
@@ -233,7 +228,7 @@
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('idCardBack', url)"
@success="url => handleIdCardUploadSuccess('idCardBack', url)"
/>
</el-col>
<el-col :span="8">
@@ -317,7 +312,10 @@
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('drivingLicenseFront', url)"
@success="
(url, file) =>
handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
"
/>
</el-col>
<el-col :span="12">
@@ -328,7 +326,9 @@
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('drivingLicenseBack', url)"
@success="
(url, file) => handleDrivingLicenseUploadSuccess('drivingLicenseBack', url, file)
"
/>
</el-col>
</el-row>
@@ -483,6 +483,7 @@
<script>
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import { ElLoading } from 'element-plus';
import { option } from '@/option/transportCapacity/driver';
import {
getList,
@@ -491,10 +492,13 @@ import {
remove,
changeStatus,
getExpiryStat,
recognitionIDCard,
recognitionTransportCertificates,
} from '@/api/transportCapacity/driver';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
@@ -565,10 +569,8 @@ export default {
driverBox: false,
readonly: false,
driverForm: emptyForm(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
drivingLicenseUploads: {},
uploadHeaders: getUploadHeaders(),
nationOptions: [
'汉族',
'蒙古族',
@@ -773,6 +775,7 @@ export default {
},
resetDriver() {
this.driverForm = emptyForm();
this.drivingLicenseUploads = {};
this.readonly = false;
this.submitLoading = false;
this.$refs.driverForm?.clearValidate();
@@ -781,6 +784,230 @@ export default {
this.driverForm[prop] = url;
this.$refs.driverForm?.validateField(prop);
},
handleIdCardUploadSuccess(prop, url) {
this.setImage(prop, url);
this.recognizeIdCard(url);
},
handleDrivingLicenseUploadSuccess(prop, url, file = {}) {
this.setImage(prop, url);
this.drivingLicenseUploads[prop] = this.getUploadRecognitionKey(file, url);
this.recognizeDrivingLicense();
},
recognizeIdCard(url = '') {
if (!url) {
this.$message.warning('身份证图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '身份证识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionIDCard(url)
.then(res => {
const data = res.data.data || {};
this.applyIdCardRecognition(data);
this.$message.success('身份证识别完成');
})
.catch(() => {
this.$message.warning('身份证图片上传成功,自动识别失败,请手动填写身份信息');
})
.finally(() => {
loading.close();
});
},
recognizeDrivingLicense() {
const objectKeys = [
this.drivingLicenseUploads.drivingLicenseFront || this.driverForm.drivingLicenseFront,
this.drivingLicenseUploads.drivingLicenseBack || this.driverForm.drivingLicenseBack,
].filter(Boolean);
if (!objectKeys.length) {
this.$message.warning('驾驶证图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '驾驶证识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionTransportCertificates(objectKeys)
.then(res => {
const data = res.data.data || {};
this.applyDrivingLicenseRecognition(data);
this.$message.success('驾驶证识别完成');
})
.catch(() => {
this.$message.warning('驾驶证图片上传成功,自动识别失败,请手动填写驾驶证信息');
})
.finally(() => {
loading.close();
});
},
applyDrivingLicenseRecognition(data = {}) {
const driverName = data.driverLicenseName || data.name || this.getOcrValue(data, ['name', '姓名']);
const idCardNo = String(
data.driverLicenseIdNo ||
data.idNo ||
data.idCardNo ||
this.getOcrValue(data, ['ID_no', 'idNo', 'idCardNo', '证号'])
).toUpperCase();
const drivingType =
data.driverLicenseType ||
data.drivingType ||
data.driverClass ||
this.getOcrValue(data, ['class', '准驾车型']);
const drivingLicenseNo =
data.driverLicenseFileNo ||
data.drivingLicenseNo ||
data.fileNo ||
this.getOcrValue(data, ['file_no', '档案编号']);
const validDateStart =
data.driverLicenseValidDateStart ||
data.drivingLicenseStartDate ||
this.getOcrValue(data, ['date_vaild_start', '有效期限(起始时间)']);
const validDateEnd =
data.driverLicenseValidDateEnd ||
data.drivingLicenseEndDate ||
this.getOcrValue(data, ['date_vaild_end', '有效期限(终止时间)', '有效期限(终⽌时间)']);
if (driverName && !this.driverForm.driverName) {
this.driverForm.driverName = driverName;
}
if (idCardNo && !this.driverForm.idCardNo) {
this.driverForm.idCardNo = idCardNo;
}
if (idCardNo && !this.driverForm.qualificationNo) {
this.driverForm.qualificationNo = idCardNo;
}
if (drivingType) {
this.driverForm.drivingType = drivingType;
}
if (drivingLicenseNo) {
this.driverForm.drivingLicenseNo = drivingLicenseNo;
}
if (validDateStart) {
this.driverForm.drivingLicenseStartDate = this.normalizeBirthday(validDateStart);
}
if (validDateEnd) {
this.driverForm.drivingLicenseEndDate = this.normalizeBirthday(validDateEnd);
this.driverForm.drivingLicenseLongTerm = 0;
}
this.$nextTick(() => {
[
'driverName',
'idCardNo',
'drivingType',
'drivingLicenseNo',
'drivingLicenseEndDate',
'qualificationNo',
].forEach(prop => {
this.$refs.driverForm?.validateField(prop);
});
});
},
getUploadRecognitionKey(file = {}, url = '') {
return (
file.objectKey ||
file.key ||
file.fileName ||
file.name ||
file.link ||
file.url ||
file.domain ||
url ||
''
);
},
applyIdCardRecognition(data = {}) {
const driverName = data.name || data.driverName || this.getOcrValue(data, ['name', '姓名']);
const idCardNo = String(
data.idNo ||
data.idCardNo ||
data.idCard ||
this.getOcrValue(data, ['idNo', 'idCardNo', 'number', '公民身份号码', '身份证号'])
).toUpperCase();
const ocrGender = data.gender || this.getOcrValue(data, ['gender', '性别']);
const ocrNation =
data.nation ||
data.ethnicGroup ||
this.getOcrValue(data, ['ethnic_group', 'nation', '民族']);
const ocrBirthday =
data.birthday || data.birthDate || this.getOcrValue(data, ['date', '出生']);
if (driverName) {
this.driverForm.driverName = driverName;
}
if (ocrGender) {
this.driverForm.gender = ocrGender;
}
if (ocrNation) {
this.driverForm.nation = this.normalizeNation(ocrNation);
}
if (ocrBirthday) {
this.driverForm.birthday = this.normalizeBirthday(ocrBirthday);
}
if (idCardNo) {
this.driverForm.idCardNo = idCardNo;
if (!this.driverForm.qualificationNo) {
this.driverForm.qualificationNo = idCardNo;
}
const idCardInfo = this.parseIdCardInfo(idCardNo);
if (!this.driverForm.birthday && idCardInfo.birthday) {
this.driverForm.birthday = idCardInfo.birthday;
}
if (!this.driverForm.gender && idCardInfo.gender) {
this.driverForm.gender = idCardInfo.gender;
}
}
this.$nextTick(() => {
['driverName', 'idCardNo', 'birthday', 'gender', 'nation', 'qualificationNo'].forEach(
prop => {
this.$refs.driverForm?.validateField(prop);
}
);
});
},
getOcrValue(data = {}, keys = []) {
const keyInfos = data.keyInfos || data.keyInfo || data.ocrKeyInfos || [];
const list = Array.isArray(keyInfos) ? keyInfos : [];
const item = list.find(info => {
const key = String(info.key || '').toLowerCase();
const description = String(info.description || '');
return keys.some(value => key === String(value).toLowerCase() || description === value);
});
return item ? item.value || '' : '';
},
normalizeNation(value = '') {
const nation = String(value || '').trim();
if (!nation) return '';
if (this.nationOptions.includes(nation)) return nation;
const fullNation = `${nation}`;
return this.nationOptions.includes(fullNation) ? fullNation : nation;
},
normalizeBirthday(value = '') {
const birthday = String(value || '').trim();
const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return birthday;
const [, year, month, day] = match;
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
},
parseIdCardInfo(idCardNo = '') {
if (!/^\d{17}[\dX]$/.test(idCardNo)) {
return {};
}
const birthday = `${idCardNo.slice(6, 10)}-${idCardNo.slice(10, 12)}-${idCardNo.slice(
12,
14
)}`;
const date = new Date(`${birthday}T00:00:00`);
const validDate =
!Number.isNaN(date.getTime()) &&
date.getFullYear() === Number(idCardNo.slice(6, 10)) &&
date.getMonth() + 1 === Number(idCardNo.slice(10, 12)) &&
date.getDate() === Number(idCardNo.slice(12, 14));
return {
birthday: validDate ? birthday : '',
gender: Number(idCardNo.slice(16, 17)) % 2 === 1 ? '男' : '女',
};
},
validateFormField(prop) {
this.$refs.driverForm?.validateField(prop);
},
@@ -914,7 +1141,9 @@ export default {
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob('/blade-transport/driver/export-driver', this.buildExportParams(), { feedback: true })
exportBlob('/blade-transport/driver/export-driver', this.buildExportParams(), {
feedback: true,
})
.then(res => {
downloadXls(res.data, `司机管理${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})

View File

@@ -571,6 +571,7 @@ import {
} from '@/api/transportCapacity/transport-ship';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
@@ -639,10 +640,7 @@ export default {
shipBox: false,
readonly: false,
shipForm: emptyForm(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
uploadHeaders: getUploadHeaders(),
organizationOptions: [],
formRules: {
organizationName: [{ required: true, message: '请选择船舶所属组织', trigger: 'change' }],

View File

@@ -406,7 +406,10 @@
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => setImage('drivingLicenseImage', url)"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseImage', url, file)
"
/>
</el-col>
<el-col :span="8">
@@ -418,7 +421,10 @@
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => setImage('drivingLicenseMainBack', url)"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseMainBack', url, file)
"
/>
</el-col>
<el-col :span="8">
@@ -430,7 +436,10 @@
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => setImage('drivingLicenseViceFront', url)"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseViceFront', url, file)
"
/>
</el-col>
</el-row>
@@ -444,7 +453,10 @@
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => setImage('drivingLicenseViceBack', url)"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseViceBack', url, file)
"
/>
</el-col>
<el-col :span="8">
@@ -468,7 +480,10 @@
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => setImage('registrationImage', url)"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('registrationImage', url, file)
"
/>
</el-col>
</el-row>
@@ -486,6 +501,7 @@
<script>
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import { ElLoading } from 'element-plus';
import { option } from '@/option/transportCapacity/transport-vehicle';
import {
getList,
@@ -494,10 +510,12 @@ import {
remove,
changeStatus,
getExpiryStat,
recognitionTransportCertificates,
} from '@/api/transportCapacity/transport-vehicle';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
@@ -608,10 +626,8 @@ export default {
vehicleBox: false,
readonly: false,
vehicleForm: emptyForm(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
vehicleCertificateUploads: {},
uploadHeaders: getUploadHeaders(),
organizationOptions: [],
plateProvinceOptions,
plateColorOptions,
@@ -733,6 +749,7 @@ export default {
},
resetVehicle() {
this.vehicleForm = emptyForm();
this.vehicleCertificateUploads = {};
this.readonly = false;
this.submitLoading = false;
this.$refs.vehicleForm?.clearValidate();
@@ -741,6 +758,116 @@ export default {
this.vehicleForm[prop] = url;
this.$refs.vehicleForm?.validateField(prop);
},
handleVehicleCertificateUploadSuccess(prop, url, file = {}) {
this.setImage(prop, url);
this.vehicleCertificateUploads[prop] = this.getUploadRecognitionKey(file, url);
this.recognizeVehicleCertificates();
},
recognizeVehicleCertificates() {
const objectKeys = [
this.vehicleCertificateUploads.drivingLicenseImage || this.vehicleForm.drivingLicenseImage,
this.vehicleCertificateUploads.drivingLicenseMainBack ||
this.vehicleForm.drivingLicenseMainBack,
this.vehicleCertificateUploads.drivingLicenseViceFront ||
this.vehicleForm.drivingLicenseViceFront,
this.vehicleCertificateUploads.drivingLicenseViceBack ||
this.vehicleForm.drivingLicenseViceBack,
this.vehicleCertificateUploads.registrationImage || this.vehicleForm.registrationImage,
].filter(Boolean);
if (!objectKeys.length) {
this.$message.warning('车辆资质图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '车辆资质识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionTransportCertificates(objectKeys)
.then(res => {
this.applyVehicleCertificateRecognition(res.data.data || {});
this.$message.success('车辆资质识别完成');
})
.catch(() => {
this.$message.warning('车辆资质图片上传成功,自动识别失败,请手动填写车辆资质信息');
})
.finally(() => {
loading.close();
});
},
applyVehicleCertificateRecognition(data = {}) {
const plateNo = data.drivingPlateNo || data.vehicleLicensePlateNo || '';
const vehicleType = data.vehicleLicenseVehicleType || data.vehicleType || '';
const drivingLicenseNo =
data.vehicleLicenseFileNo || data.drivingLicenseNo || data.fileNo || '';
const drivingLicenseEndDate =
data.vehicleLicenseInspectionValidDate ||
data.drivingLicenseEndDate ||
data.vehicleLicenseValidDateEnd ||
'';
const registrationDate =
data.vehicleLicenseRegisterDate || data.registrationDate || data.dateRegister || '';
const registrationNo = data.registrationNo || data.vehicleLicenseNo || '';
if (plateNo) {
this.vehicleForm.plateNo = plateNo;
this.splitPlateNo();
this.syncPlateNo(false);
}
if (vehicleType) {
this.vehicleForm.vehicleType = this.normalizeVehicleType(vehicleType);
}
if (drivingLicenseNo) {
this.vehicleForm.drivingLicenseNo = drivingLicenseNo;
}
if (drivingLicenseEndDate) {
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate);
this.vehicleForm.drivingLicenseLongTerm = 0;
}
if (registrationDate) {
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate);
}
if (registrationNo) {
this.vehicleForm.registrationNo = registrationNo;
}
this.$nextTick(() => {
[
'plateNo',
'vehicleType',
'drivingLicenseNo',
'drivingLicenseEndDate',
'registrationNo',
].forEach(prop => {
this.$refs.vehicleForm?.validateField(prop);
});
});
},
getUploadRecognitionKey(file = {}, url = '') {
return (
file.objectKey ||
file.key ||
file.fileName ||
file.name ||
file.link ||
file.url ||
file.domain ||
url ||
''
);
},
normalizeDate(value = '') {
const dateValue = String(value || '').trim();
const match = dateValue.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return dateValue;
const [, year, month, day] = match;
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
},
normalizeVehicleType(value = '') {
const vehicleType = String(value || '').trim();
const option = this.vehicleTypeOptions.find(
item => item.value === vehicleType || item.label === vehicleType
);
return option ? option.value : vehicleType;
},
handlePlateBodyInput(value) {
this.vehicleForm.plateNoBody = String(value || '').toUpperCase();
this.syncPlateNo();

View File

@@ -252,7 +252,7 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="营业期限" prop="businessEndDate">
<el-form-item label="营业期限" prop="businessEndDate" required>
<div class="business-term-field">
<el-date-picker
v-model="archiveForm.businessEndDate"
@@ -1122,6 +1122,7 @@ import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { ArrowRight } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
@@ -1353,7 +1354,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 320,
column: [
{
label: '客商编号',
@@ -1535,9 +1536,7 @@ export default {
return missing.length ? `缺失核心客商材料:${missing.join('、')}` : '';
},
uploadHeaders() {
return {
[this.website.tokenHeader]: getToken(),
};
return getUploadHeaders();
},
scoreList() {
return this.archiveForm.scores || [];

View File

@@ -157,6 +157,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex';
import { excelOption, option } from '@/option/vehicle/insurance-record';
@@ -225,9 +226,7 @@ export default {
};
},
uploadHeaders() {
return {
[this.website.tokenHeader]: getToken(),
};
return getUploadHeaders();
},
},
watch: {