6955784e93
Co-authored-by: Cursor <cursoragent@cursor.com>
1029 lines
33 KiB
Vue
1029 lines
33 KiB
Vue
<template>
|
||
<basic-container class="exception-disposal-page">
|
||
<section class="exception-disposal-page__search">
|
||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||
<div class="exception-disposal-page__search-grid">
|
||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||
<el-date-picker
|
||
v-if="field.type === 'daterange'"
|
||
v-model="query[field.prop]"
|
||
type="daterange"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="请选择"
|
||
end-placeholder="请选择"
|
||
range-separator="-"
|
||
/>
|
||
<el-cascader
|
||
v-else-if="field.prop === 'exceptionType'"
|
||
v-model="query[field.prop]"
|
||
:options="field.options"
|
||
clearable
|
||
filterable
|
||
placeholder="全部"
|
||
:props="{ emitPath: false }"
|
||
/>
|
||
<el-select
|
||
v-else-if="field.type === 'select'"
|
||
v-model="query[field.prop]"
|
||
clearable
|
||
placeholder="全部"
|
||
>
|
||
<el-option
|
||
v-for="item in field.options"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<div class="exception-disposal-page__search-actions">
|
||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||
{{ searchExpanded ? '收起' : '展开' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</section>
|
||
|
||
<section class="exception-disposal-page__table-panel">
|
||
<div class="exception-disposal-page__toolbar">
|
||
<div>
|
||
<el-button
|
||
v-if="permission.exception_disposal_batch_complete"
|
||
type="primary"
|
||
plain
|
||
@click="handleBatchComplete"
|
||
>
|
||
批量完成
|
||
</el-button>
|
||
</div>
|
||
<div class="exception-disposal-page__toolbar-right">
|
||
<el-tooltip content="刷新" placement="top">
|
||
<el-button :icon="Refresh" text @click="loadTable" />
|
||
</el-tooltip>
|
||
<el-tooltip content="表格密度" placement="top">
|
||
<el-button :icon="Rank" text @click="noop" />
|
||
</el-tooltip>
|
||
<el-tooltip content="列设置" placement="top">
|
||
<el-button :icon="Setting" text @click="noop" />
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="rows"
|
||
border
|
||
class="exception-disposal-page__table"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||
<el-table-column type="index" label="序号" width="72" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-for="column in tableColumns"
|
||
:key="column.prop"
|
||
:prop="column.prop"
|
||
:label="column.label"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<el-link
|
||
v-if="column.link && row[column.prop]"
|
||
type="primary"
|
||
@click="openWaybill(row)"
|
||
>
|
||
{{ row[column.prop] }}
|
||
</el-link>
|
||
<el-link
|
||
v-else-if="column.prop === 'disposalStatusName'"
|
||
:type="statusLinkType(row.disposalStatus)"
|
||
@click="openDisposal(row, row.disposalStatus === 'completed' ? 'detail' : 'follow')"
|
||
>
|
||
{{ row[column.prop] || '-' }}
|
||
</el-link>
|
||
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="exception-disposal-page__actions">
|
||
<template v-if="row.disposalStatus !== 'completed'">
|
||
<el-link
|
||
v-if="permission.exception_disposal_follow"
|
||
type="primary"
|
||
@click="openDisposal(row, 'follow')"
|
||
>
|
||
跟进
|
||
</el-link>
|
||
<el-link
|
||
v-if="permission.exception_disposal_edit"
|
||
type="primary"
|
||
@click="openDisposal(row, 'edit')"
|
||
>
|
||
编辑
|
||
</el-link>
|
||
<el-link
|
||
v-if="permission.exception_disposal_complete"
|
||
type="primary"
|
||
@click="completeRow(row)"
|
||
>
|
||
完成
|
||
</el-link>
|
||
</template>
|
||
<template v-else>
|
||
<el-link
|
||
v-if="permission.exception_disposal_view"
|
||
type="primary"
|
||
@click="openDisposal(row, 'detail')"
|
||
>
|
||
详情
|
||
</el-link>
|
||
<el-link
|
||
v-if="permission.exception_disposal_edit"
|
||
type="primary"
|
||
@click="openDisposal(row, 'edit')"
|
||
>
|
||
编辑
|
||
</el-link>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="exception-disposal-page__pagination">
|
||
<el-pagination
|
||
v-model:current-page="page.current"
|
||
v-model:page-size="page.size"
|
||
:total="page.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="loadTable"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<el-dialog
|
||
v-model="dialog.visible"
|
||
:title="dialogTitle"
|
||
:width="dialog.mode === 'edit' ? '1080px' : '960px'"
|
||
top="6vh"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<div v-loading="dialog.loading" class="exception-disposal-page__dialog">
|
||
<!-- 编辑态:只呈现运单信息与可编辑项,不再重复详情页的只读信息 -->
|
||
<el-form
|
||
v-if="dialog.mode === 'edit'"
|
||
ref="editFormRef"
|
||
:model="editForm"
|
||
:rules="editRules"
|
||
label-position="right"
|
||
label-width="96px"
|
||
>
|
||
<section-card title="运单信息">
|
||
<el-descriptions :column="3" border size="small">
|
||
<el-descriptions-item
|
||
v-for="field in editWaybillFields"
|
||
:key="field.prop"
|
||
:label="field.label"
|
||
>
|
||
{{ detailValue(field.prop) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</section-card>
|
||
|
||
<section-card title="异常信息">
|
||
<el-row :gutter="18">
|
||
<el-col :span="12">
|
||
<el-form-item label="上报人">
|
||
<span class="exception-disposal-page__readonly">{{
|
||
detailValue('reporterName')
|
||
}}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="上报时间">
|
||
<span class="exception-disposal-page__readonly">{{
|
||
detailValue('reportTime')
|
||
}}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="异常节点">
|
||
<span class="exception-disposal-page__readonly">{{
|
||
detailValue('nodeName')
|
||
}}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="异常类型" prop="exceptionType">
|
||
<el-select
|
||
v-model="editForm.exceptionType"
|
||
placeholder="请选择"
|
||
@change="handleEditTypeChange"
|
||
>
|
||
<el-option
|
||
v-for="item in exceptionTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="异常原因" prop="exceptionReason">
|
||
<el-select
|
||
v-model="editForm.exceptionReason"
|
||
clearable
|
||
placeholder="请选择"
|
||
:disabled="!editForm.exceptionType"
|
||
>
|
||
<el-option
|
||
v-for="item in editReasonOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="上报说明" prop="reportDescription">
|
||
<el-input
|
||
v-model="editForm.reportDescription"
|
||
type="textarea"
|
||
maxlength="500"
|
||
show-word-limit
|
||
:rows="3"
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="现场照片">
|
||
<div class="exception-disposal-page__edit-photos">
|
||
<div
|
||
v-for="(url, index) in editForm.scenePhotoList"
|
||
:key="url"
|
||
class="exception-disposal-page__edit-photo"
|
||
>
|
||
<el-image
|
||
:src="url"
|
||
:preview-src-list="editForm.scenePhotoList"
|
||
:initial-index="index"
|
||
fit="cover"
|
||
/>
|
||
<el-link type="danger" :underline="false" @click="removeEditPhoto(index)"
|
||
>删除</el-link
|
||
>
|
||
</div>
|
||
<el-upload
|
||
action="/api/blade-resource/oss/endpoint/put-file"
|
||
name="file"
|
||
:headers="uploadHeaders"
|
||
:show-file-list="false"
|
||
:before-upload="beforePhotoUpload"
|
||
:on-success="handlePhotoSuccess"
|
||
:on-error="handlePhotoError"
|
||
>
|
||
<div class="exception-disposal-page__edit-photo-add">+ 添加</div>
|
||
</el-upload>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</section-card>
|
||
</el-form>
|
||
|
||
<template v-else>
|
||
<section-card title="运单信息">
|
||
<el-descriptions :column="3" border size="small">
|
||
<el-descriptions-item
|
||
v-for="field in detailWaybillFields"
|
||
:key="field.prop"
|
||
:label="field.label"
|
||
>
|
||
{{ detailValue(field.prop) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</section-card>
|
||
|
||
<section-card title="异常信息">
|
||
<el-descriptions :column="2" border size="small">
|
||
<el-descriptions-item
|
||
v-for="field in detailExceptionFields"
|
||
:key="field.prop"
|
||
:label="field.label"
|
||
:span="field.span === 2 ? 2 : 1"
|
||
>
|
||
{{ detailValue(field.prop) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</section-card>
|
||
|
||
<section-card title="现场照片">
|
||
<div class="exception-disposal-page__photos">
|
||
<el-image
|
||
v-for="(url, index) in photoUrls"
|
||
:key="url"
|
||
:src="url"
|
||
:preview-src-list="photoUrls"
|
||
:initial-index="index"
|
||
fit="cover"
|
||
/>
|
||
<el-empty v-if="!photoUrls.length" description="暂无现场照片" :image-size="56" />
|
||
</div>
|
||
</section-card>
|
||
|
||
<section-card v-if="dialog.mode === 'follow'" title="跟进">
|
||
<el-form
|
||
ref="followFormRef"
|
||
:model="followForm"
|
||
:rules="followRules"
|
||
label-position="right"
|
||
label-width="96px"
|
||
>
|
||
<el-row :gutter="18">
|
||
<el-col :span="24">
|
||
<el-form-item label="跟进说明" prop="followContent">
|
||
<el-input
|
||
v-model="followForm.followContent"
|
||
type="textarea"
|
||
maxlength="500"
|
||
show-word-limit
|
||
:rows="3"
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="跟进附件">
|
||
<vehicle-attachment-upload
|
||
v-model="followForm.attachments"
|
||
:limit="20"
|
||
:max-size="500"
|
||
button-text="上传附件"
|
||
:show-tip="false"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</section-card>
|
||
|
||
<section-card title="历史跟进">
|
||
<el-table :data="detail.followRecords || []" border>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column
|
||
v-for="column in followColumns"
|
||
:key="column.prop"
|
||
:prop="column.prop"
|
||
:label="column.label"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="附件" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
<template v-if="parseAttachments(row.attachmentsJson).length">
|
||
<span
|
||
v-for="(file, index) in parseAttachments(row.attachmentsJson)"
|
||
:key="index"
|
||
class="attachment-file-name exception-disposal-page__attachment-link"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="previewAttachment(file, parseAttachments(row.attachmentsJson))"
|
||
@keydown.enter="previewAttachment(file, parseAttachments(row.attachmentsJson))"
|
||
@keydown.space.prevent="
|
||
previewAttachment(file, parseAttachments(row.attachmentsJson))
|
||
"
|
||
>{{ file.name || '附件' + (index + 1) }}</span>
|
||
</template>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
</template>
|
||
</div>
|
||
<template #footer>
|
||
<el-button
|
||
v-if="dialog.mode === 'edit' && permission.exception_disposal_edit"
|
||
type="primary"
|
||
:loading="dialog.submitting"
|
||
@click="submitEdit"
|
||
>
|
||
提交
|
||
</el-button>
|
||
<el-button
|
||
v-if="dialog.mode === 'follow' && permission.exception_disposal_follow"
|
||
type="primary"
|
||
:loading="dialog.submitting"
|
||
@click="submitFollow"
|
||
>
|
||
提交
|
||
</el-button>
|
||
<el-button
|
||
v-if="dialog.mode === 'follow' && permission.exception_disposal_complete"
|
||
type="primary"
|
||
plain
|
||
@click="completeFromDialog"
|
||
>
|
||
完成
|
||
</el-button>
|
||
<el-button @click="dialog.visible = false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="attachmentDocumentPreviewVisible"
|
||
:title="attachmentPreviewFile.name || '附件预览'"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="90%"
|
||
top="4vh"
|
||
>
|
||
<pdf-preview
|
||
v-if="
|
||
attachmentDocumentPreviewVisible &&
|
||
attachmentPreviewFile.url &&
|
||
attachmentPreviewFile.isPdf
|
||
"
|
||
:source="attachmentPreviewFile.url"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
<open-file-viewer
|
||
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||
:file="attachmentPreviewFile.url"
|
||
:file-name="attachmentPreviewFile.name"
|
||
:mime-type="attachmentPreviewFile.mimeType"
|
||
width="100%"
|
||
height="72vh"
|
||
fit="contain"
|
||
theme="auto"
|
||
locale="zh-CN"
|
||
:toolbar="attachmentViewerToolbar"
|
||
:plugins="attachmentViewerPlugins"
|
||
@unsupported="handleAttachmentPreviewUnsupported"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { ArrowDown, ArrowUp, Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||
import { mapGetters } from 'vuex';
|
||
import {
|
||
detailExceptionFields,
|
||
detailWaybillFields,
|
||
disposalStatusOptions,
|
||
editWaybillFields,
|
||
exceptionCategoryOptions,
|
||
exceptionReasonOptions,
|
||
exceptionTypeOptions,
|
||
followColumns,
|
||
searchFields,
|
||
tableColumns,
|
||
} from '@/option/transit/exception-disposal';
|
||
import { getUploadHeaders } from '@/utils/upload';
|
||
import * as api from '@/api/transit/exception-disposal';
|
||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||
import { ElImageViewer } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
|
||
const attachmentName = row => row?.originalName || row?.name || row?.fileName || '附件';
|
||
const attachmentUrl = row =>
|
||
row?.url || row?.link || row?.fileUrl || row?.downloadUrl || row?.domain || '';
|
||
const attachmentExtension = row => {
|
||
const source = String(attachmentName(row) || attachmentUrl(row)).split('?')[0];
|
||
const index = source.lastIndexOf('.');
|
||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||
};
|
||
const isAttachmentImage = row =>
|
||
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(attachmentExtension(row));
|
||
const isAttachmentPdf = row => {
|
||
const mimeType = String(row.mimeType || row.contentType || '').toLowerCase();
|
||
return mimeType.includes('application/pdf') || attachmentExtension(row) === 'pdf';
|
||
};
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
|
||
export default {
|
||
components: { ElImageViewer, OpenFileViewer, PdfPreview },
|
||
data() {
|
||
return {
|
||
Rank,
|
||
Refresh,
|
||
Setting,
|
||
ArrowDown,
|
||
ArrowUp,
|
||
searchFields,
|
||
tableColumns,
|
||
detailExceptionFields,
|
||
detailWaybillFields,
|
||
followColumns,
|
||
exceptionTypeOptions,
|
||
editWaybillFields,
|
||
loading: false,
|
||
query: {},
|
||
rows: [],
|
||
selection: [],
|
||
searchExpanded: false,
|
||
page: { current: 1, size: 10, total: 0 },
|
||
dialog: { visible: false, mode: 'follow', loading: false, submitting: false },
|
||
detail: {},
|
||
followForm: { followContent: '', attachments: [] },
|
||
followRules: {
|
||
followContent: [{ max: 500, message: '跟进说明不能超过500个字符', trigger: 'blur' }],
|
||
},
|
||
editForm: {
|
||
exceptionType: '',
|
||
exceptionReason: '',
|
||
reportDescription: '',
|
||
scenePhotoList: [],
|
||
},
|
||
editRules: {
|
||
exceptionType: [{ required: true, message: '请选择异常类型', trigger: 'change' }],
|
||
reportDescription: [
|
||
{ required: true, message: '请输入上报说明', trigger: 'blur' },
|
||
{ max: 500, message: '上报说明不能超过500个字符', trigger: 'blur' },
|
||
],
|
||
},
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission']),
|
||
dialogTitle() {
|
||
return (
|
||
{ detail: '异常详情', follow: '异常跟进', edit: '编辑异常' }[this.dialog.mode] || '异常详情'
|
||
);
|
||
},
|
||
editReasonOptions() {
|
||
return exceptionReasonOptions(this.editForm.exceptionType);
|
||
},
|
||
uploadHeaders() {
|
||
return getUploadHeaders();
|
||
},
|
||
visibleSearchFields() {
|
||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||
},
|
||
photoUrls() {
|
||
return this.parsePhotos(this.detail.scenePhotos);
|
||
},
|
||
},
|
||
mounted() {
|
||
this.loadTable();
|
||
},
|
||
methods: {
|
||
async loadTable() {
|
||
this.loading = true;
|
||
try {
|
||
const res = await api.getList(this.page.current, this.page.size, this.buildQuery());
|
||
const data = this.unwrapPage(res);
|
||
this.rows = (data.records || []).map(this.decorateRow);
|
||
this.page.total = data.total || 0;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
buildQuery() {
|
||
const params = { ...this.query };
|
||
if (Array.isArray(params.reportTimeRange)) {
|
||
params.reportStartTime = params.reportTimeRange[0];
|
||
params.reportEndTime = params.reportTimeRange[1];
|
||
}
|
||
delete params.reportTimeRange;
|
||
return params;
|
||
},
|
||
handleSearch() {
|
||
this.page.current = 1;
|
||
this.loadTable();
|
||
},
|
||
handleReset() {
|
||
this.query = {};
|
||
this.handleSearch();
|
||
},
|
||
toggleSearch() {
|
||
this.searchExpanded = !this.searchExpanded;
|
||
},
|
||
handleSizeChange() {
|
||
this.page.current = 1;
|
||
this.loadTable();
|
||
},
|
||
handleSelectionChange(selection) {
|
||
this.selection = selection;
|
||
},
|
||
async openDisposal(row, mode) {
|
||
this.dialog = { visible: true, mode, loading: true, submitting: false };
|
||
this.followForm = { followContent: '', attachments: [] };
|
||
try {
|
||
const res = await api.getDetail(row.id);
|
||
this.detail = this.decorateRow(res.data?.data || res.data || res || {});
|
||
if (mode === 'edit') {
|
||
this.editForm = {
|
||
exceptionType: this.detail.exceptionType || '',
|
||
exceptionReason: this.detail.exceptionReason || '',
|
||
reportDescription: this.detail.reportDescription || '',
|
||
scenePhotoList: this.parsePhotos(this.detail.scenePhotos),
|
||
};
|
||
}
|
||
} finally {
|
||
this.dialog.loading = false;
|
||
}
|
||
},
|
||
detailValue(prop) {
|
||
const value = this.detail[prop];
|
||
return value === null || value === undefined || value === '' ? '-' : value;
|
||
},
|
||
handleEditTypeChange() {
|
||
// 一级类型变了,旧二级可能不属于新类型
|
||
const stillValid = this.editReasonOptions.some(
|
||
item => item.value === this.editForm.exceptionReason
|
||
);
|
||
if (!stillValid) {
|
||
this.editForm.exceptionReason = '';
|
||
}
|
||
},
|
||
beforePhotoUpload(file) {
|
||
const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type);
|
||
if (!validType) {
|
||
this.$message.error('仅支持 JPG、PNG、BMP 图片');
|
||
return false;
|
||
}
|
||
if (file.size / 1024 / 1024 >= 5) {
|
||
this.$message.error('图片大小不能超过 5MB');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
handlePhotoSuccess(res) {
|
||
if (res.code === 200 && res.data) {
|
||
const url = res.data.link || res.data.url || res.data.domain || '';
|
||
if (url) {
|
||
this.editForm.scenePhotoList = [...this.editForm.scenePhotoList, url];
|
||
}
|
||
} else {
|
||
this.$message.error(res.msg || '上传失败');
|
||
}
|
||
},
|
||
handlePhotoError() {
|
||
this.$message.error('上传失败');
|
||
},
|
||
removeEditPhoto(index) {
|
||
const list = [...this.editForm.scenePhotoList];
|
||
list.splice(index, 1);
|
||
this.editForm.scenePhotoList = list;
|
||
},
|
||
async submitEdit() {
|
||
await this.$refs.editFormRef.validate();
|
||
this.dialog.submitting = true;
|
||
try {
|
||
await api.update({
|
||
id: this.detail.id,
|
||
exceptionType: this.editForm.exceptionType,
|
||
exceptionReason: this.editForm.exceptionReason,
|
||
reportDescription: this.editForm.reportDescription,
|
||
scenePhotos: this.editForm.scenePhotoList.join(','),
|
||
});
|
||
this.$message.success('编辑成功');
|
||
this.dialog.visible = false;
|
||
this.loadTable();
|
||
} finally {
|
||
this.dialog.submitting = false;
|
||
}
|
||
},
|
||
async submitFollow() {
|
||
await this.$refs.followFormRef.validate();
|
||
const followContent = (this.followForm.followContent || '').trim();
|
||
const attachmentsJson = this.followForm.attachments.length
|
||
? JSON.stringify(this.followForm.attachments)
|
||
: '';
|
||
// 后端要求二者至少有其一
|
||
if (!followContent && !attachmentsJson) {
|
||
this.$message.warning('请填写跟进说明或上传附件');
|
||
return;
|
||
}
|
||
this.dialog.submitting = true;
|
||
try {
|
||
await api.follow({ id: this.detail.id, followContent, attachmentsJson });
|
||
this.$message.success('跟进成功');
|
||
this.dialog.visible = false;
|
||
this.loadTable();
|
||
} finally {
|
||
this.dialog.submitting = false;
|
||
}
|
||
},
|
||
parseAttachments(value) {
|
||
if (!value) return [];
|
||
if (Array.isArray(value)) return value;
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return Array.isArray(parsed) ? parsed.filter(item => item && item.url) : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
},
|
||
previewAttachment(row, rows = []) {
|
||
const url = attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法预览');
|
||
return;
|
||
}
|
||
if (isAttachmentImage(row)) {
|
||
this.attachmentImagePreviewUrls = (rows || [])
|
||
.filter(item => isAttachmentImage(item) && attachmentUrl(item))
|
||
.map(item => attachmentUrl(item));
|
||
this.attachmentImagePreviewIndex = Math.max(
|
||
this.attachmentImagePreviewUrls.indexOf(url),
|
||
0
|
||
);
|
||
this.attachmentImagePreviewVisible = true;
|
||
return;
|
||
}
|
||
const isPdf = isAttachmentPdf(row);
|
||
this.attachmentPreviewFile = {
|
||
name: attachmentName(row),
|
||
url,
|
||
mimeType: isPdf ? 'application/pdf' : row.mimeType || row.contentType || '',
|
||
isPdf,
|
||
};
|
||
this.attachmentDocumentPreviewVisible = true;
|
||
},
|
||
handleAttachmentPreviewUnsupported() {
|
||
this.$message.warning('当前文件暂不支持在线预览');
|
||
},
|
||
handleAttachmentPreviewError() {
|
||
this.$message.error('附件预览失败');
|
||
},
|
||
async completeRow(row) {
|
||
await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' });
|
||
await api.complete({ id: row.id });
|
||
this.$message.success('完成成功');
|
||
this.loadTable();
|
||
},
|
||
async completeFromDialog() {
|
||
await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' });
|
||
await api.complete({ id: this.detail.id });
|
||
this.$message.success('完成成功');
|
||
this.dialog.visible = false;
|
||
this.loadTable();
|
||
},
|
||
async handleBatchComplete() {
|
||
if (!this.selection.length) {
|
||
this.$message.warning('请选择需要完成的异常');
|
||
return;
|
||
}
|
||
await this.$confirm('确认批量完成已选择的异常处置?', '提示', { type: 'warning' });
|
||
await api.batchComplete(this.selection.map(item => item.id).join(','));
|
||
this.$message.success('批量完成成功');
|
||
this.loadTable();
|
||
},
|
||
openWaybill(row) {
|
||
if (row.waybillId) {
|
||
this.$router.push({
|
||
path: '/business/waybill-manage/detail',
|
||
query: { id: row.waybillId },
|
||
});
|
||
}
|
||
},
|
||
decorateRow(row) {
|
||
return {
|
||
...row,
|
||
exceptionTypeName: this.exceptionName(row.exceptionType),
|
||
disposalStatusName:
|
||
disposalStatusOptions.find(item => item.value === row.disposalStatus)?.label ||
|
||
row.disposalStatus ||
|
||
'-',
|
||
};
|
||
},
|
||
exceptionName(value) {
|
||
if (!value) return '-';
|
||
// 一级存中文大类名(如「交通异常」),二级存子类名(如「货损」),两级都查
|
||
const group = exceptionCategoryOptions.find(item => item.value === value);
|
||
if (group) return group.label;
|
||
for (const item of exceptionCategoryOptions) {
|
||
const match = item.children.find(child => child.value === value);
|
||
if (match) return match.label;
|
||
}
|
||
return value;
|
||
},
|
||
statusLinkType(status) {
|
||
return status === 'completed' ? 'info' : 'primary';
|
||
},
|
||
parsePhotos(value) {
|
||
if (!value) return [];
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
if (Array.isArray(parsed)) return parsed.filter(Boolean);
|
||
} catch (error) {
|
||
return String(value)
|
||
.split(',')
|
||
.map(item => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
return [];
|
||
},
|
||
unwrapPage(res) {
|
||
const data = res.data?.data || res.data || res || {};
|
||
return data.records ? data : { records: [], total: 0 };
|
||
},
|
||
noop() {},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.exception-disposal-page {
|
||
color: #303133;
|
||
}
|
||
|
||
.exception-disposal-page__search {
|
||
margin-bottom: 8px;
|
||
padding: 12px 12px 4px;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.el-form-item__label) {
|
||
min-width: 88px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.exception-disposal-page__search-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||
gap: 8px 24px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-cascader),
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.exception-disposal-page__search-actions {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.exception-disposal-page__table-panel {
|
||
//background: #fff;
|
||
}
|
||
|
||
.exception-disposal-page__toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 0;
|
||
}
|
||
|
||
.exception-disposal-page__toolbar-right,
|
||
.exception-disposal-page__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.exception-disposal-page__table {
|
||
--el-table-border-color: #eff1f7;
|
||
|
||
:deep(th.el-table__cell) {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
|
||
.exception-disposal-page__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.exception-disposal-page__dialog {
|
||
padding: 4px 0 0;
|
||
}
|
||
|
||
.exception-disposal-page__attachment-link {
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.exception-disposal-page__readonly {
|
||
color: #606266;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.exception-disposal-page__photos {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
min-height: 80px;
|
||
|
||
:deep(.el-image) {
|
||
width: 96px;
|
||
height: 96px;
|
||
border: 1px solid #eff1f7;
|
||
}
|
||
}
|
||
|
||
.exception-disposal-page__dialog :deep(.section-card + .section-card) {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.exception-disposal-page__edit-photos {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.exception-disposal-page__edit-photo {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
:deep(.el-image) {
|
||
width: 96px;
|
||
height: 96px;
|
||
border: 1px solid #eff1f7;
|
||
}
|
||
}
|
||
|
||
.exception-disposal-page__edit-photo-add {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 96px;
|
||
height: 96px;
|
||
border: 1px dashed #dcdfe6;
|
||
border-radius: 4px;
|
||
color: #909399;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
border-color: #409eff;
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 1200px) {
|
||
.exception-disposal-page__search-grid {
|
||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 760px) {
|
||
.exception-disposal-page__search-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|