| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- <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="queryParams.content" placeholder="订单号/履约者姓名" prefix-icon="Search" clearable @keyup.enter="handleQuery" style="width: 220px" />
- <el-select v-model="queryParams.type" placeholder="异常类型" clearable @change="handleQuery" style="width: 140px">
- <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- <el-select v-model="queryParams.status" placeholder="审核状态" clearable @change="handleQuery" 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="Search" @click="handleQuery">查询</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['fulfiller:anamaly:add']">新增上报</el-button>
- </div>
- </div>
- </template>
- <el-table :data="tableData" style="width: 100%" v-loading="loading">
- <el-table-column prop="orderCode" label="关联订单号" width="180">
- <template #default="scope">
- <el-link type="primary" :underline="false">{{ scope.row.orderCode }}</el-link>
- </template>
- </el-table-column>
- <el-table-column prop="storeName" label="所属门店" width="150" show-overflow-tooltip>
- <template #default="scope">
- <span>{{ scope.row.storeName || '-' }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="type" label="异常类型" width="120">
- <template #default="scope">
- <dict-tag :options="flf_anamaly_type" :value="scope.row.type" />
- </template>
- </el-table-column>
- <el-table-column prop="fulfiller" label="履约者" width="150">
- <template #default="scope">
- <div>{{ scope.row.fulfillerName || scope.row.fulfiller }}</div>
- <div style="font-size: 12px; color: #999" v-if="scope.row.fulfillerPhone">{{ scope.row.fulfillerPhone }}</div>
- </template>
- </el-table-column>
- <el-table-column prop="content" label="上报内容" show-overflow-tooltip />
- <el-table-column prop="photosUrls" label="上报图片" width="120">
- <template #default="scope">
- <div v-if="scope.row.photosUrls && scope.row.photosUrls.length">
- <el-image
- :src="scope.row.photosUrls[0]"
- :preview-src-list="scope.row.photosUrls"
- 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.photosUrls.length > 1" style="font-size: 12px; color: #999; margin-left: 5px">+{{ scope.row.photosUrls.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')" v-hasPermi="['fulfiller:anamaly:audit']">审核</el-button>
- <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:anamaly:edit']">编辑</el-button>
- <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
- </template>
- <template v-else>
- <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'view')" v-hasPermi="['fulfiller:anamaly:query']">详情</el-button>
- <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
- </template>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="queryParams.pageNum"
- v-model:page-size="queryParams.pageSize"
- :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="getList"
- @current-change="getList"
- />
- </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.orderCode }}</el-descriptions-item>
- <el-descriptions-item label="所属门店">{{ currentItem.storeName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="提交时间">{{ currentItem.createTime }}</el-descriptions-item>
- <el-descriptions-item label="履约者">{{ currentItem.fulfillerName || currentItem.fulfiller }}</el-descriptions-item>
- <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone || '-' }}</el-descriptions-item>
- <el-descriptions-item label="异常类型">
- <dict-tag :options="flf_anamaly_type" :value="currentItem.type" />
- </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.photosUrls && currentItem.photosUrls.length">
- <el-image
- v-for="(img, idx) in currentItem.photosUrls"
- :key="idx"
- :src="img"
- :preview-src-list="currentItem.photosUrls"
- :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" v-hasPermi="['fulfiller:anamaly:audit']">提交审核</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>
- <p>内容:提交了异常上报</p>
- </el-card>
- </el-timeline-item>
- <el-timeline-item
- v-if="currentItem.status !== 0"
- :timestamp="currentItem.auditTime || '未知时间'"
- placement="top"
- :type="currentItem.status === 1 ? 'success' : 'danger'"
- >
- <el-card shadow="never" class="log-card">
- <h4>{{ currentItem.status === 1 ? '审核通过' : '审核驳回' }}</h4>
- <p>操作人:{{ currentItem.auditorName || '未知' }}</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.orderCode" placeholder="请输入订单号" :disabled="isEdit" />
- </el-form-item>
- <el-form-item label="履约者" required>
- <PageSelect
- v-model="form.fulfiller"
- :options="fulfillerOptions"
- :total="fulfillerTotal"
- :pageSize="fulfillerQueryParams.pageSize"
- placeholder="请输入姓名/电话检索"
- :filter-method="handleFulfillerSearch"
- @page-change="handleFulfillerPageChange"
- @change="handleFulfillerSelect"
- style="width: 100%"
- :disabled="isEdit"
- />
- </el-form-item>
- <el-form-item label="异常类型" required>
- <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
- <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
- </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="现场图片">
- <ImageUpload v-model="form.photos" />
- </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 lang="ts">
- import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance, toRefs, watch } from 'vue';
- import { useRoute } from 'vue-router';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import { getAnamalyList, addAnamaly, auditAnamaly, deleteAnamaly, getAnamaly } from '@/api/fulfiller/anamaly';
- import type { AnamalyQuery, AnamalyVO, AnamalyForm } from '@/api/fulfiller/anamaly/types';
- import { listByNameAndPhoneNumber } from '@/api/fulfiller/fulfiller';
- import type { FulfillerSearchQuery } from '@/api/fulfiller/fulfiller/types';
- import PageSelect from '@/components/PageSelect/index.vue';
- import ImageUpload from '@/components/ImageUpload/index.vue';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- const { flf_anamaly_type } = toRefs<any>(proxy?.useDict('flf_anamaly_type'));
- const route = useRoute();
- const loading = ref(false);
- const queryParams = reactive<AnamalyQuery>({
- pageNum: 1,
- pageSize: 10,
- content: '',
- type: '',
- status: undefined
- });
- const total = ref(0);
- const tableData = ref<AnamalyVO[]>([]);
- const getList = async () => {
- loading.value = true;
- try {
- const res = await getAnamalyList(queryParams);
- tableData.value = res.rows;
- total.value = res.total;
- } finally {
- loading.value = false;
- }
- };
- const handleQuery = () => {
- queryParams.pageNum = 1;
- getList();
- };
- const resetQuery = () => {
- queryParams.content = '';
- queryParams.type = '';
- queryParams.status = undefined;
- handleQuery();
- };
- // Dialog State (For Add/Edit)
- const dialogVisible = ref(false);
- const isEdit = ref(false);
- const fulfillerOptions = ref<any[]>([]);
- const fulfillerTotal = ref(0);
- const fulfillerQueryParams = reactive<FulfillerSearchQuery>({
- pageNum: 1,
- pageSize: 10,
- content: ''
- });
- const getFulfillerList = async () => {
- const res = await listByNameAndPhoneNumber(fulfillerQueryParams);
- fulfillerTotal.value = res.total;
- fulfillerOptions.value = res.rows.map((item: any) => ({
- label: `${item.name} - ${item.phoneNumber}`,
- value: item.id,
- name: item.name,
- phone: item.phoneNumber
- }));
- };
- const handleFulfillerSearch = (query: string) => {
- fulfillerQueryParams.content = query;
- fulfillerQueryParams.pageNum = 1;
- getFulfillerList();
- };
- const handleFulfillerPageChange = (page: number) => {
- fulfillerQueryParams.pageNum = page;
- getFulfillerList();
- };
- const handleFulfillerSelect = (id: any) => {
- // 备用选择回调
- };
- // Drawer State (For Audit/View)
- const drawerVisible = ref(false);
- const drawerMode = ref('view'); // 'audit' or 'view'
- const currentItem = ref<Partial<AnamalyVO>>({});
- const auditForm = reactive({ result: 1, remark: '' });
- const form = reactive<AnamalyForm>({
- id: undefined,
- orderCode: undefined,
- fulfiller: undefined,
- type: '',
- content: '',
- photos: '',
- status: 0
- });
- // Actions
- const handleAdd = () => {
- isEdit.value = false;
- Object.assign(form, {
- id: undefined,
- orderCode: undefined,
- fulfiller: undefined,
- type: '',
- content: '',
- photos: '',
- status: 0
- });
- getFulfillerList();
- dialogVisible.value = true;
- };
- const handleEdit = (row: AnamalyVO) => {
- isEdit.value = true;
- Object.assign(form, {
- id: row.id,
- orderCode: row.orderCode,
- fulfiller: row.fulfiller,
- type: row.type,
- content: row.content,
- photos: row.photos,
- status: row.status
- });
- getFulfillerList();
- dialogVisible.value = true;
- };
- const handleDelete = (row: AnamalyVO) => {
- ElMessageBox.confirm('确定删除该条记录吗?', '警告', { type: 'warning' }).then(async () => {
- try {
- await deleteAnamaly(row.id);
- ElMessage.success('删除成功');
- getList();
- } catch (error) {
- console.error(error);
- }
- });
- };
- // Open Drawer for Audit or View
- const handleOpenDrawer = (row: AnamalyVO, mode: string) => {
- 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 = async () => {
- if (!currentItem.value.id) return;
- try {
- await auditAnamaly({
- id: currentItem.value.id,
- result: auditForm.result,
- remark: auditForm.remark
- });
- ElMessage.success('审核成功');
- drawerVisible.value = false;
- getList();
- } catch (error) {
- console.error(error);
- }
- };
- const saveData = async () => {
- if (!form.orderCode || !form.type) return ElMessage.warning('请填写必填项');
- try {
- if (!isEdit.value) {
- await addAnamaly(form);
- ElMessage.success('新增上报成功');
- } else {
- ElMessage.success('请完善编辑接口');
- }
- dialogVisible.value = false;
- getList();
- } catch (error) {
- console.error(error);
- }
- };
- onMounted(() => {
- getList();
- if (route.query.id) {
- getAnamaly(route.query.id as any).then(res => {
- if (res.data) {
- handleOpenDrawer(res.data, res.data.status === 0 ? 'audit' : 'view');
- }
- });
- }
- });
- // 监听路由参数变化,实现从消息直达详情
- watch(() => route.query.id, (newId) => {
- if (newId) {
- getAnamaly(newId as any).then(res => {
- if (res.data) {
- handleOpenDrawer(res.data, res.data.status === 0 ? 'audit' : 'view');
- }
- });
- }
- });
- </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>
|