window.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <template>
  2. <!-- 更换合同弹窗 -->
  3. <el-dialog
  4. v-model="replaceDialogVisible"
  5. title="更换合同"
  6. width="860px"
  7. :before-close="handleReplaceDialogClose"
  8. :close-on-click-modal="false"
  9. :close-on-press-escape="false"
  10. >
  11. <el-scrollbar height="400px" class="replace-upload-scrollbar">
  12. <div class="replace-upload-area" :class="{ 'upload-full': uploadedImageCount >= uploadMaxCount }">
  13. <el-upload
  14. v-model:file-list="fileList"
  15. list-type="picture-card"
  16. :accept="'.jpg,.png'"
  17. :limit="uploadMaxCount"
  18. :auto-upload="false"
  19. :disabled="hasUnuploadedImages"
  20. multiple
  21. :on-change="handleUploadChange"
  22. :on-exceed="handleUploadExceed"
  23. :on-preview="handlePictureCardPreview"
  24. :before-remove="handleBeforeRemove"
  25. :on-remove="handleRemove"
  26. :show-file-list="true"
  27. >
  28. <template #trigger>
  29. <div v-if="uploadedImageCount < uploadMaxCount" class="upload-trigger-card el-upload--picture-card">
  30. <el-icon>
  31. <Plus />
  32. </el-icon>
  33. <div class="upload-tip">({{ uploadedImageCount }}/{{ uploadMaxCount }})</div>
  34. </div>
  35. </template>
  36. </el-upload>
  37. </div>
  38. </el-scrollbar>
  39. <template #footer>
  40. <div class="dialog-footer">
  41. <el-button @click="handleCancelReplace" :disabled="hasUnuploadedImages"> 取消 </el-button>
  42. <el-button type="primary" @click="handleSubmitReplace" :disabled="hasUnuploadedImages"> 去审核 </el-button>
  43. </div>
  44. </template>
  45. </el-dialog>
  46. <!-- 图片预览 -->
  47. <el-image-viewer
  48. v-if="imageViewerVisible"
  49. :url-list="imageViewerUrlList"
  50. :initial-index="imageViewerInitialIndex"
  51. @close="imageViewerVisible = false"
  52. />
  53. </template>
  54. <script setup lang="ts" name="contractManagementwindow">
  55. import { ref, computed } from "vue";
  56. import { ElMessage, ElMessageBox } from "element-plus";
  57. import { Plus } from "@element-plus/icons-vue";
  58. import type { UploadProps, UploadFile } from "element-plus";
  59. import { localGet } from "@/utils";
  60. import { uploadContractImage, submitContractReview, getStoreContractStatus } from "@/api/modules/licenseManagement";
  61. const replaceDialogVisible = ref(false);
  62. const fileList = ref<UploadFile[]>([]);
  63. // ==================== 图片上传相关变量 ====================
  64. const uploadMaxCount = 20;
  65. const uploading = ref(false);
  66. const pendingUploadFiles = ref<UploadFile[]>([]);
  67. const imageUrlList = ref<string[]>([]); // 存储图片URL列表
  68. // 图片预览相关
  69. const imageViewerVisible = ref(false);
  70. const imageViewerUrlList = ref<string[]>([]);
  71. const imageViewerInitialIndex = ref(0);
  72. // 计算属性:获取已成功上传的图片数量
  73. const uploadedImageCount = computed(() => {
  74. return fileList.value.filter((file: any) => file.status === "success" && file.url).length;
  75. });
  76. // 计算属性:检查是否有未上传完成的图片
  77. const hasUnuploadedImages = computed(() => {
  78. // 检查是否有正在上传的文件
  79. if (uploading.value || pendingUploadFiles.value.length > 0) {
  80. return true;
  81. }
  82. // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
  83. if (fileList.value && fileList.value.length > 0) {
  84. return fileList.value.some((file: any) => {
  85. return file.status === "ready" || file.status === "uploading";
  86. });
  87. }
  88. return false;
  89. });
  90. const handleReplace = async () => {
  91. fileList.value = [];
  92. imageUrlList.value = [];
  93. pendingUploadFiles.value = [];
  94. uploading.value = false;
  95. const params = {
  96. id: localGet("createdId")
  97. };
  98. const res: any = await getStoreContractStatus(params);
  99. if (res.data.renewContractStatus === 2) {
  100. ElMessage.warning("合同审核中,请耐心等待");
  101. } else {
  102. replaceDialogVisible.value = true;
  103. }
  104. };
  105. /**
  106. * 检查文件是否在排队中(未上传)
  107. * @param file 文件对象
  108. * @returns 是否在排队中
  109. */
  110. const isFilePending = (file: any): boolean => {
  111. // 只检查 ready 状态(排队中),不包括 uploading(正在上传)
  112. if (file.status === "ready") {
  113. return true;
  114. }
  115. // 检查是否在待上传队列中
  116. if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  117. return true;
  118. }
  119. return false;
  120. };
  121. /**
  122. * 图片上传 - 删除前确认
  123. * @param uploadFile 要删除的文件对象
  124. * @param uploadFiles 当前文件列表
  125. * @returns Promise<boolean>,true 允许删除,false 阻止删除
  126. */
  127. const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
  128. // 如果文件在排队中(未上传),禁止删除
  129. if (isFilePending(uploadFile)) {
  130. ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
  131. return false;
  132. }
  133. try {
  134. await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
  135. confirmButtonText: "确定",
  136. cancelButtonText: "取消",
  137. type: "warning"
  138. });
  139. // 用户确认删除,返回 true 允许删除
  140. return true;
  141. } catch {
  142. // 用户取消删除,返回 false 阻止删除
  143. return false;
  144. }
  145. };
  146. /**
  147. * 图片上传 - 移除图片回调(删除成功后调用)
  148. * @param uploadFile 已删除的文件对象
  149. * @param uploadFiles 删除后的文件列表
  150. */
  151. const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  152. // 从被删除的文件对象中获取 url
  153. const file = uploadFile as any;
  154. const imageUrl = file.url;
  155. if (imageUrl) {
  156. // 从 imageUrl 数组中删除对应的 URL
  157. const urlIndex = imageUrlList.value.indexOf(imageUrl);
  158. if (urlIndex > -1) {
  159. imageUrlList.value.splice(urlIndex, 1);
  160. }
  161. }
  162. if (file.url && file.url.startsWith("blob:")) {
  163. URL.revokeObjectURL(file.url);
  164. }
  165. // 同步文件列表
  166. fileList.value = [...uploadFiles];
  167. // 删除成功后提示
  168. ElMessage.success("图片已删除");
  169. };
  170. /**
  171. * 上传文件超出限制提示
  172. */
  173. const handleUploadExceed: UploadProps["onExceed"] = () => {
  174. ElMessage.warning(`最多只能上传${uploadMaxCount}张图片`);
  175. };
  176. /**
  177. * el-upload 文件变更(选中或移除)
  178. */
  179. const handleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
  180. // 检查文件类型,只允许 jpg 和 png
  181. if (uploadFile.raw) {
  182. const fileType = uploadFile.raw.type.toLowerCase();
  183. const fileName = uploadFile.name.toLowerCase();
  184. const validTypes = ["image/jpeg", "image/jpg", "image/png"];
  185. const validExtensions = [".jpg", ".jpeg", ".png"];
  186. // 检查 MIME 类型或文件扩展名
  187. const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
  188. if (!isValidType) {
  189. // 从文件列表中移除不符合类型的文件
  190. const index = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  191. if (index > -1) {
  192. fileList.value.splice(index, 1);
  193. }
  194. // 从 uploadFiles 中移除
  195. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  196. if (uploadIndex > -1) {
  197. uploadFiles.splice(uploadIndex, 1);
  198. }
  199. // 如果文件有 blob URL,释放它
  200. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  201. URL.revokeObjectURL(uploadFile.url);
  202. }
  203. ElMessage.warning("只支持上传 JPG 和 PNG 格式的图片");
  204. return;
  205. }
  206. }
  207. // 同步文件列表到表单数据(只添加通过验证的文件)
  208. const existingIndex = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  209. if (existingIndex === -1) {
  210. fileList.value.push(uploadFile);
  211. }
  212. const readyFiles = fileList.value.filter(file => file.status === "ready");
  213. if (readyFiles.length) {
  214. readyFiles.forEach(file => {
  215. if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  216. pendingUploadFiles.value.push(file);
  217. }
  218. });
  219. }
  220. processUploadQueue();
  221. };
  222. /**
  223. * 处理上传队列 - 逐个上传文件
  224. */
  225. const processUploadQueue = async () => {
  226. if (uploading.value || pendingUploadFiles.value.length === 0) {
  227. return;
  228. }
  229. // 每次只取一个文件进行上传
  230. const file = pendingUploadFiles.value.shift();
  231. if (file) {
  232. await uploadSingleFile(file);
  233. // 继续处理队列中的下一个文件
  234. processUploadQueue();
  235. }
  236. };
  237. /**
  238. * 单文件上传图片
  239. * @param file 待上传的文件
  240. */
  241. const uploadSingleFile = async (file: UploadFile) => {
  242. if (!file.raw) {
  243. return;
  244. }
  245. const rawFile = file.raw as File;
  246. const formData = new FormData();
  247. formData.append("file", rawFile);
  248. formData.append("user", "text");
  249. file.status = "uploading";
  250. file.percentage = 0;
  251. uploading.value = true;
  252. try {
  253. // 上传过程中先保持进度为 0,避免接口异常时进度条误显示 100%
  254. const result: any = await uploadContractImage(formData);
  255. if (result?.code === 200 && result.data) {
  256. // 处理单个文件的上传结果
  257. let imageUrl = result.data[0];
  258. if (!imageUrl) {
  259. throw new Error("上传成功但未获取到图片URL");
  260. }
  261. file.status = "success";
  262. file.percentage = 100;
  263. // 保存图片URL到文件对象
  264. file.url = imageUrl;
  265. file.response = { url: imageUrl };
  266. // 保存图片URL
  267. if (!Array.isArray(imageUrlList.value)) {
  268. imageUrlList.value = [];
  269. }
  270. if (!imageUrlList.value.includes(imageUrl)) {
  271. imageUrlList.value.push(imageUrl);
  272. }
  273. } else {
  274. throw new Error(result?.msg || "图片上传失败");
  275. }
  276. } catch (error: any) {
  277. // 上传失败时保持进度条为 0
  278. file.percentage = 0;
  279. file.status = "fail";
  280. if (file.url && file.url.startsWith("blob:")) {
  281. URL.revokeObjectURL(file.url);
  282. }
  283. // 从文件列表中移除失败的文件
  284. const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
  285. if (index > -1) {
  286. fileList.value.splice(index, 1);
  287. }
  288. } finally {
  289. uploading.value = false;
  290. // 触发视图更新
  291. fileList.value = [...fileList.value];
  292. }
  293. };
  294. /**
  295. * 图片预览 - 使用 el-image-viewer 预览功能
  296. * @param file 上传文件对象
  297. */
  298. const handlePictureCardPreview = (file: any) => {
  299. // 如果文件在排队中(未上传),禁止预览
  300. if (isFilePending(file)) {
  301. ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
  302. return;
  303. }
  304. // 如果文件正在上传中,允许预览(使用本地预览)
  305. if (file.status === "uploading" && file.url) {
  306. imageViewerUrlList.value = [file.url];
  307. imageViewerInitialIndex.value = 0;
  308. imageViewerVisible.value = true;
  309. return;
  310. }
  311. // 获取所有图片的 URL 列表(只包含已上传成功的图片)
  312. const urlList = fileList.value
  313. .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
  314. .map((item: any) => item.url || item.response?.data);
  315. // 找到当前点击的图片索引
  316. const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
  317. if (currentIndex < 0) {
  318. ElMessage.warning("图片尚未上传完成,无法预览");
  319. return;
  320. }
  321. imageViewerUrlList.value = urlList;
  322. imageViewerInitialIndex.value = currentIndex;
  323. imageViewerVisible.value = true;
  324. };
  325. const handleCancelReplace = async () => {
  326. // 如果有图片正在上传,阻止关闭
  327. if (hasUnuploadedImages.value) {
  328. ElMessage.warning("请等待图片上传完成后再关闭");
  329. return;
  330. }
  331. if (fileList.value.length > 0) {
  332. try {
  333. await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
  334. confirmButtonText: "确定",
  335. cancelButtonText: "取消",
  336. type: "warning"
  337. });
  338. // 用户确认取消
  339. fileList.value = [];
  340. imageUrlList.value = [];
  341. pendingUploadFiles.value = [];
  342. uploading.value = false;
  343. replaceDialogVisible.value = false;
  344. } catch {
  345. // 用户取消操作,不做任何处理
  346. }
  347. } else {
  348. replaceDialogVisible.value = false;
  349. }
  350. };
  351. const handleReplaceDialogClose = async (done: () => void) => {
  352. // 如果有图片正在上传,阻止关闭
  353. if (hasUnuploadedImages.value) {
  354. ElMessage.warning("请等待图片上传完成后再关闭");
  355. return; // 不调用 done(),阻止关闭弹窗
  356. }
  357. if (fileList.value.length > 0) {
  358. try {
  359. await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
  360. confirmButtonText: "确定",
  361. cancelButtonText: "取消",
  362. type: "warning"
  363. });
  364. // 用户确认取消,清空数据并关闭弹窗
  365. fileList.value = [];
  366. imageUrlList.value = [];
  367. pendingUploadFiles.value = [];
  368. uploading.value = false;
  369. done(); // 调用 done() 允许关闭弹窗
  370. } catch {
  371. // 用户取消操作,不调用 done(),阻止关闭弹窗
  372. }
  373. } else {
  374. // 没有文件,直接关闭
  375. done();
  376. }
  377. };
  378. const handleSubmitReplace = async () => {
  379. // 检查是否有未上传完成的图片
  380. if (hasUnuploadedImages.value) {
  381. ElMessage.warning("请等待图片上传完成后再提交");
  382. return;
  383. }
  384. if (fileList.value.length === 0) {
  385. ElMessage.warning("请至少上传一张图片");
  386. return;
  387. }
  388. const uploadedFiles = fileList.value.filter(file => file.status === "success");
  389. if (uploadedFiles.length === 0) {
  390. ElMessage.warning("请先上传图片");
  391. return;
  392. }
  393. try {
  394. // 根据文件列表顺序,生成带排序的图片数据(排序从0开始)
  395. const imageDataWithSort = uploadedFiles.map((file, index) => ({
  396. imgUrl: file.url,
  397. imgSort: index,
  398. storeId: localGet("createdId")
  399. }));
  400. await submitContractReview(imageDataWithSort);
  401. ElMessage.success("提交审核成功");
  402. replaceDialogVisible.value = false;
  403. fileList.value = [];
  404. imageUrlList.value = [];
  405. pendingUploadFiles.value = [];
  406. uploading.value = false;
  407. } catch (error) {
  408. ElMessage.error("提交审核失败");
  409. }
  410. };
  411. </script>
  412. <style lang="scss" scoped>
  413. .replace-upload-scrollbar {
  414. :deep(.el-scrollbar__wrap) {
  415. overflow-x: hidden;
  416. }
  417. }
  418. .replace-upload-area {
  419. min-height: 300px;
  420. padding: 20px;
  421. :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
  422. display: inline-flex !important;
  423. opacity: 1 !important;
  424. }
  425. :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
  426. display: inline-flex !important;
  427. opacity: 1 !important;
  428. }
  429. :deep(.el-upload-list--picture-card .el-icon--close-tip) {
  430. display: none !important;
  431. }
  432. &.upload-full {
  433. :deep(.el-upload--picture-card) {
  434. display: none !important;
  435. }
  436. }
  437. }
  438. .dialog-footer {
  439. display: flex;
  440. gap: 10px;
  441. justify-content: center;
  442. }
  443. /* el-upload 图片预览铺满容器 */
  444. :deep(.el-upload-list--picture-card) {
  445. .el-upload-list__item {
  446. overflow: hidden;
  447. .el-upload-list__item-thumbnail {
  448. width: 100%;
  449. height: 100%;
  450. object-fit: fill;
  451. }
  452. }
  453. /* 排队中(未上传)的图片禁用样式 */
  454. .el-upload-list__item[data-status="ready"],
  455. .el-upload-list__item.is-ready {
  456. position: relative;
  457. pointer-events: none;
  458. cursor: not-allowed;
  459. opacity: 0.6;
  460. &::after {
  461. position: absolute;
  462. inset: 0;
  463. z-index: 1;
  464. content: "";
  465. background-color: rgb(0 0 0 / 30%);
  466. }
  467. .el-upload-list__item-actions {
  468. pointer-events: none;
  469. opacity: 0.5;
  470. }
  471. }
  472. }
  473. .upload-trigger-card {
  474. display: flex;
  475. flex-direction: column;
  476. align-items: center;
  477. justify-content: center;
  478. width: 100%;
  479. height: 100%;
  480. font-size: 28px;
  481. color: #8c939d;
  482. .upload-tip {
  483. margin-top: 8px;
  484. font-size: 14px;
  485. color: #8c939d;
  486. }
  487. }
  488. </style>