index.vue 17 KB

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