businessLicense.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. <template>
  2. <div class="card content-box">
  3. <div class="content-section">
  4. <!-- <div class="tip-text">营业执照到期时间:{{ expirationTime || "&#45;&#45;" }}</div>-->
  5. <!-- <div class="action-buttons">-->
  6. <!-- <el-button type="primary" @click="handleReplace"> 更换 </el-button>-->
  7. <!-- <el-button type="primary" @click="handleViewChangeRecord"> 查看变更记录 </el-button>-->
  8. <!-- </div>-->
  9. </div>
  10. <div class="license-container" v-if="licenseImage">
  11. <div class="license-display">
  12. <el-image
  13. :src="licenseImage"
  14. fit="contain"
  15. class="license-image"
  16. :preview-src-list="licensePreviewList.length ? licensePreviewList : [licenseImage]"
  17. >
  18. <template #error>
  19. <div class="empty-image-box">
  20. <el-icon class="empty-icon">
  21. <Picture />
  22. </el-icon>
  23. </div>
  24. </template>
  25. </el-image>
  26. </div>
  27. </div>
  28. <div v-else class="empty-license">
  29. <el-empty description="暂无营业执照" :image-size="100" />
  30. </div>
  31. <!-- 更换营业执照弹窗 -->
  32. <el-dialog
  33. v-model="replaceDialogVisible"
  34. title="更换营业执照"
  35. width="600px"
  36. :before-close="handleReplaceDialogClose"
  37. :close-on-click-modal="false"
  38. :close-on-press-escape="false"
  39. >
  40. <div class="replace-upload-area" :class="{ 'upload-full': uploadedImageCount >= 1 }">
  41. <div class="upload-tip">只允许上传jpg,jpeg,png格式图片;上传图片不得超过20M</div>
  42. <el-upload
  43. v-model:file-list="fileList"
  44. list-type="picture-card"
  45. :accept="'.jpg,.jpeg,.png'"
  46. :limit="1"
  47. :auto-upload="false"
  48. :disabled="hasUnuploadedImages"
  49. :on-change="handleUploadChange"
  50. :on-exceed="handleUploadExceed"
  51. :on-preview="handlePictureCardPreview"
  52. :before-remove="handleBeforeRemove"
  53. :on-remove="handleRemove"
  54. :show-file-list="true"
  55. >
  56. <template #trigger>
  57. <div v-if="uploadedImageCount < 1" class="upload-trigger-card el-upload--picture-card">
  58. <el-icon>
  59. <Plus />
  60. </el-icon>
  61. <div class="upload-tip">({{ uploadedImageCount }}/1)</div>
  62. </div>
  63. </template>
  64. </el-upload>
  65. </div>
  66. <template #footer>
  67. <div class="dialog-footer">
  68. <el-button @click="handleCancelReplace" :disabled="hasUnuploadedImages"> 取消 </el-button>
  69. <el-button type="primary" @click="handleSubmitReplace" :disabled="hasUnuploadedImages"> 去审核 </el-button>
  70. </div>
  71. </template>
  72. </el-dialog>
  73. <!-- 图片预览 -->
  74. <PcImagePreviewViewer
  75. v-model:visible="imageViewerVisible"
  76. :url-list="imageViewerUrlList"
  77. :initial-index="imageViewerInitialIndex"
  78. />
  79. <!-- 变更记录弹窗 -->
  80. <el-dialog v-model="changeRecordDialogVisible" title="变更记录" width="900px" :close-on-click-modal="false">
  81. <el-scrollbar height="400px" class="change-record-scrollbar">
  82. <div v-if="changeRecordList && changeRecordList.length > 0" class="change-record-content">
  83. <div v-for="(item, index) in changeRecordList" :key="index" class="record-group">
  84. <div class="record-date">
  85. {{ item.createdDateFormat }}
  86. </div>
  87. <div class="record-items">
  88. <div
  89. v-for="(url, uIdx) in parseCommaSeparatedImgUrls(item.imgUrl)"
  90. :key="`${item.id ?? index}-${uIdx}`"
  91. class="record-item"
  92. >
  93. <div class="record-status-badge" :class="getStatusClass(item.licenseExecuteStatus)">
  94. {{ getLicenseExecuteRecordText(item) }}
  95. </div>
  96. <el-image
  97. :src="url"
  98. fit="cover"
  99. class="record-image"
  100. :preview-src-list="parseCommaSeparatedImgUrls(item.imgUrl)"
  101. :initial-index="uIdx"
  102. >
  103. <template #error>
  104. <div class="image-slot">
  105. <el-icon><Picture /></el-icon>
  106. </div>
  107. </template>
  108. </el-image>
  109. </div>
  110. </div>
  111. <div v-if="item.reasonRefusal" class="rejection-reason">拒绝原因: {{ item.reasonRefusal }}</div>
  112. </div>
  113. </div>
  114. <div v-else class="empty-record">
  115. <el-empty description="暂无变更记录" :image-size="100" />
  116. </div>
  117. </el-scrollbar>
  118. <template #footer>
  119. <div class="dialog-footer">
  120. <el-button type="primary" @click="changeRecordDialogVisible = false"> 关闭 </el-button>
  121. </div>
  122. </template>
  123. </el-dialog>
  124. </div>
  125. </template>
  126. <script setup lang="ts" name="businessLicense">
  127. import { ref, computed, onMounted } from "vue";
  128. import { ElMessage, ElMessageBox } from "element-plus";
  129. import { Picture, Plus } from "@element-plus/icons-vue";
  130. import type { UploadProps, UploadFile } from "element-plus";
  131. import {
  132. getBusinessLicense,
  133. getStoreBusinessLicenseStatus,
  134. getBusinessLicenseChangeRecords,
  135. uploadContractImage,
  136. submitBusinessLicenseReview,
  137. ocrRequestUrl
  138. } from "@/api/modules/licenseManagement";
  139. import { localGet } from "@/utils";
  140. import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewViewer.vue";
  141. const userInfo = localGet("geeker-user")?.userInfo || {};
  142. // 状态映射对象
  143. const statusMap: Record<number, { name: string; class: string }> = {
  144. 1: { name: "审核通过", class: "status-success" },
  145. 2: { name: "审核中", class: "status-pending" },
  146. 3: { name: "审核失败", class: "status-failed" }
  147. };
  148. const id = localGet("createdId");
  149. const licenseImage = ref<string>("");
  150. /** 接口 imgUrl 可能为多图逗号拼接 */
  151. const licensePreviewList = ref<string[]>([]);
  152. const expirationTime = ref<string>("");
  153. const replaceDialogVisible = ref(false);
  154. const changeRecordDialogVisible = ref(false);
  155. const fileList = ref<UploadFile[]>([]);
  156. const changeRecordList = ref<any>([]);
  157. // ==================== 图片上传相关变量 ====================
  158. const uploading = ref(false);
  159. const pendingUploadFiles = ref<UploadFile[]>([]);
  160. const imageUrlList = ref<string[]>([]); // 存储图片URL列表
  161. // 图片预览相关
  162. const imageViewerVisible = ref(false);
  163. const imageViewerUrlList = ref<string[]>([]);
  164. const imageViewerInitialIndex = ref(0);
  165. // 计算属性:获取已成功上传的图片数量
  166. const uploadedImageCount = computed(() => {
  167. return fileList.value.filter((file: any) => file.status === "success" && file.url).length;
  168. });
  169. // 计算属性:检查是否有未上传完成的图片
  170. const hasUnuploadedImages = computed(() => {
  171. // 检查是否有正在上传的文件
  172. if (uploading.value || pendingUploadFiles.value.length > 0) {
  173. return true;
  174. }
  175. // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
  176. if (fileList.value && fileList.value.length > 0) {
  177. return fileList.value.some((file: any) => {
  178. return file.status === "ready" || file.status === "uploading";
  179. });
  180. }
  181. return false;
  182. });
  183. onMounted(async () => {
  184. await initData();
  185. });
  186. /** 资质/变更记录:imgUrl 多为英文逗号分隔的多张图地址 */
  187. function parseCommaSeparatedImgUrls(raw: unknown): string[] {
  188. if (raw == null || raw === "") return [];
  189. if (Array.isArray(raw)) {
  190. return raw.map(u => String(u).trim()).filter(Boolean);
  191. }
  192. const s = String(raw).trim();
  193. if (!s) return [];
  194. return s
  195. .split(",")
  196. .map(part => part.trim())
  197. .filter(Boolean);
  198. }
  199. const initData = async () => {
  200. const params = {
  201. id: id
  202. };
  203. const res: any = await getBusinessLicense(params);
  204. if (res.code == 200) {
  205. const urls = parseCommaSeparatedImgUrls(res.data[0]?.imgUrl);
  206. licensePreviewList.value = urls;
  207. licenseImage.value = urls[0] || "";
  208. expirationTime.value = res.data[0]?.expirationTime;
  209. }
  210. };
  211. const handleReplace = async () => {
  212. fileList.value = [];
  213. imageUrlList.value = [];
  214. pendingUploadFiles.value = [];
  215. uploading.value = false;
  216. const params = {
  217. id: localGet("createdId")
  218. };
  219. const res: any = await getStoreBusinessLicenseStatus(params);
  220. if (res.data.businessLicenseStatus == 2) {
  221. ElMessage.warning("营业执照审核中,请耐心等待");
  222. } else {
  223. replaceDialogVisible.value = true;
  224. }
  225. };
  226. const handleViewChangeRecord = async () => {
  227. try {
  228. const params = {
  229. storeId: localGet("createdId")
  230. };
  231. const res: any = await getBusinessLicenseChangeRecords(params);
  232. if (res.code === 200) {
  233. changeRecordList.value = res.data;
  234. } else {
  235. // 请求失败时清空数据
  236. changeRecordList.value = [];
  237. }
  238. changeRecordDialogVisible.value = true;
  239. } catch (error) {
  240. ElMessage.error("获取变更记录失败");
  241. // 发生错误时清空数据并显示空状态
  242. changeRecordList.value = [];
  243. changeRecordDialogVisible.value = true;
  244. }
  245. };
  246. /**
  247. * 检查文件是否在排队中(未上传)
  248. * @param file 文件对象
  249. * @returns 是否在排队中
  250. */
  251. const isFilePending = (file: any): boolean => {
  252. // 只检查 ready 状态(排队中),不包括 uploading(正在上传)
  253. if (file.status === "ready") {
  254. return true;
  255. }
  256. // 检查是否在待上传队列中
  257. if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  258. return true;
  259. }
  260. return false;
  261. };
  262. /**
  263. * 图片上传 - 删除前确认
  264. * @param uploadFile 要删除的文件对象
  265. * @param uploadFiles 当前文件列表
  266. * @returns Promise<boolean>,true 允许删除,false 阻止删除
  267. */
  268. const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
  269. // 如果文件在排队中(未上传),禁止删除
  270. if (isFilePending(uploadFile)) {
  271. ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
  272. return false;
  273. }
  274. try {
  275. await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
  276. confirmButtonText: "确定",
  277. cancelButtonText: "取消",
  278. type: "warning"
  279. });
  280. // 用户确认删除,返回 true 允许删除
  281. return true;
  282. } catch {
  283. // 用户取消删除,返回 false 阻止删除
  284. return false;
  285. }
  286. };
  287. /**
  288. * 图片上传 - 移除图片回调(删除成功后调用)
  289. * @param uploadFile 已删除的文件对象
  290. * @param uploadFiles 删除后的文件列表
  291. */
  292. const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  293. // 从被删除的文件对象中获取 url
  294. const file = uploadFile as any;
  295. const imageUrl = file.url;
  296. if (imageUrl) {
  297. // 从 imageUrl 数组中删除对应的 URL
  298. const urlIndex = imageUrlList.value.indexOf(imageUrl);
  299. if (urlIndex > -1) {
  300. imageUrlList.value.splice(urlIndex, 1);
  301. }
  302. }
  303. if (file.url && file.url.startsWith("blob:")) {
  304. URL.revokeObjectURL(file.url);
  305. }
  306. // 同步文件列表
  307. fileList.value = [...uploadFiles];
  308. // 删除成功后提示
  309. ElMessage.success("图片已删除");
  310. };
  311. /**
  312. * 上传文件超出限制提示
  313. */
  314. const handleUploadExceed: UploadProps["onExceed"] = () => {
  315. ElMessage.warning("最多只能上传1张图片");
  316. };
  317. /**
  318. * el-upload 文件变更(选中或移除)
  319. */
  320. const handleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
  321. // 检查文件类型,只允许 jpg 和 png
  322. if (uploadFile.raw) {
  323. const fileType = uploadFile.raw.type.toLowerCase();
  324. const fileName = uploadFile.name.toLowerCase();
  325. const validTypes = ["image/jpeg", "image/jpg", "image/png"];
  326. const validExtensions = [".jpg", ".jpeg", ".png"];
  327. // 检查 MIME 类型或文件扩展名
  328. const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
  329. if (!isValidType) {
  330. // 从文件列表中移除不符合类型的文件
  331. const index = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  332. if (index > -1) {
  333. fileList.value.splice(index, 1);
  334. }
  335. // 从 uploadFiles 中移除
  336. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  337. if (uploadIndex > -1) {
  338. uploadFiles.splice(uploadIndex, 1);
  339. }
  340. // 如果文件有 blob URL,释放它
  341. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  342. URL.revokeObjectURL(uploadFile.url);
  343. }
  344. ElMessage.warning("只支持上传 JPG、JPEG 和 PNG 格式的图片");
  345. return;
  346. }
  347. // 检查文件大小,不得超过20M
  348. const maxSize = 20 * 1024 * 1024; // 20MB
  349. if (uploadFile.raw.size > maxSize) {
  350. // 从文件列表中移除超过大小的文件
  351. const index = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  352. if (index > -1) {
  353. fileList.value.splice(index, 1);
  354. }
  355. // 从 uploadFiles 中移除
  356. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  357. if (uploadIndex > -1) {
  358. uploadFiles.splice(uploadIndex, 1);
  359. }
  360. // 如果文件有 blob URL,释放它
  361. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  362. URL.revokeObjectURL(uploadFile.url);
  363. }
  364. ElMessage.warning("上传图片不得超过20M");
  365. return;
  366. }
  367. }
  368. // 同步文件列表到表单数据(只添加通过验证的文件)
  369. const existingIndex = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  370. if (existingIndex === -1) {
  371. fileList.value.push(uploadFile);
  372. }
  373. const readyFiles = fileList.value.filter(file => file.status === "ready");
  374. if (readyFiles.length) {
  375. readyFiles.forEach(file => {
  376. if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  377. pendingUploadFiles.value.push(file);
  378. }
  379. });
  380. }
  381. processUploadQueue();
  382. };
  383. /**
  384. * 处理上传队列 - 逐个上传文件
  385. */
  386. const processUploadQueue = async () => {
  387. if (uploading.value || pendingUploadFiles.value.length === 0) {
  388. return;
  389. }
  390. // 每次只取一个文件进行上传
  391. const file = pendingUploadFiles.value.shift();
  392. if (file) {
  393. await uploadSingleFile(file);
  394. // 继续处理队列中的下一个文件
  395. processUploadQueue();
  396. }
  397. };
  398. /**
  399. * 单文件上传图片
  400. * @param file 待上传的文件
  401. */
  402. const uploadSingleFile = async (file: UploadFile) => {
  403. if (!file.raw) {
  404. return;
  405. }
  406. const rawFile = file.raw as File;
  407. const formData = new FormData();
  408. formData.append("file", rawFile);
  409. formData.append("user", "text");
  410. file.status = "uploading";
  411. file.percentage = 0;
  412. uploading.value = true;
  413. try {
  414. // 上传过程中保持进度为 0,避免接口异常时进度条误显示 100%
  415. const result: any = await uploadContractImage(formData);
  416. if (result?.code === 200 && result.data) {
  417. // 处理单个文件的上传结果
  418. let imageUrl = result.data[0];
  419. if (!imageUrl) {
  420. throw new Error("上传成功但未获取到图片URL");
  421. }
  422. file.status = "success";
  423. file.percentage = 100;
  424. // 保存图片URL到文件对象
  425. file.url = imageUrl;
  426. file.response = { url: imageUrl };
  427. // 保存图片URL
  428. if (!Array.isArray(imageUrlList.value)) {
  429. imageUrlList.value = [];
  430. }
  431. if (!imageUrlList.value.includes(imageUrl)) {
  432. imageUrlList.value.push(imageUrl);
  433. }
  434. // 图片上传成功后调用 OCR 接口进行识别校验(营业执照)
  435. try {
  436. const params = {
  437. imageUrls: imageUrl,
  438. ocrType: "BUSINESS_LICENSE",
  439. storeId: userInfo.storeId,
  440. storeUserId: userInfo.id
  441. };
  442. const res: any = await ocrRequestUrl(params);
  443. if (res.code == 200) {
  444. ElMessage.success("识别成功");
  445. } else {
  446. // OCR 识别失败:提示并删除当前图片
  447. ElMessage.error(res?.msg || "识别失败,请重试");
  448. const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
  449. if (index > -1) {
  450. fileList.value.splice(index, 1);
  451. }
  452. const urlIndex = imageUrlList.value.indexOf(imageUrl);
  453. if (urlIndex > -1) {
  454. imageUrlList.value.splice(urlIndex, 1);
  455. }
  456. if (file.url && file.url.startsWith("blob:")) {
  457. URL.revokeObjectURL(file.url);
  458. }
  459. }
  460. } catch (error) {
  461. // 调用接口异常时:提示并删除当前图片
  462. const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
  463. if (index > -1) {
  464. fileList.value.splice(index, 1);
  465. }
  466. const urlIndex = imageUrlList.value.indexOf(imageUrl);
  467. if (urlIndex > -1) {
  468. imageUrlList.value.splice(urlIndex, 1);
  469. }
  470. if (file.url && file.url.startsWith("blob:")) {
  471. URL.revokeObjectURL(file.url);
  472. }
  473. }
  474. } else {
  475. throw new Error(result?.msg || "图片上传失败");
  476. }
  477. } catch (error: any) {
  478. file.status = "fail";
  479. // 上传失败时保持进度条为 0
  480. file.percentage = 0;
  481. if (file.url && file.url.startsWith("blob:")) {
  482. URL.revokeObjectURL(file.url);
  483. }
  484. // 从文件列表中移除失败的文件
  485. const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
  486. if (index > -1) {
  487. fileList.value.splice(index, 1);
  488. }
  489. // Error message handled by global upload method, except for specific business logic errors
  490. if (error?.message && error.message.includes("未获取到图片URL")) {
  491. ElMessage.error(error.message);
  492. }
  493. } finally {
  494. uploading.value = false;
  495. // 触发视图更新
  496. fileList.value = [...fileList.value];
  497. }
  498. };
  499. /**
  500. * 图片预览 - 使用统一 PcImagePreviewViewer(Element Plus 图集层)
  501. * @param file 上传文件对象
  502. */
  503. const handlePictureCardPreview = (file: any) => {
  504. // 如果文件在排队中(未上传),禁止预览
  505. if (isFilePending(file)) {
  506. ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
  507. return;
  508. }
  509. // 如果文件正在上传中,允许预览(使用本地预览)
  510. if (file.status === "uploading" && file.url) {
  511. imageViewerUrlList.value = [file.url];
  512. imageViewerInitialIndex.value = 0;
  513. imageViewerVisible.value = true;
  514. return;
  515. }
  516. // 获取所有图片的 URL 列表(只包含已上传成功的图片)
  517. const urlList = fileList.value
  518. .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
  519. .map((item: any) => item.url || item.response?.data);
  520. // 找到当前点击的图片索引
  521. const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
  522. if (currentIndex < 0) {
  523. ElMessage.warning("图片尚未上传完成,无法预览");
  524. return;
  525. }
  526. imageViewerUrlList.value = urlList;
  527. imageViewerInitialIndex.value = currentIndex;
  528. imageViewerVisible.value = true;
  529. };
  530. const handleCancelReplace = async () => {
  531. // 如果有图片正在上传,阻止关闭
  532. if (hasUnuploadedImages.value) {
  533. ElMessage.warning("请等待图片上传完成后再关闭");
  534. return;
  535. }
  536. if (fileList.value.length > 0) {
  537. try {
  538. await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
  539. confirmButtonText: "确定",
  540. cancelButtonText: "取消",
  541. type: "warning"
  542. });
  543. // 用户确认取消
  544. fileList.value = [];
  545. imageUrlList.value = [];
  546. pendingUploadFiles.value = [];
  547. uploading.value = false;
  548. replaceDialogVisible.value = false;
  549. } catch {
  550. // 用户取消操作,不做任何处理
  551. }
  552. } else {
  553. replaceDialogVisible.value = false;
  554. }
  555. };
  556. const handleReplaceDialogClose = async (done: () => void) => {
  557. // 如果有图片正在上传,阻止关闭
  558. if (hasUnuploadedImages.value) {
  559. ElMessage.warning("请等待图片上传完成后再关闭");
  560. return; // 不调用 done(),阻止关闭弹窗
  561. }
  562. if (fileList.value.length > 0) {
  563. try {
  564. await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
  565. confirmButtonText: "确定",
  566. cancelButtonText: "取消",
  567. type: "warning"
  568. });
  569. // 用户确认取消,清空数据并关闭弹窗
  570. fileList.value = [];
  571. imageUrlList.value = [];
  572. pendingUploadFiles.value = [];
  573. uploading.value = false;
  574. done(); // 调用 done() 允许关闭弹窗
  575. } catch {
  576. // 用户取消操作,不调用 done(),阻止关闭弹窗
  577. }
  578. } else {
  579. // 没有文件,直接关闭
  580. done();
  581. }
  582. };
  583. const handleSubmitReplace = async () => {
  584. // 检查是否有未上传完成的图片
  585. if (hasUnuploadedImages.value) {
  586. ElMessage.warning("请等待图片上传完成后再提交");
  587. return;
  588. }
  589. if (fileList.value.length === 0) {
  590. ElMessage.warning("请先上传图片");
  591. return;
  592. }
  593. const uploadedFiles = fileList.value.filter(file => file.status === "success");
  594. if (uploadedFiles.length === 0) {
  595. ElMessage.warning("请先上传图片");
  596. return;
  597. }
  598. try {
  599. // 只提交单张图片,排序为0
  600. const imageDataWithSort = uploadedFiles.map((file, index) => ({
  601. imgUrl: file.url,
  602. imgSort: index,
  603. storeId: localGet("createdId")
  604. }));
  605. await submitBusinessLicenseReview(imageDataWithSort[0]);
  606. ElMessage.success("提交审核成功");
  607. replaceDialogVisible.value = false;
  608. fileList.value = [];
  609. imageUrlList.value = [];
  610. pendingUploadFiles.value = [];
  611. uploading.value = false;
  612. await initData();
  613. } catch (error) {
  614. ElMessage.error("提交审核失败");
  615. }
  616. };
  617. const getStatusClass = (status: string | number) => {
  618. const statusInfo = statusMap[status as number];
  619. return statusInfo ? statusInfo.class : "";
  620. };
  621. /** 变更记录每条图都展示同一执行状态文案(与 licenseExecuteStatus 对齐) */
  622. const getLicenseExecuteRecordText = (item: { licenseExecuteStatus?: number; licenseExecuteName?: string }) => {
  623. const s = item.licenseExecuteStatus;
  624. if (s == null) return item.licenseExecuteName ?? "未知";
  625. return statusMap[s]?.name ?? item.licenseExecuteName ?? "未知";
  626. };
  627. const getStatusText = (status: string) => {
  628. const map: Record<string, string> = {
  629. pending: "审核中",
  630. success: "审核通过",
  631. failed: "审核失败"
  632. };
  633. return map[status] || "未知";
  634. };
  635. </script>
  636. <style lang="scss" scoped>
  637. .page-header {
  638. margin-bottom: 20px;
  639. }
  640. .store-title {
  641. margin: 0;
  642. font-size: 24px;
  643. font-weight: 600;
  644. color: var(--el-text-color-primary);
  645. }
  646. .content-section {
  647. display: flex;
  648. align-items: center;
  649. justify-content: space-between;
  650. width: 100%;
  651. margin-top: 20px;
  652. margin-bottom: 50px;
  653. }
  654. .tip-text {
  655. font-size: 18px;
  656. color: var(--el-text-color-regular);
  657. }
  658. .action-buttons {
  659. display: flex;
  660. flex-shrink: 0;
  661. gap: 10px;
  662. }
  663. .license-container {
  664. padding: 20px;
  665. background-color: var(--el-bg-color-page);
  666. border-radius: 8px;
  667. }
  668. .license-display {
  669. display: flex;
  670. align-items: center;
  671. justify-content: center;
  672. width: 700px;
  673. height: 500px;
  674. }
  675. .license-image {
  676. width: 100%;
  677. height: 100%;
  678. border-radius: 8px;
  679. box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
  680. }
  681. .empty-image-box {
  682. display: flex;
  683. align-items: center;
  684. justify-content: center;
  685. width: 100%;
  686. height: 100%;
  687. background-color: var(--el-fill-color-lighter);
  688. border-radius: 8px;
  689. .empty-icon {
  690. font-size: 64px;
  691. color: var(--el-text-color-placeholder);
  692. }
  693. }
  694. .empty-license {
  695. display: flex;
  696. align-items: center;
  697. justify-content: center;
  698. min-height: 570px;
  699. padding: 40px 20px;
  700. }
  701. .replace-upload-area {
  702. .upload-tip {
  703. margin-bottom: 10px;
  704. }
  705. min-height: 300px;
  706. // padding: 20px;
  707. :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
  708. display: inline-flex !important;
  709. opacity: 1 !important;
  710. }
  711. :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
  712. display: inline-flex !important;
  713. opacity: 1 !important;
  714. }
  715. :deep(.el-upload-list--picture-card .el-icon--close-tip) {
  716. display: none !important;
  717. }
  718. &.upload-full {
  719. :deep(.el-upload--picture-card) {
  720. display: none !important;
  721. }
  722. }
  723. }
  724. .dialog-footer {
  725. display: flex;
  726. gap: 10px;
  727. justify-content: center;
  728. }
  729. /* el-upload 图片预览铺满容器 */
  730. :deep(.el-upload-list--picture-card) {
  731. .el-upload-list__item {
  732. overflow: hidden;
  733. .el-upload-list__item-thumbnail {
  734. width: 100%;
  735. height: 100%;
  736. object-fit: fill;
  737. }
  738. }
  739. /* 排队中(未上传)的图片禁用样式 */
  740. .el-upload-list__item[data-status="ready"],
  741. .el-upload-list__item.is-ready {
  742. position: relative;
  743. pointer-events: none;
  744. cursor: not-allowed;
  745. opacity: 0.6;
  746. &::after {
  747. position: absolute;
  748. inset: 0;
  749. z-index: 1;
  750. content: "";
  751. background-color: rgb(0 0 0 / 30%);
  752. }
  753. .el-upload-list__item-actions {
  754. pointer-events: none;
  755. opacity: 0.5;
  756. }
  757. }
  758. }
  759. .upload-trigger-card {
  760. display: flex;
  761. flex-direction: column;
  762. align-items: center;
  763. justify-content: center;
  764. width: 100%;
  765. height: 100%;
  766. font-size: 28px;
  767. color: #8c939d;
  768. .upload-tip {
  769. margin-top: 8px;
  770. font-size: 14px;
  771. color: #8c939d;
  772. }
  773. }
  774. .change-record-scrollbar {
  775. :deep(.el-scrollbar__wrap) {
  776. overflow-x: hidden;
  777. }
  778. }
  779. .change-record-content {
  780. .record-group {
  781. padding: 20px;
  782. margin-bottom: 30px;
  783. background-color: var(--el-fill-color-lighter);
  784. &:last-child {
  785. margin-bottom: 0;
  786. }
  787. .record-date {
  788. margin-bottom: 15px;
  789. font-size: 16px;
  790. font-weight: 500;
  791. color: var(--el-text-color-primary);
  792. }
  793. .record-items {
  794. display: flex;
  795. flex-wrap: wrap;
  796. gap: 15px;
  797. }
  798. .record-item {
  799. position: relative;
  800. width: 150px;
  801. height: 150px;
  802. overflow: hidden;
  803. border-radius: 8px;
  804. .record-status-badge {
  805. position: absolute;
  806. right: 0;
  807. bottom: 0;
  808. left: 0;
  809. z-index: 1;
  810. padding: 4px 8px;
  811. font-size: 12px;
  812. font-weight: 500;
  813. text-align: center;
  814. border-radius: 0 0 8px 8px;
  815. &.status-pending {
  816. color: #e6a23c;
  817. background-color: rgb(253 246 236 / 90%);
  818. border-top: 1px solid #e6a23c;
  819. }
  820. &.status-success {
  821. color: #67c23a;
  822. background-color: rgb(240 249 255 / 90%);
  823. border-top: 1px solid #67c23a;
  824. }
  825. &.status-failed {
  826. color: #f56c6c;
  827. background-color: rgb(254 240 240 / 90%);
  828. border-top: 1px solid #f56c6c;
  829. }
  830. }
  831. .record-image {
  832. width: 100%;
  833. height: 100%;
  834. .image-slot {
  835. display: flex;
  836. align-items: center;
  837. justify-content: center;
  838. width: 100%;
  839. height: 100%;
  840. font-size: 30px;
  841. color: var(--el-text-color-placeholder);
  842. background: var(--el-fill-color-light);
  843. }
  844. }
  845. }
  846. .rejection-reason {
  847. margin-top: 15px;
  848. font-size: 14px;
  849. font-weight: 500;
  850. color: var(--el-text-color-regular);
  851. border-radius: 8px;
  852. }
  853. }
  854. }
  855. .empty-record {
  856. display: flex;
  857. align-items: center;
  858. justify-content: center;
  859. min-height: 300px;
  860. padding: 40px 20px;
  861. }
  862. </style>