| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- <template>
- <div class="page-container">
- <el-card shadow="never">
- <template #header>
- <div class="card-header">
- <div class="left-panel">
- <span class="title">异常上报管理</span>
- </div>
- <div class="right-panel" style="display: flex; align-items: center; gap: 10px">
- <el-input v-model="searchKey" placeholder="订单号/履约者姓名" prefix-icon="Search" clearable style="width: 220px" />
- <el-select v-model="searchType" placeholder="异常类型" clearable style="width: 140px">
- <el-option label="无法联系用户" value="contact_fail" />
- <el-option label="地址错误" value="addr_error" />
- <el-option label="宠物攻击性强" value="pet_aggressive" />
- <el-option label="天气原因" value="weather_delay" />
- <el-option label="突发意外" value="accident" />
- </el-select>
- <el-select v-model="searchStatus" placeholder="审核状态" clearable style="width: 120px">
- <el-option label="待审核" :value="0" />
- <el-option label="已通过" :value="1" />
- <el-option label="已驳回" :value="2" />
- </el-select>
- <el-button type="primary" icon="Plus" @click="handleAdd">新增上报</el-button>
- </div>
- </div>
- </template>
- <el-table :data="filteredTableData" style="width: 100%" v-loading="loading">
- <el-table-column prop="orderNo" label="关联订单号" width="180">
- <template #default="scope">
- <el-link type="primary" :underline="false">{{ scope.row.orderNo }}</el-link>
- </template>
- </el-table-column>
- <el-table-column prop="type" label="异常类型" width="120">
- <template #default="scope">
- <el-tag :type="getExceptionTag(scope.row.type)">{{ getExceptionLabel(scope.row.type) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="fulfillerName" label="履约者" width="150">
- <template #default="scope">
- <div>{{ scope.row.fulfillerName }}</div>
- <div style="font-size: 12px; color: #999">{{ scope.row.fulfillerPhone }}</div>
- </template>
- </el-table-column>
- <el-table-column prop="content" label="上报内容" show-overflow-tooltip />
- <el-table-column prop="images" label="上报图片" width="120">
- <template #default="scope">
- <div v-if="scope.row.images && scope.row.images.length">
- <el-image
- :src="scope.row.images[0]"
- :preview-src-list="scope.row.images"
- fit="cover"
- style="width: 40px; height: 40px; border-radius: 4px"
- preview-teleported
- >
- <template #error>
- <div class="image-slot">
- <el-icon><Picture /></el-icon>
- </div>
- </template>
- </el-image>
- <span v-if="scope.row.images.length > 1" style="font-size: 12px; color: #999; margin-left: 5px">+{{ scope.row.images.length }}</span>
- </div>
- <span v-else style="color: #ccc">无图</span>
- </template>
- </el-table-column>
- <el-table-column prop="createTime" label="提交时间" width="170" sortable />
- <el-table-column prop="status" label="审核状态" width="100">
- <template #default="scope">
- <el-tag v-if="scope.row.status === 0" type="warning">待审核</el-tag>
- <el-tag v-else-if="scope.row.status === 1" type="success">已通过</el-tag>
- <el-tag v-else type="danger">已驳回</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" fixed="right" align="center">
- <template #default="scope">
- <template v-if="scope.row.status === 0">
- <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'audit')">审核</el-button>
- <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
- <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- <template v-else>
- <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'view')">详情</el-button>
- <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </el-card>
- <!-- Audit/Detail Drawer -->
- <el-drawer v-model="drawerVisible" :title="drawerMode === 'audit' ? '异常上报审核' : '异常详情'" direction="rtl" size="600px">
- <div class="drawer-content">
- <!-- 1. Basic Info -->
- <el-descriptions title="基础信息" :column="2" border>
- <el-descriptions-item label="订单号">{{ currentItem.orderNo }}</el-descriptions-item>
- <el-descriptions-item label="提交时间">{{ currentItem.createTime }}</el-descriptions-item>
- <el-descriptions-item label="履约者">{{ currentItem.fulfillerName }}</el-descriptions-item>
- <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone }}</el-descriptions-item>
- <el-descriptions-item label="异常类型">
- <el-tag :type="getExceptionTag(currentItem.type)">{{ getExceptionLabel(currentItem.type) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="当前状态">
- <el-tag v-if="currentItem.status === 0" type="warning">待审核</el-tag>
- <el-tag v-else-if="currentItem.status === 1" type="success">已通过</el-tag>
- <el-tag v-else type="danger">已驳回</el-tag>
- </el-descriptions-item>
- </el-descriptions>
- <!-- 2. Content & Images -->
- <div class="section-block">
- <div class="section-title">上报内容</div>
- <div class="text-content">{{ currentItem.content }}</div>
- <div class="image-list" v-if="currentItem.images && currentItem.images.length">
- <el-image
- v-for="(img, idx) in currentItem.images"
- :key="idx"
- :src="img"
- :preview-src-list="currentItem.images"
- :initial-index="idx"
- fit="cover"
- class="detail-img"
- />
- </div>
- </div>
- <!-- 3. Audit Form (If Audit Mode) -->
- <div class="section-block audit-form" v-if="drawerMode === 'audit'">
- <div class="section-title">审核处理</div>
- <el-form :model="auditForm" label-position="top">
- <el-form-item label="审核结果" required>
- <el-radio-group v-model="auditForm.result">
- <el-radio :label="1" border>通过</el-radio>
- <el-radio :label="2" border>驳回</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="处理备注">
- <el-input v-model="auditForm.remark" type="textarea" :rows="3" placeholder="请输入审核意见 (通过或驳回均可选填)" />
- </el-form-item>
- </el-form>
- <div class="drawer-footer-actions">
- <el-button @click="drawerVisible = false">取消</el-button>
- <el-button type="primary" @click="submitAudit">提交审核</el-button>
- </div>
- </div>
- <!-- 4. Log Record (All Modes) -->
- <div class="section-block">
- <div class="section-title">处理记录</div>
- <el-timeline>
- <el-timeline-item :timestamp="currentItem.createTime" placement="top" type="primary">
- <el-card shadow="never" class="log-card">
- <h4>提交上报</h4>
- <p>履约者 {{ currentItem.fulfillerName }} 提交了异常上报</p>
- </el-card>
- </el-timeline-item>
- <el-timeline-item
- v-if="currentItem.status !== 0"
- :timestamp="currentItem.auditTime || '2026-02-05 18:00:00'"
- placement="top"
- :type="currentItem.status === 1 ? 'success' : 'danger'"
- >
- <el-card shadow="never" class="log-card">
- <h4>{{ currentItem.status === 1 ? '审核通过' : '审核驳回' }}</h4>
- <p>操作人:系统管理员</p>
- <p>备注:{{ currentItem.auditRemark || '无' }}</p>
- </el-card>
- </el-timeline-item>
- </el-timeline>
- </div>
- </div>
- </el-drawer>
- <!-- Edit/Add Dialog (Keep for Edit) -->
- <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑异常' : '新增异常'" width="600px">
- <el-form :model="form" label-width="100px">
- <el-form-item label="关联订单" required>
- <el-input v-model="form.orderNo" placeholder="请输入订单号" />
- </el-form-item>
- <el-form-item label="履约者" required>
- <el-select v-model="form.fulfillerId" filterable placeholder="请输入姓名/电话检索" style="width: 100%" @change="handleFulfillerSelect">
- <el-option v-for="item in fulfillerOptions" :key="item.id" :label="item.name + ' - ' + item.phone" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="异常类型" required>
- <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
- <el-option label="无法联系用户" value="contact_fail" />
- <el-option label="地址错误/不存在" value="addr_error" />
- <el-option label="宠物攻击性强" value="pet_aggressive" />
- <el-option label="恶劣天气延迟" value="weather_delay" />
- <el-option label="突发意外" value="accident" />
- </el-select>
- </el-form-item>
- <el-form-item label="上报内容" required>
- <el-input v-model="form.content" type="textarea" :rows="3" placeholder="请详细描述异常情况..." />
- </el-form-item>
- <el-form-item label="现场图片">
- <el-upload
- action="#"
- list-type="picture-card"
- :auto-upload="false"
- v-model:file-list="fileList"
- :on-change="handleFileChange"
- :on-remove="handleFileRemove"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- </el-form-item>
- </el-form>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="saveData">保存</el-button>
- </span>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed } from 'vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- const loading = ref(false);
- const searchKey = ref('');
- const searchType = ref('');
- const searchStatus = ref('');
- const currentPage = ref(1);
- const pageSize = ref(10);
- const total = ref(42);
- // Dialog State (For Add/Edit)
- const dialogVisible = ref(false);
- const isEdit = ref(false);
- const fileList = ref([]);
- // Mock Fulfiller Search Options
- const fulfillerOptions = ref([
- { id: 101, name: '李建国', phone: '13812341234' },
- { id: 102, name: '王大力', phone: '13987654321' },
- { id: 103, name: '张小妹', phone: '13766668888' },
- { id: 104, name: '刘跑跑', phone: '13611112222' }
- ]);
- const handleFulfillerSelect = (id) => {
- const target = fulfillerOptions.value.find((item) => item.id === id);
- if (target) {
- form.fulfillerName = target.name;
- form.fulfillerPhone = target.phone;
- }
- };
- // Drawer State (For Audit/View)
- const drawerVisible = ref(false);
- const drawerMode = ref('view'); // 'audit' or 'view'
- const currentItem = ref({});
- const auditForm = reactive({ result: 1, remark: '' });
- const form = reactive({
- id: '',
- orderNo: '',
- fulfillerId: '',
- fulfillerName: '',
- fulfillerPhone: '',
- type: '',
- content: '',
- images: [],
- status: 0,
- auditRemark: ''
- });
- // Mock Data
- const tableData = ref([
- {
- id: 1,
- orderNo: 'ORD202402048805',
- fulfillerName: '李建国',
- fulfillerPhone: '13812341234',
- type: 'contact_fail',
- content: '到达指定地点后,多次拨打宠主电话无人接听,敲门无响应超过15分钟。',
- images: [
- 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
- 'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png'
- ],
- status: 0,
- createTime: '2026-02-04 14:30:00',
- auditRemark: '',
- auditTime: ''
- },
- {
- id: 2,
- orderNo: 'ORD202402048802',
- fulfillerName: '王大力',
- fulfillerPhone: '13987654321',
- type: 'pet_aggressive',
- content: '进门时金毛表现出强烈护食行为,试图攻击,无法进行喂食操作。',
- images: [],
- status: 1,
- auditRemark: '已确认,取消本次订单,按此单结算跑腿费。',
- createTime: '2026-02-04 10:15:00',
- auditTime: '2026-02-04 11:00:00'
- }
- ]);
- const filteredTableData = computed(() => {
- return tableData.value.filter((item) => {
- const matchKey = !searchKey.value || item.orderNo.includes(searchKey.value) || item.fulfillerName.includes(searchKey.value);
- const matchType = !searchType.value || item.type === searchType.value;
- const matchStatus = searchStatus.value === '' || item.status === searchStatus.value;
- return matchKey && matchType && matchStatus;
- });
- });
- // Helpers
- const getExceptionLabel = (type) => {
- const map = {
- 'contact_fail': '无法联系用户',
- 'addr_error': '地址错误',
- 'pet_aggressive': '宠物攻击性强',
- 'weather_delay': '恶劣天气延迟',
- 'accident': '突发意外'
- };
- return map[type] || '其他';
- };
- const getExceptionTag = (type) => {
- const map = {
- 'contact_fail': 'warning',
- 'addr_error': 'info',
- 'pet_aggressive': 'danger',
- 'weather_delay': 'primary',
- 'accident': 'danger'
- };
- return map[type] || 'info';
- };
- // Actions
- const handleAdd = () => {
- isEdit.value = false;
- fileList.value = [];
- Object.assign(form, {
- id: '',
- orderNo: '',
- fulfillerId: '',
- fulfillerName: '',
- fulfillerPhone: '',
- type: '',
- content: '',
- images: [],
- status: 0,
- auditRemark: ''
- });
- dialogVisible.value = true;
- };
- const handleEdit = (row) => {
- isEdit.value = true;
- Object.assign(form, JSON.parse(JSON.stringify(row)));
- fileList.value = (row.images || []).map((url, i) => ({ name: `img${i}`, url: url }));
- dialogVisible.value = true;
- };
- const handleDelete = (row) => {
- ElMessageBox.confirm('确定删除该条记录吗?', '警告', { type: 'warning' }).then(() => {
- tableData.value = tableData.value.filter((item) => item.id !== row.id);
- ElMessage.success('删除成功');
- });
- };
- // Open Drawer for Audit or View
- const handleOpenDrawer = (row, mode) => {
- drawerMode.value = mode;
- currentItem.value = JSON.parse(JSON.stringify(row));
- // Reset audit form
- if (mode === 'audit') {
- auditForm.result = 1;
- auditForm.remark = '';
- }
- drawerVisible.value = true;
- };
- const submitAudit = () => {
- const target = tableData.value.find((i) => i.id === currentItem.value.id);
- if (target) {
- target.status = auditForm.result;
- target.auditRemark = auditForm.remark;
- target.auditTime = new Date().toLocaleString();
- ElMessage.success('审核已提交');
- drawerVisible.value = false;
- }
- };
- const saveData = () => {
- if (!form.orderNo || !form.type) return ElMessage.warning('请填写必填项');
- form.images = fileList.value.map((f) => f.url || 'https://placeholder.com/img');
- if (isEdit.value) {
- const idx = tableData.value.findIndex((i) => i.id === form.id);
- if (idx !== -1) Object.assign(tableData.value[idx], form);
- } else {
- tableData.value.unshift({
- id: Date.now(),
- ...form,
- createTime: new Date().toLocaleString()
- });
- }
- ElMessage.success('保存成功');
- dialogVisible.value = false;
- };
- const handleFileChange = (uploadFile, uploadFiles) => {
- fileList.value = uploadFiles;
- };
- const handleFileRemove = (uploadFile, uploadFiles) => {
- fileList.value = uploadFiles;
- };
- const handleSizeChange = (val) => {};
- const handleCurrentChange = (val) => {};
- </script>
- <style scoped>
- .page-container {
- padding: 20px;
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .title {
- font-weight: bold;
- }
- .pagination-container {
- margin-top: 20px;
- display: flex;
- justify-content: flex-end;
- }
- /* Drawer Styles */
- .drawer-content {
- padding: 0 10px;
- }
- .section-block {
- margin-top: 25px;
- }
- .section-title {
- font-weight: bold;
- border-left: 4px solid #409eff;
- padding-left: 10px;
- margin-bottom: 15px;
- font-size: 15px;
- color: #303133;
- }
- .text-content {
- background: #f8f9fa;
- padding: 12px;
- border-radius: 4px;
- color: #606266;
- line-height: 1.6;
- }
- .image-list {
- margin-top: 15px;
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- }
- .detail-img {
- width: 100px;
- height: 100px;
- border-radius: 6px;
- cursor: pointer;
- }
- .audit-form {
- background: #f0f9eb;
- padding: 15px;
- border-radius: 8px;
- border: 1px solid #e1f3d8;
- }
- .drawer-footer-actions {
- display: flex;
- justify-content: flex-end;
- margin-top: 15px;
- grid-gap: 10px;
- }
- .log-card {
- margin-bottom: 5px;
- }
- .log-card h4 {
- margin: 0 0 5px;
- font-size: 14px;
- color: #303133;
- }
- .log-card p {
- margin: 0;
- font-size: 12px;
- color: #909399;
- line-height: 1.4;
- }
- </style>
|