index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <template>
  2. <div class="page-container">
  3. <el-card shadow="never">
  4. <template #header>
  5. <div class="card-header">
  6. <div class="left-panel">
  7. <span class="title">异常上报管理</span>
  8. </div>
  9. <div class="right-panel" style="display: flex; align-items: center; gap: 10px">
  10. <el-input v-model="queryParams.content" placeholder="订单号/履约者姓名" prefix-icon="Search" clearable @keyup.enter="handleQuery" style="width: 220px" />
  11. <el-select v-model="queryParams.type" placeholder="异常类型" clearable @change="handleQuery" style="width: 140px">
  12. <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  13. </el-select>
  14. <el-select v-model="queryParams.status" placeholder="审核状态" clearable @change="handleQuery" style="width: 120px">
  15. <el-option label="待审核" :value="0" />
  16. <el-option label="已通过" :value="1" />
  17. <el-option label="已驳回" :value="2" />
  18. </el-select>
  19. <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
  20. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  21. <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['fulfiller:anamaly:add']">新增上报</el-button>
  22. </div>
  23. </div>
  24. </template>
  25. <el-table :data="tableData" style="width: 100%" v-loading="loading">
  26. <el-table-column prop="orderCode" label="关联订单号" width="180">
  27. <template #default="scope">
  28. <el-link type="primary" :underline="false">{{ scope.row.orderCode }}</el-link>
  29. </template>
  30. </el-table-column>
  31. <el-table-column prop="storeName" label="所属门店" width="150" show-overflow-tooltip>
  32. <template #default="scope">
  33. <span>{{ scope.row.storeName || '-' }}</span>
  34. </template>
  35. </el-table-column>
  36. <el-table-column prop="type" label="异常类型" width="120">
  37. <template #default="scope">
  38. <dict-tag :options="flf_anamaly_type" :value="scope.row.type" />
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="fulfiller" label="履约者" width="150">
  42. <template #default="scope">
  43. <div>{{ scope.row.fulfillerName || scope.row.fulfiller }}</div>
  44. <div style="font-size: 12px; color: #999" v-if="scope.row.fulfillerPhone">{{ scope.row.fulfillerPhone }}</div>
  45. </template>
  46. </el-table-column>
  47. <el-table-column prop="content" label="上报内容" show-overflow-tooltip />
  48. <el-table-column prop="photosUrls" label="上报图片" width="120">
  49. <template #default="scope">
  50. <div v-if="scope.row.photosUrls && scope.row.photosUrls.length">
  51. <el-image
  52. :src="scope.row.photosUrls[0]"
  53. :preview-src-list="scope.row.photosUrls"
  54. fit="cover"
  55. style="width: 40px; height: 40px; border-radius: 4px"
  56. preview-teleported
  57. >
  58. <template #error>
  59. <div class="image-slot">
  60. <el-icon><Picture /></el-icon>
  61. </div>
  62. </template>
  63. </el-image>
  64. <span v-if="scope.row.photosUrls.length > 1" style="font-size: 12px; color: #999; margin-left: 5px">+{{ scope.row.photosUrls.length }}</span>
  65. </div>
  66. <span v-else style="color: #ccc">无图</span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column prop="createTime" label="提交时间" width="170" sortable />
  70. <el-table-column prop="status" label="审核状态" width="100">
  71. <template #default="scope">
  72. <el-tag v-if="scope.row.status === 0" type="warning">待审核</el-tag>
  73. <el-tag v-else-if="scope.row.status === 1" type="success">已通过</el-tag>
  74. <el-tag v-else type="danger">已驳回</el-tag>
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="操作" width="200" fixed="right" align="center">
  78. <template #default="scope">
  79. <template v-if="scope.row.status === 0">
  80. <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'audit')" v-hasPermi="['fulfiller:anamaly:audit']">审核</el-button>
  81. <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:anamaly:edit']">编辑</el-button>
  82. <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
  83. </template>
  84. <template v-else>
  85. <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'view')" v-hasPermi="['fulfiller:anamaly:query']">详情</el-button>
  86. <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
  87. </template>
  88. </template>
  89. </el-table-column>
  90. </el-table>
  91. <div class="pagination-container">
  92. <el-pagination
  93. v-model:current-page="queryParams.pageNum"
  94. v-model:page-size="queryParams.pageSize"
  95. :page-sizes="[10, 20, 50, 100]"
  96. layout="total, sizes, prev, pager, next, jumper"
  97. :total="total"
  98. @size-change="getList"
  99. @current-change="getList"
  100. />
  101. </div>
  102. </el-card>
  103. <!-- Audit/Detail Drawer -->
  104. <el-drawer v-model="drawerVisible" :title="drawerMode === 'audit' ? '异常上报审核' : '异常详情'" direction="rtl" size="600px">
  105. <div class="drawer-content">
  106. <!-- 1. Basic Info -->
  107. <el-descriptions title="基础信息" :column="2" border>
  108. <el-descriptions-item label="订单号">{{ currentItem.orderCode }}</el-descriptions-item>
  109. <el-descriptions-item label="所属门店">{{ currentItem.storeName || '-' }}</el-descriptions-item>
  110. <el-descriptions-item label="提交时间">{{ currentItem.createTime }}</el-descriptions-item>
  111. <el-descriptions-item label="履约者">{{ currentItem.fulfillerName || currentItem.fulfiller }}</el-descriptions-item>
  112. <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone || '-' }}</el-descriptions-item>
  113. <el-descriptions-item label="异常类型">
  114. <dict-tag :options="flf_anamaly_type" :value="currentItem.type" />
  115. </el-descriptions-item>
  116. <el-descriptions-item label="当前状态">
  117. <el-tag v-if="currentItem.status === 0" type="warning">待审核</el-tag>
  118. <el-tag v-else-if="currentItem.status === 1" type="success">已通过</el-tag>
  119. <el-tag v-else type="danger">已驳回</el-tag>
  120. </el-descriptions-item>
  121. </el-descriptions>
  122. <!-- 2. Content & Images -->
  123. <div class="section-block">
  124. <div class="section-title">上报内容</div>
  125. <div class="text-content">{{ currentItem.content }}</div>
  126. <div class="image-list" v-if="currentItem.photosUrls && currentItem.photosUrls.length">
  127. <el-image
  128. v-for="(img, idx) in currentItem.photosUrls"
  129. :key="idx"
  130. :src="img"
  131. :preview-src-list="currentItem.photosUrls"
  132. :initial-index="idx"
  133. fit="cover"
  134. class="detail-img"
  135. />
  136. </div>
  137. </div>
  138. <!-- 3. Audit Form (If Audit Mode) -->
  139. <div class="section-block audit-form" v-if="drawerMode === 'audit'">
  140. <div class="section-title">审核处理</div>
  141. <el-form :model="auditForm" label-position="top">
  142. <el-form-item label="审核结果" required>
  143. <el-radio-group v-model="auditForm.result">
  144. <el-radio :label="1" border>通过</el-radio>
  145. <el-radio :label="2" border>驳回</el-radio>
  146. </el-radio-group>
  147. </el-form-item>
  148. <el-form-item label="处理备注">
  149. <el-input v-model="auditForm.remark" type="textarea" :rows="3" placeholder="请输入审核意见 (通过或驳回均可选填)" />
  150. </el-form-item>
  151. </el-form>
  152. <div class="drawer-footer-actions">
  153. <el-button @click="drawerVisible = false">取消</el-button>
  154. <el-button type="primary" @click="submitAudit" v-hasPermi="['fulfiller:anamaly:audit']">提交审核</el-button>
  155. </div>
  156. </div>
  157. <!-- 4. Log Record (All Modes) -->
  158. <div class="section-block">
  159. <div class="section-title">处理记录</div>
  160. <el-timeline>
  161. <el-timeline-item :timestamp="currentItem.createTime" placement="top" type="primary">
  162. <el-card shadow="never" class="log-card">
  163. <h4>提交上报</h4>
  164. <p>操作人:{{ currentItem.fulfillerName || '未知' }}</p>
  165. <p>内容:提交了异常上报</p>
  166. </el-card>
  167. </el-timeline-item>
  168. <el-timeline-item
  169. v-if="currentItem.status !== 0"
  170. :timestamp="currentItem.auditTime || '未知时间'"
  171. placement="top"
  172. :type="currentItem.status === 1 ? 'success' : 'danger'"
  173. >
  174. <el-card shadow="never" class="log-card">
  175. <h4>{{ currentItem.status === 1 ? '审核通过' : '审核驳回' }}</h4>
  176. <p>操作人:{{ currentItem.auditorName || '未知' }}</p>
  177. <p>备注:{{ currentItem.auditRemark || '无' }}</p>
  178. </el-card>
  179. </el-timeline-item>
  180. </el-timeline>
  181. </div>
  182. </div>
  183. </el-drawer>
  184. <!-- Edit/Add Dialog (Keep for Edit) -->
  185. <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑异常' : '新增异常'" width="600px">
  186. <el-form :model="form" label-width="100px">
  187. <el-form-item label="关联订单" required>
  188. <el-input v-model="form.orderCode" placeholder="请输入订单号" :disabled="isEdit" />
  189. </el-form-item>
  190. <el-form-item label="履约者" required>
  191. <PageSelect
  192. v-model="form.fulfiller"
  193. :options="fulfillerOptions"
  194. :total="fulfillerTotal"
  195. :pageSize="fulfillerQueryParams.pageSize"
  196. placeholder="请输入姓名/电话检索"
  197. :filter-method="handleFulfillerSearch"
  198. @page-change="handleFulfillerPageChange"
  199. @change="handleFulfillerSelect"
  200. style="width: 100%"
  201. :disabled="isEdit"
  202. />
  203. </el-form-item>
  204. <el-form-item label="异常类型" required>
  205. <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
  206. <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  207. </el-select>
  208. </el-form-item>
  209. <el-form-item label="上报内容" required>
  210. <el-input v-model="form.content" type="textarea" :rows="3" placeholder="请详细描述异常情况..." />
  211. </el-form-item>
  212. <el-form-item label="现场图片">
  213. <ImageUpload v-model="form.photos" />
  214. </el-form-item>
  215. </el-form>
  216. <template #footer>
  217. <span class="dialog-footer">
  218. <el-button @click="dialogVisible = false">取消</el-button>
  219. <el-button type="primary" @click="saveData">保存</el-button>
  220. </span>
  221. </template>
  222. </el-dialog>
  223. </div>
  224. </template>
  225. <script setup lang="ts">
  226. import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance, toRefs, watch } from 'vue';
  227. import { useRoute } from 'vue-router';
  228. import { ElMessage, ElMessageBox } from 'element-plus';
  229. import { getAnamalyList, addAnamaly, auditAnamaly, deleteAnamaly, getAnamaly } from '@/api/fulfiller/anamaly';
  230. import type { AnamalyQuery, AnamalyVO, AnamalyForm } from '@/api/fulfiller/anamaly/types';
  231. import { listByNameAndPhoneNumber } from '@/api/fulfiller/fulfiller';
  232. import type { FulfillerSearchQuery } from '@/api/fulfiller/fulfiller/types';
  233. import PageSelect from '@/components/PageSelect/index.vue';
  234. import ImageUpload from '@/components/ImageUpload/index.vue';
  235. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  236. const { flf_anamaly_type } = toRefs<any>(proxy?.useDict('flf_anamaly_type'));
  237. const route = useRoute();
  238. const loading = ref(false);
  239. const queryParams = reactive<AnamalyQuery>({
  240. pageNum: 1,
  241. pageSize: 10,
  242. content: '',
  243. type: '',
  244. status: undefined
  245. });
  246. const total = ref(0);
  247. const tableData = ref<AnamalyVO[]>([]);
  248. const getList = async () => {
  249. loading.value = true;
  250. try {
  251. const res = await getAnamalyList(queryParams);
  252. tableData.value = res.rows;
  253. total.value = res.total;
  254. } finally {
  255. loading.value = false;
  256. }
  257. };
  258. const handleQuery = () => {
  259. queryParams.pageNum = 1;
  260. getList();
  261. };
  262. const resetQuery = () => {
  263. queryParams.content = '';
  264. queryParams.type = '';
  265. queryParams.status = undefined;
  266. handleQuery();
  267. };
  268. // Dialog State (For Add/Edit)
  269. const dialogVisible = ref(false);
  270. const isEdit = ref(false);
  271. const fulfillerOptions = ref<any[]>([]);
  272. const fulfillerTotal = ref(0);
  273. const fulfillerQueryParams = reactive<FulfillerSearchQuery>({
  274. pageNum: 1,
  275. pageSize: 10,
  276. content: ''
  277. });
  278. const getFulfillerList = async () => {
  279. const res = await listByNameAndPhoneNumber(fulfillerQueryParams);
  280. fulfillerTotal.value = res.total;
  281. fulfillerOptions.value = res.rows.map((item: any) => ({
  282. label: `${item.name} - ${item.phoneNumber}`,
  283. value: item.id,
  284. name: item.name,
  285. phone: item.phoneNumber
  286. }));
  287. };
  288. const handleFulfillerSearch = (query: string) => {
  289. fulfillerQueryParams.content = query;
  290. fulfillerQueryParams.pageNum = 1;
  291. getFulfillerList();
  292. };
  293. const handleFulfillerPageChange = (page: number) => {
  294. fulfillerQueryParams.pageNum = page;
  295. getFulfillerList();
  296. };
  297. const handleFulfillerSelect = (id: any) => {
  298. // 备用选择回调
  299. };
  300. // Drawer State (For Audit/View)
  301. const drawerVisible = ref(false);
  302. const drawerMode = ref('view'); // 'audit' or 'view'
  303. const currentItem = ref<Partial<AnamalyVO>>({});
  304. const auditForm = reactive({ result: 1, remark: '' });
  305. const form = reactive<AnamalyForm>({
  306. id: undefined,
  307. orderCode: undefined,
  308. fulfiller: undefined,
  309. type: '',
  310. content: '',
  311. photos: '',
  312. status: 0
  313. });
  314. // Actions
  315. const handleAdd = () => {
  316. isEdit.value = false;
  317. Object.assign(form, {
  318. id: undefined,
  319. orderCode: undefined,
  320. fulfiller: undefined,
  321. type: '',
  322. content: '',
  323. photos: '',
  324. status: 0
  325. });
  326. getFulfillerList();
  327. dialogVisible.value = true;
  328. };
  329. const handleEdit = (row: AnamalyVO) => {
  330. isEdit.value = true;
  331. Object.assign(form, {
  332. id: row.id,
  333. orderCode: row.orderCode,
  334. fulfiller: row.fulfiller,
  335. type: row.type,
  336. content: row.content,
  337. photos: row.photos,
  338. status: row.status
  339. });
  340. getFulfillerList();
  341. dialogVisible.value = true;
  342. };
  343. const handleDelete = (row: AnamalyVO) => {
  344. ElMessageBox.confirm('确定删除该条记录吗?', '警告', { type: 'warning' }).then(async () => {
  345. try {
  346. await deleteAnamaly(row.id);
  347. ElMessage.success('删除成功');
  348. getList();
  349. } catch (error) {
  350. console.error(error);
  351. }
  352. });
  353. };
  354. // Open Drawer for Audit or View
  355. const handleOpenDrawer = (row: AnamalyVO, mode: string) => {
  356. drawerMode.value = mode;
  357. currentItem.value = JSON.parse(JSON.stringify(row));
  358. // Reset audit form
  359. if (mode === 'audit') {
  360. auditForm.result = 1;
  361. auditForm.remark = '';
  362. }
  363. drawerVisible.value = true;
  364. };
  365. const submitAudit = async () => {
  366. if (!currentItem.value.id) return;
  367. try {
  368. await auditAnamaly({
  369. id: currentItem.value.id,
  370. result: auditForm.result,
  371. remark: auditForm.remark
  372. });
  373. ElMessage.success('审核成功');
  374. drawerVisible.value = false;
  375. getList();
  376. } catch (error) {
  377. console.error(error);
  378. }
  379. };
  380. const saveData = async () => {
  381. if (!form.orderCode || !form.type) return ElMessage.warning('请填写必填项');
  382. try {
  383. if (!isEdit.value) {
  384. await addAnamaly(form);
  385. ElMessage.success('新增上报成功');
  386. } else {
  387. ElMessage.success('请完善编辑接口');
  388. }
  389. dialogVisible.value = false;
  390. getList();
  391. } catch (error) {
  392. console.error(error);
  393. }
  394. };
  395. onMounted(() => {
  396. getList();
  397. if (route.query.id) {
  398. getAnamaly(route.query.id as any).then(res => {
  399. if (res.data) {
  400. handleOpenDrawer(res.data, res.data.status === 0 ? 'audit' : 'view');
  401. }
  402. });
  403. }
  404. });
  405. // 监听路由参数变化,实现从消息直达详情
  406. watch(() => route.query.id, (newId) => {
  407. if (newId) {
  408. getAnamaly(newId as any).then(res => {
  409. if (res.data) {
  410. handleOpenDrawer(res.data, res.data.status === 0 ? 'audit' : 'view');
  411. }
  412. });
  413. }
  414. });
  415. </script>
  416. <style scoped>
  417. .page-container {
  418. padding: 20px;
  419. }
  420. .card-header {
  421. display: flex;
  422. justify-content: space-between;
  423. align-items: center;
  424. }
  425. .title {
  426. font-weight: bold;
  427. }
  428. .pagination-container {
  429. margin-top: 20px;
  430. display: flex;
  431. justify-content: flex-end;
  432. }
  433. /* Drawer Styles */
  434. .drawer-content {
  435. padding: 0 10px;
  436. }
  437. .section-block {
  438. margin-top: 25px;
  439. }
  440. .section-title {
  441. font-weight: bold;
  442. border-left: 4px solid #409eff;
  443. padding-left: 10px;
  444. margin-bottom: 15px;
  445. font-size: 15px;
  446. color: #303133;
  447. }
  448. .text-content {
  449. background: #f8f9fa;
  450. padding: 12px;
  451. border-radius: 4px;
  452. color: #606266;
  453. line-height: 1.6;
  454. }
  455. .image-list {
  456. margin-top: 15px;
  457. display: flex;
  458. gap: 10px;
  459. flex-wrap: wrap;
  460. }
  461. .detail-img {
  462. width: 100px;
  463. height: 100px;
  464. border-radius: 6px;
  465. cursor: pointer;
  466. }
  467. .audit-form {
  468. background: #f0f9eb;
  469. padding: 15px;
  470. border-radius: 8px;
  471. border: 1px solid #e1f3d8;
  472. }
  473. .drawer-footer-actions {
  474. display: flex;
  475. justify-content: flex-end;
  476. margin-top: 15px;
  477. grid-gap: 10px;
  478. }
  479. .log-card {
  480. margin-bottom: 5px;
  481. }
  482. .log-card h4 {
  483. margin: 0 0 5px;
  484. font-size: 14px;
  485. color: #303133;
  486. }
  487. .log-card p {
  488. margin: 0;
  489. font-size: 12px;
  490. color: #909399;
  491. line-height: 1.4;
  492. }
  493. </style>