go-flow.vue 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678
  1. <template>
  2. <div class="form-container">
  3. <div>
  4. <!-- 返回按钮 -->
  5. <el-button class="back-btn" @click="handleBack"> 返回 </el-button>
  6. <!-- 进度条 -->
  7. <div class="progress-container">
  8. <el-steps :active="currentStep" style="max-width: 1500px" align-center>
  9. <el-step v-for="(item, index) in entryList" :key="index">
  10. <template #title>
  11. <div class="step-title-wrapper">
  12. <span class="step-title">{{ item.title }}</span>
  13. </div>
  14. </template>
  15. </el-step>
  16. </el-steps>
  17. </div>
  18. </div>
  19. <!-- 第一步:个人实名 - 身份证正反面上传 -->
  20. <div v-if="currentStep === 1" class="step1-content">
  21. <div class="form-content">
  22. <h3 class="section-title">身份证正反面</h3>
  23. <div class="id-card-upload-container">
  24. <!-- 正面 -->
  25. <div class="upload-item">
  26. <div class="upload-label">正面</div>
  27. <el-upload
  28. v-model:file-list="idCardFrontList"
  29. :http-request="handleHttpUpload"
  30. list-type="picture-card"
  31. :limit="1"
  32. :on-exceed="handleExceed"
  33. :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD')"
  34. :on-preview="handlePictureCardPreview"
  35. :on-remove="handleRemove"
  36. accept="image/*"
  37. class="id-card-upload"
  38. :class="{ 'upload-complete': idCardFrontList.length > 0 }"
  39. >
  40. <template v-if="idCardFrontList.length === 0">
  41. <div class="upload-placeholder">
  42. <span class="placeholder-text">示例图</span>
  43. </div>
  44. </template>
  45. </el-upload>
  46. </div>
  47. <!-- 反面 -->
  48. <div class="upload-item">
  49. <div class="upload-label">反面</div>
  50. <el-upload
  51. v-model:file-list="idCardBackList"
  52. :http-request="handleHttpUpload"
  53. list-type="picture-card"
  54. :limit="1"
  55. :on-exceed="handleExceed"
  56. :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD')"
  57. :on-preview="handlePictureCardPreview"
  58. :on-remove="handleRemove"
  59. accept="image/*"
  60. class="id-card-upload"
  61. :class="{ 'upload-complete': idCardBackList.length > 0 }"
  62. >
  63. <template v-if="idCardBackList.length === 0">
  64. <div class="upload-placeholder">
  65. <span class="placeholder-text">示例图</span>
  66. </div>
  67. </template>
  68. </el-upload>
  69. </div>
  70. </div>
  71. <!-- OCR 识别结果展示 -->
  72. <div class="ocr-result-container" v-if="isIdCardUploadComplete">
  73. <div class="ocr-result-item" v-if="isOcrProcessing">
  74. <span class="label">识别中:</span>
  75. <span class="value">正在识别身份证信息,请稍候...</span>
  76. </div>
  77. <template v-else>
  78. <div class="ocr-result-item" v-if="ocrResult.name">
  79. <span class="label">姓名:</span>
  80. <span class="value">{{ ocrResult.name }}</span>
  81. </div>
  82. <div class="ocr-result-item" v-if="ocrResult.idCard">
  83. <span class="label">身份证号:</span>
  84. <span class="value">{{ ocrResult.idCard }}</span>
  85. </div>
  86. <div class="ocr-result-tip" v-if="!ocrResult.name && !ocrResult.idCard">请等待身份证识别完成</div>
  87. </template>
  88. </div>
  89. </div>
  90. <!-- 按钮 -->
  91. <div class="form-actions">
  92. <el-button type="primary" size="large" @click="handleNextStep"> 下一步 </el-button>
  93. </div>
  94. </div>
  95. <!-- 第二步:填写信息 -->
  96. <div v-if="currentStep === 2">
  97. <!-- 表单内容 -->
  98. <div class="form-content step2-form">
  99. <el-form :model="step2Form" :rules="step2Rules" ref="step2FormRef" label-width="125px">
  100. <div class="form-row">
  101. <!-- 左列 -->
  102. <div class="form-col">
  103. <el-form-item label="店铺名称" prop="storeName">
  104. <el-input v-model="step2Form.storeName" placeholder="请输入店铺名称" maxlength="30" />
  105. </el-form-item>
  106. <el-form-item label="容纳人数" prop="storeCapacity">
  107. <el-input-number v-model="step2Form.storeCapacity" :min="1" :max="9999" />
  108. </el-form-item>
  109. <el-form-item label="门店面积" prop="storeArea">
  110. <el-radio-group v-model="step2Form.storeArea">
  111. <el-radio label="小于20平米" value="1"> 小于20平米 </el-radio>
  112. <el-radio label="20-50平米" value="2"> 20-50平米 </el-radio>
  113. <el-radio label="50-100平米" value="3"> 50-100平米 </el-radio>
  114. <el-radio label="100-300平米" value="4"> 100-300平米 </el-radio>
  115. <el-radio label="500-1000平米" value="5"> 500-1000平米 </el-radio>
  116. <el-radio label="大于1000平米" value="6"> 大于1000平米 </el-radio>
  117. </el-radio-group>
  118. </el-form-item>
  119. <el-form-item label="所在地区" prop="region">
  120. <el-cascader :props="areaProps" v-model="step2Form.region" style="width: 100%" />
  121. </el-form-item>
  122. <el-form-item label="详细地址" prop="storeDetailAddress">
  123. <el-input v-model="step2Form.storeDetailAddress" type="textarea" :rows="3" placeholder="请输入" maxlength="255" />
  124. </el-form-item>
  125. <el-form-item label="门店简介" prop="storeBlurb">
  126. <el-input v-model="step2Form.storeBlurb" type="textarea" :rows="3" placeholder="请输入" maxlength="300" />
  127. </el-form-item>
  128. <el-form-item label="经营板块" prop="businessSection">
  129. <el-radio-group v-model="step2Form.businessSection" @change="changeBusinessSector">
  130. <el-radio
  131. v-for="businessSection in businessSectionList"
  132. :value="businessSection.dictId"
  133. :key="businessSection.dictId"
  134. >
  135. {{ businessSection.dictDetail }}
  136. </el-radio>
  137. </el-radio-group>
  138. </el-form-item>
  139. <!-- 如果有二级分类数据,显示经营种类 -->
  140. <el-form-item label="经营种类" prop="businessTypes" v-if="secondLevelList.length > 0">
  141. <el-radio-group v-model="step2Form.businessTypes" @change="changeBusinessSecondLevel">
  142. <el-radio v-for="item in secondLevelList" :value="item.dictId" :key="item.dictId">
  143. {{ item.dictDetail }}
  144. </el-radio>
  145. </el-radio-group>
  146. </el-form-item>
  147. <!-- 只有当有三级分类数据且已选中经营种类时,才显示分类(三级分类多选) -->
  148. <el-form-item
  149. label="分类"
  150. prop="businessSecondLevel"
  151. v-if="thirdLevelList && thirdLevelList.length > 0 && step2Form.businessTypes"
  152. >
  153. <el-checkbox-group v-model="step2Form.businessSecondLevel">
  154. <el-checkbox v-for="item in thirdLevelList" :key="item.dictId" :label="item.dictDetail" :value="item.dictId" />
  155. </el-checkbox-group>
  156. </el-form-item>
  157. <!-- <el-form-item label="分类" prop="businessSecondLevel">
  158. <el-radio-group v-model="step2Form.businessSecondLevel" @change="changeBusinessSecondLevel">
  159. <el-radio v-for="item in secondLevelList" :value="item.dictId" :key="item.dictId">
  160. {{ item.dictDetail }}
  161. </el-radio>
  162. </el-radio-group>
  163. </el-form-item> -->
  164. <el-form-item label="是否提供餐食" prop="businessSecondMeal" v-if="showMealOption">
  165. <el-radio-group v-model="step2Form.businessSecondMeal" @change="changeBusinessSecondMeal">
  166. <el-radio v-for="item in secondMealList" :value="item.value" :key="item.key">
  167. {{ item.dictDetail }}
  168. </el-radio>
  169. </el-radio-group>
  170. </el-form-item>
  171. </div>
  172. <!-- 右列 -->
  173. <div class="form-col">
  174. <el-form-item label="门店营业状态" prop="businessType">
  175. <el-radio-group v-model="step2Form.businessType">
  176. <el-radio label="正常营业"> 正常营业 </el-radio>
  177. <el-radio label="暂停营业"> 暂停营业 </el-radio>
  178. <el-radio label="筹建中"> 筹建中 </el-radio>
  179. </el-radio-group>
  180. </el-form-item>
  181. <el-form-item label="经度" prop="storePositionLongitude" v-show="latShow">
  182. <el-input disabled v-model="step2Form.storePositionLongitude" placeholder="请填写经度" clearable />
  183. </el-form-item>
  184. <el-form-item label="纬度" prop="storePositionLatitude" v-show="latShow">
  185. <el-input disabled v-model="step2Form.storePositionLatitude" placeholder="请填写纬度" clearable />
  186. </el-form-item>
  187. <el-form-item label="经纬度查询" prop="address">
  188. <el-select
  189. v-model="step2Form.address"
  190. filterable
  191. placeholder="请输入地址进行查询"
  192. remote
  193. reserve-keyword
  194. :remote-method="getLonAndLat"
  195. @change="selectAddress"
  196. >
  197. <el-option v-for="item in addressList" :key="item.id" :label="item.name" :value="item.location">
  198. <span style="float: left">{{ item.name }}</span>
  199. <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{ item.district }}</span>
  200. </el-option>
  201. </el-select>
  202. </el-form-item>
  203. <el-form-item label="营业执照" prop="businessLicenseAddress">
  204. <el-upload
  205. v-model:file-list="step2Form.businessLicenseAddress"
  206. :http-request="handleHttpUpload"
  207. list-type="picture-card"
  208. :limit="1"
  209. :on-exceed="handleExceed"
  210. :on-success="(response, file) => handleUploadSuccess(response, file, 'BUSINESS_LICENSE', '营业执照')"
  211. :on-preview="handlePictureCardPreview"
  212. >
  213. <el-icon><Plus /></el-icon>
  214. <template #tip>
  215. <div class="el-upload__tip">({{ step2Form.businessLicenseAddress.length }}/1)</div>
  216. </template>
  217. </el-upload>
  218. </el-form-item>
  219. <!-- 合同图片上传表单项 - 已隐藏 (2026-01-17) -->
  220. <!-- <el-form-item label="合同图片" prop="contractImageList">
  221. <el-upload
  222. v-model:file-list="step2Form.contractImageList"
  223. :http-request="handleHttpUpload"
  224. list-type="picture-card"
  225. :limit="20"
  226. :on-exceed="handleExceed"
  227. :on-success="(response, file) => handleUploadSuccess(response, file, '', '合同图片')"
  228. :on-preview="handlePictureCardPreview"
  229. >
  230. <el-icon><Plus /></el-icon>
  231. <template #tip>
  232. <div class="el-upload__tip">({{ step2Form.contractImageList.length }}/20)</div>
  233. </template>
  234. </el-upload>
  235. </el-form-item> -->
  236. <!-- 食品经营许可证上传表单项 - 已隐藏 (2026-01-17) -->
  237. <!-- <el-form-item label="食品经营许可证" prop="foodLicenceImgList" v-if="showFoodLicence">
  238. <el-upload
  239. v-model:file-list="step2Form.foodLicenceImgList"
  240. :http-request="handleHttpUpload"
  241. list-type="picture-card"
  242. :limit="1"
  243. :on-exceed="handleExceed"
  244. :on-success="(response, file) => handleUploadSuccess(response, file, 'FOOD_MANAGE_LICENSE', '食品经营许可证')"
  245. :on-preview="handlePictureCardPreview"
  246. >
  247. <el-icon><Plus /></el-icon>
  248. <template #tip>
  249. <div class="el-upload__tip">({{ step2Form.foodLicenceImgList.length }}/1)</div>
  250. </template>
  251. </el-upload>
  252. </el-form-item> -->
  253. <el-form-item label="娱乐经营许可证" prop="disportLicenceImgList" v-if="showDisportLicence">
  254. <el-upload
  255. v-model:file-list="step2Form.disportLicenceImgList"
  256. :http-request="handleHttpUpload"
  257. list-type="picture-card"
  258. :limit="1"
  259. :on-exceed="handleExceed"
  260. :on-success="(response, file) => handleUploadSuccess(response, file, 'BUSINESS_LICENSE', '娱乐经营许可证')"
  261. :on-preview="handlePictureCardPreview"
  262. >
  263. <el-icon><Plus /></el-icon>
  264. <template #tip>
  265. <div class="el-upload__tip">({{ step2Form.disportLicenceImgList.length }}/1)</div>
  266. </template>
  267. </el-upload>
  268. </el-form-item>
  269. </div>
  270. </div>
  271. </el-form>
  272. </div>
  273. <!-- 按钮 -->
  274. <div class="form-actions">
  275. <el-button type="primary" size="large" @click="handleSubmit"> 提交 </el-button>
  276. </div>
  277. </div>
  278. </div>
  279. <!-- 图片预览 -->
  280. <el-image-viewer
  281. v-if="imageViewerVisible"
  282. :url-list="imageViewerUrlList"
  283. :initial-index="imageViewerInitialIndex"
  284. @close="imageViewerVisible = false"
  285. />
  286. </template>
  287. <script setup lang="ts">
  288. import { ref, reactive, watch, onMounted, computed } from "vue";
  289. import {
  290. ElMessage,
  291. ElMessageBox,
  292. type FormInstance,
  293. type FormRules,
  294. UploadProps,
  295. UploadUserFile,
  296. UploadRequestOptions
  297. } from "element-plus";
  298. import { Plus } from "@element-plus/icons-vue";
  299. import {
  300. applyStore,
  301. getMerchantByPhone,
  302. getFirstLevelList,
  303. getSecondLevelList,
  304. getThirdLevelList,
  305. verifyIdInfo
  306. } from "@/api/modules/homeEntry";
  307. import { getInputPrompt, getDistrict, uploadImg, ocrRequestUrl, getAiapprovestoreInfo } from "@/api/modules/newLoginApi";
  308. import { localGet, localSet } from "@/utils/index";
  309. import { useAuthStore } from "@/stores/modules/auth";
  310. const authStore = useAuthStore();
  311. const userInfo = localGet("geeker-user")?.userInfo || {};
  312. const latShow = ref(false);
  313. // 图片预览相关
  314. const imageViewerVisible = ref(false);
  315. const imageViewerUrlList = ref<string[]>([]);
  316. const imageViewerInitialIndex = ref(0);
  317. const entryList = ref([
  318. {
  319. title: "个人实名"
  320. },
  321. {
  322. title: "填写信息"
  323. },
  324. {
  325. title: "等待审核"
  326. },
  327. {
  328. title: "入驻成功"
  329. }
  330. ]);
  331. // 身份证正反面上传列表
  332. const idCardFrontList = ref<UploadUserFile[]>([]);
  333. const idCardBackList = ref<UploadUserFile[]>([]);
  334. //
  335. const showFoodLicence = ref(true);
  336. const changeBusinessSecondMeal = (value: any) => {
  337. if (value == 0) {
  338. showFoodLicence.value = false;
  339. } else {
  340. showFoodLicence.value = true;
  341. }
  342. };
  343. // OCR 识别结果
  344. const ocrResult = ref<{
  345. name?: string;
  346. idCard?: string;
  347. }>({});
  348. // 食品经营许可证到期时间(从 OCR 结果中提取)
  349. const foodLicenceExpirationTime = ref<string>("");
  350. // 娱乐经营许可证到期时间(从 OCR 结果中提取)
  351. const entertainmentLicenceExpirationTime = ref<string>("");
  352. // 是否正在识别中
  353. const isOcrProcessing = ref(false);
  354. // OCR识别状态:'success' | 'failed' | 'none'(未识别)
  355. const businessLicenseOcrStatus = ref<"success" | "failed" | "none">("none"); // 营业执照OCR状态
  356. const foodLicenseOcrStatus = ref<"success" | "failed" | "none">("none"); // 食品经营许可证OCR状态
  357. const entertainmentLicenseOcrStatus = ref<"success" | "failed" | "none">("none"); // 娱乐经营许可证OCR状态
  358. // 日期格式转换函数:支持两种格式
  359. // 1. "20220508" -> "2022-05-08"
  360. // 2. "2024年01月14日" -> "2024-01-14"
  361. const formatDate = (dateStr: string): string => {
  362. if (!dateStr) {
  363. return "";
  364. }
  365. // 处理中文日期格式:2024年01月14日
  366. if (dateStr.includes("年") && dateStr.includes("月") && dateStr.includes("日")) {
  367. const match = dateStr.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
  368. if (match) {
  369. const year = match[1];
  370. const month = match[2].padStart(2, "0"); // 补零,确保是两位数
  371. const day = match[3].padStart(2, "0"); // 补零,确保是两位数
  372. return `${year}-${month}-${day}`;
  373. }
  374. }
  375. // 处理8位数字格式:20220508
  376. if (dateStr.length === 8 && /^\d{8}$/.test(dateStr)) {
  377. const year = dateStr.substring(0, 4);
  378. const month = dateStr.substring(4, 6);
  379. const day = dateStr.substring(6, 8);
  380. return `${year}-${month}-${day}`;
  381. }
  382. return "";
  383. };
  384. // 计算是否已上传完成(正反面都上传完成)
  385. const isIdCardUploadComplete = computed(() => {
  386. return idCardFrontList.value.length > 0 && idCardBackList.value.length > 0;
  387. });
  388. // 计算是否需要显示娱乐经营许可证(酒吧或KTV时显示)
  389. const showDisportLicence = computed(() => {
  390. const sectionName = step2Form.businessSectionName;
  391. const sectionId = step2Form.businessSection;
  392. // 通过名称判断(最直接的方式)
  393. if (sectionName === "酒吧" || sectionName === "KTV" || sectionName === "ktv") {
  394. return true;
  395. }
  396. // 通过查找 businessSectionList 来判断(兼容 id 和 dictId)
  397. if (businessSectionList.value.length > 0 && sectionId) {
  398. const selectedSection = businessSectionList.value.find((item: any) => {
  399. // 匹配 id 或 dictId
  400. const isMatchId =
  401. item.id === sectionId ||
  402. item.dictId === sectionId ||
  403. String(item.id) === String(sectionId) ||
  404. String(item.dictId) === String(sectionId);
  405. // 匹配名称
  406. const isMatchName = item.dictDetail === "酒吧" || item.dictDetail === "KTV" || item.dictDetail === "ktv";
  407. return isMatchId && isMatchName;
  408. });
  409. if (selectedSection) {
  410. return true;
  411. }
  412. }
  413. // 通过 dictId 判断(KTV 的 dictId 通常是 "3")
  414. if (String(sectionId) === "3") {
  415. return true;
  416. }
  417. return false;
  418. });
  419. // 计算是否需要显示"是否提供餐食"(KTV、按摩足疗、丽人美发、运动健身、洗浴汗蒸时显示)
  420. const showMealOption = computed(() => {
  421. const sectionName = step2Form.businessSectionName;
  422. const sectionId = step2Form.businessSection;
  423. // 需要显示的经营板块名称列表
  424. const mealSectionNames = ["KTV", "ktv", "按摩足疗", "丽人美发", "运动健身", "洗浴汗蒸"];
  425. // 通过名称判断(最直接的方式)
  426. if (mealSectionNames.includes(sectionName)) {
  427. return true;
  428. }
  429. // 通过查找 businessSectionList 来判断(兼容 id 和 dictId)
  430. if (businessSectionList.value.length > 0 && sectionId) {
  431. const selectedSection = businessSectionList.value.find((item: any) => {
  432. // 匹配 id 或 dictId
  433. const isMatchId =
  434. item.dictId === sectionId ||
  435. item.dictId === sectionId ||
  436. String(item.dictId) === String(sectionId) ||
  437. String(item.dictId) === String(sectionId);
  438. // 匹配名称
  439. const isMatchName = mealSectionNames.includes(item.dictDetail);
  440. return isMatchId && isMatchName;
  441. });
  442. if (selectedSection) {
  443. return true;
  444. }
  445. }
  446. // 通过 dictId 判断(KTV=3, 按摩足疗=5, 丽人美发=6, 运动健身=7, 洗浴汗蒸=4)
  447. const mealSectionIds = ["3", "4", "5", "6", "7"];
  448. if (mealSectionIds.includes(String(sectionId))) {
  449. return true;
  450. }
  451. return false;
  452. });
  453. // 下一步 - 验证身份证正反面是否已上传
  454. const handleNextStep = async () => {
  455. // 识别成功,进入下一步
  456. try {
  457. const res: any = await verifyIdInfo({
  458. idCard: ocrResult.value.idCard,
  459. name: ocrResult.value.name,
  460. appType: 1
  461. });
  462. if (res.code === 200) {
  463. ElMessage.success("身份证识别成功");
  464. setStep(2);
  465. } else {
  466. ElMessage.error(res?.msg || "身份证识别失败");
  467. }
  468. } catch (error: any) {
  469. console.log(error);
  470. }
  471. };
  472. const secondMealList = ref([
  473. { key: 1, value: 1, dictDetail: "提供" },
  474. { key: 0, value: 0, dictDetail: "不提供" }
  475. ]);
  476. const step2Rules: FormRules = {
  477. storeName: [{ required: true, message: "请输入店铺名称", trigger: "blur" }],
  478. storeCapacity: [{ required: true, message: "请输入容纳人数", trigger: "blur" }],
  479. storeArea: [{ required: true, message: "请选择门店面积", trigger: "change" }],
  480. storeBlurb: [{ required: true, message: "请输入门店简介", trigger: "change" }],
  481. storeIntro: [{ required: true, message: "请输入门店简介", trigger: "blur" }],
  482. businessSection: [{ required: true, message: "请选择经营板块", trigger: "change" }],
  483. businessSecondLevel: [{ required: true, message: "请选择分类", trigger: "change" }],
  484. businessSecondMeal: [{ required: true, message: "请选择是否提供餐食", trigger: "change" }],
  485. businessTypes: [
  486. {
  487. required: true,
  488. message: "请选择经营种类",
  489. trigger: "change",
  490. validator: (rule: any, value: any, callback: any) => {
  491. if (!value || value.length === 0) {
  492. callback(new Error("请选择经营种类"));
  493. } else {
  494. callback();
  495. }
  496. }
  497. }
  498. ],
  499. address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
  500. businessLicenseAddress: [{ required: true, message: "请上传营业执照", trigger: "change" }],
  501. // contractImageList: [{ required: true, message: "请上传合同图片", trigger: "change" }], // 已隐藏 (2026-01-17)
  502. // foodLicenceImgList: [{ required: true, message: "请上传食品经营许可证", trigger: "change" }], // 已隐藏 (2026-01-17)
  503. disportLicenceImgList: [{ required: true, message: "请上传娱乐经营许可证", trigger: "change" }]
  504. };
  505. //地址集合
  506. const addressList = ref<any[]>([]);
  507. //查询地址名称
  508. const queryAddress = ref<string>("");
  509. const props = defineProps({
  510. currentStep: {
  511. type: Number,
  512. default: 1
  513. },
  514. storeApplicationStatus: {
  515. type: Number,
  516. default: 0
  517. }
  518. });
  519. const emit = defineEmits(["update:currentStep", "update:get-user-info"]);
  520. // 调用父组件的 getUserInfo 方法
  521. const callGetUserInfo = () => {
  522. emit("update:get-user-info");
  523. };
  524. // 内部步骤状态,和父组件同步
  525. const currentStep = ref<number>(props.currentStep || 1);
  526. const storeApplicationStatus = ref<number>(props.storeApplicationStatus || 0);
  527. watch(
  528. () => props.currentStep,
  529. val => {
  530. if (typeof val === "number") currentStep.value = val;
  531. }
  532. );
  533. watch(
  534. () => props.storeApplicationStatus,
  535. val => {
  536. if (typeof val === "number") storeApplicationStatus.value = val;
  537. }
  538. );
  539. // 隐藏财务管理菜单的函数
  540. const hideFinancialManagementMenu = () => {
  541. const hideMenus = (menuList: any[]) => {
  542. menuList.forEach(menu => {
  543. if (menu.name && menu.name === "financialManagement") {
  544. menu.meta.isHide = true;
  545. }
  546. if (menu.children && menu.children.length > 0) {
  547. hideMenus(menu.children);
  548. }
  549. });
  550. };
  551. if (authStore.authMenuList && authStore.authMenuList.length > 0) {
  552. hideMenus(authStore.authMenuList);
  553. }
  554. };
  555. // 显示财务管理菜单的函数
  556. const showFinancialManagementMenu = () => {
  557. const showMenus = (menuList: any[]) => {
  558. menuList.forEach(menu => {
  559. if (menu.name && menu.name === "financialManagement") {
  560. menu.meta.isHide = false;
  561. }
  562. if (menu.children && menu.children.length > 0) {
  563. showMenus(menu.children);
  564. }
  565. });
  566. };
  567. if (authStore.authMenuList && authStore.authMenuList.length > 0) {
  568. showMenus(authStore.authMenuList);
  569. }
  570. };
  571. // 更新缓存中的 storeId
  572. const updateStoreIdInCache = async () => {
  573. try {
  574. const geekerUser = localGet("geeker-user");
  575. if (!geekerUser || !geekerUser.userInfo || !geekerUser.userInfo.phone) {
  576. console.error("用户信息不存在");
  577. return;
  578. }
  579. const phone = geekerUser.userInfo.phone;
  580. const res: any = await getMerchantByPhone({ phone });
  581. if (res && res.code == 200 && res.data && res.data.storeId) {
  582. geekerUser.userInfo.storeId = res.data.storeId;
  583. localSet("geeker-user", geekerUser);
  584. if (res.data.storeId) {
  585. localSet("createdId", res.data.storeId);
  586. }
  587. }
  588. } catch (error) {
  589. console.error("更新 storeId 缓存失败:", error);
  590. }
  591. };
  592. // 监听步骤和审核状态
  593. watch([() => currentStep.value, () => storeApplicationStatus.value], ([step, status]) => {
  594. if (step === 3 && (status === 0 || status === 2)) {
  595. updateStoreIdInCache();
  596. }
  597. if (status === 2) {
  598. hideFinancialManagementMenu();
  599. }
  600. if (status === 1) {
  601. showFinancialManagementMenu();
  602. }
  603. });
  604. // 监听菜单列表变化
  605. watch(
  606. () => authStore.authMenuList.length,
  607. newLength => {
  608. if (newLength > 0) {
  609. if (storeApplicationStatus.value === 2) {
  610. hideFinancialManagementMenu();
  611. }
  612. if (storeApplicationStatus.value === 1) {
  613. showFinancialManagementMenu();
  614. }
  615. }
  616. }
  617. );
  618. onMounted(() => {
  619. getBusinessSectionList();
  620. callGetUserInfo();
  621. if (currentStep.value === 3 && (storeApplicationStatus.value === 0 || storeApplicationStatus.value === 2)) {
  622. updateStoreIdInCache();
  623. }
  624. if (storeApplicationStatus.value === 2) {
  625. hideFinancialManagementMenu();
  626. } else if (storeApplicationStatus.value === 1) {
  627. showFinancialManagementMenu();
  628. }
  629. });
  630. const setStep = (val: number) => {
  631. currentStep.value = val;
  632. emit("update:currentStep", val);
  633. };
  634. // 第二步表单
  635. const step2FormRef = ref<FormInstance>();
  636. const step2Form = reactive({
  637. businessSecondMeal: 1,
  638. storeName: "",
  639. storeCapacity: 1,
  640. storeArea: "1",
  641. isChain: 0,
  642. storeDetailAddress: "",
  643. region: [],
  644. administrativeRegionProvinceAdcode: "",
  645. administrativeRegionCityAdcode: "",
  646. administrativeRegionDistrictAdcode: "",
  647. storeAddress: "",
  648. storeBlurb: "",
  649. businessSection: "1",
  650. businessSectionName: "",
  651. businessSecondLevel: [] as string[],
  652. businessTypes: "" as string,
  653. businessTypesList: [] as string[],
  654. businessStatus: 0,
  655. storeStatus: 1,
  656. businessType: "正常营业",
  657. storePositionLongitude: "",
  658. storePositionLatitude: "",
  659. businessLicenseAddress: [] as UploadUserFile[],
  660. contractImageList: [] as UploadUserFile[],
  661. foodLicenceImgList: [] as UploadUserFile[],
  662. disportLicenceImgList: [] as UploadUserFile[],
  663. address: ""
  664. });
  665. // 返回按钮
  666. const handleBack = () => {
  667. if (currentStep.value === 1) {
  668. setStep(0);
  669. } else if (currentStep.value === 2) {
  670. setStep(1);
  671. } else if (currentStep.value === 3) {
  672. setStep(2);
  673. }
  674. };
  675. // 地区选择
  676. const areaProps: any = {
  677. lazy: true,
  678. async lazyLoad(node, resolve) {
  679. const { level } = node;
  680. try {
  681. let param = { adCode: node.data.adCode ? node.data.adCode : "" };
  682. const response: any = await getDistrict(param as any);
  683. const nodes = (response?.data?.districts?.[0]?.districts || []).map((item: any) => ({
  684. value: item.adcode,
  685. adCode: item.adcode,
  686. label: item.name,
  687. leaf: level >= 2
  688. }));
  689. resolve(nodes);
  690. } catch (error) {
  691. resolve([]);
  692. }
  693. }
  694. };
  695. watch(
  696. () => step2Form.region,
  697. (newVal: any[]) => {
  698. if (newVal.length > 0) {
  699. step2Form.administrativeRegionProvinceAdcode = newVal[0];
  700. step2Form.administrativeRegionCityAdcode = newVal[1];
  701. step2Form.administrativeRegionDistrictAdcode = newVal[2];
  702. }
  703. }
  704. );
  705. //经营板块 - 一级分类
  706. const businessSectionList = ref<any[]>([]);
  707. const getBusinessSectionList = async () => {
  708. try {
  709. const res: any = await getFirstLevelList({});
  710. if (res && res.code === 200 && res.data) {
  711. businessSectionList.value = res.data;
  712. // 如果有数据,自动加载第一个经营板块的二级分类(经营种类)
  713. if (res.data && res.data.length > 0) {
  714. const firstSection = res.data[0];
  715. const firstDictId = firstSection.dictId || firstSection.id;
  716. if (firstDictId) {
  717. try {
  718. const secondRes: any = await getSecondLevelList({ parentDictId: String(firstDictId) });
  719. if (secondRes && (secondRes.code === 200 || secondRes.code === "200") && secondRes.data) {
  720. secondLevelList.value = secondRes.data;
  721. }
  722. } catch (error) {
  723. console.error("获取二级分类失败:", error);
  724. }
  725. }
  726. }
  727. }
  728. } catch (error) {
  729. console.error("获取一级分类失败:", error);
  730. ElMessage.error("获取经营板块失败");
  731. }
  732. };
  733. // 二级分类列表
  734. const secondLevelList = ref<any[]>([]);
  735. // 三级分类列表
  736. const thirdLevelList = ref<any[]>([]);
  737. // 一级分类变化时,加载二级分类
  738. const changeBusinessSector = async (dictId: string | number | boolean | undefined) => {
  739. const dictIdStr = String(dictId || "");
  740. if (!dictIdStr) {
  741. secondLevelList.value = [];
  742. thirdLevelList.value = [];
  743. step2Form.businessSecondLevel = [];
  744. step2Form.businessTypes = "";
  745. step2Form.businessTypesList = [];
  746. step2Form.businessSection = "";
  747. step2Form.businessSectionName = "";
  748. return;
  749. }
  750. // 更新一级分类信息(模板中 :value 绑定的是 dictId,所以这里查找时优先使用 dictId)
  751. const selectedSection = businessSectionList.value.find(
  752. (item: any) => String(item.dictId) === dictIdStr || String(item.id) === dictIdStr
  753. );
  754. if (selectedSection) {
  755. // 保持与模板中 :value="businessSection.dictId" 绑定一致,使用 dictId
  756. step2Form.businessSection = String(selectedSection.dictId || selectedSection.id);
  757. step2Form.businessSectionName = selectedSection.dictDetail;
  758. } else {
  759. // 如果没找到,直接使用传入的值
  760. step2Form.businessSection = dictIdStr;
  761. step2Form.businessSectionName = "";
  762. }
  763. // 清空二级和三级分类
  764. secondLevelList.value = [];
  765. thirdLevelList.value = [];
  766. step2Form.businessSecondLevel = [];
  767. step2Form.businessTypes = "";
  768. step2Form.businessTypesList = [];
  769. // 加载二级分类(使用 dictId 作为 parentDictId)
  770. const parentDictId = selectedSection?.dictId || dictIdStr;
  771. try {
  772. const res: any = await getSecondLevelList({ parentDictId });
  773. if (res && (res.code === 200 || res.code === "200") && res.data) {
  774. secondLevelList.value = res.data;
  775. }
  776. } catch (error) {
  777. console.error("获取二级分类失败:", error);
  778. ElMessage.error("获取经营种类失败");
  779. }
  780. };
  781. // 二级分类变化时,加载三级分类
  782. const changeBusinessSecondLevel = async (dictId: string | number | boolean | undefined) => {
  783. const dictIdStr = String(dictId || "");
  784. if (!dictIdStr) {
  785. thirdLevelList.value = [];
  786. step2Form.businessTypes = "";
  787. step2Form.businessTypesList = [];
  788. return;
  789. }
  790. // 清空三级分类(但不清空 businessTypes,因为这是用户刚选择的值)
  791. thirdLevelList.value = [];
  792. step2Form.businessTypesList = [];
  793. // 加载三级分类
  794. try {
  795. const res: any = await getThirdLevelList(dictIdStr);
  796. if (res && (res.code === 200 || res.code === "200") && res.data && Array.isArray(res.data) && res.data.length > 0) {
  797. thirdLevelList.value = res.data;
  798. } else {
  799. // 如果没有三级分类数据,确保清空
  800. thirdLevelList.value = [];
  801. }
  802. } catch (error) {
  803. console.error("获取三级分类失败:", error);
  804. // 如果没有三级分类,不显示错误,因为可能该二级分类下没有三级分类
  805. // 确保清空三级分类列表
  806. thirdLevelList.value = [];
  807. }
  808. };
  809. // 经纬度查询
  810. const getLonAndLat = async (keyword: string) => {
  811. if (keyword) {
  812. let param = {
  813. addressName: keyword
  814. };
  815. let res: any = await getInputPrompt(param as any);
  816. if (res.code == "200") {
  817. addressList.value = res?.data?.tips || [];
  818. } else {
  819. ElMessage.error("查询失败!");
  820. }
  821. } else {
  822. addressList.value = [];
  823. }
  824. };
  825. const selectAddress = async (param: any) => {
  826. if (!step2Form.address || typeof step2Form.address !== "string") {
  827. ElMessage.warning("地址格式不正确,请重新选择");
  828. return;
  829. }
  830. if (!step2Form.address.includes(",")) {
  831. ElMessage.warning("地址格式不正确,缺少经纬度信息");
  832. return;
  833. }
  834. let locationList = step2Form.address.split(",");
  835. if (locationList.length < 2) {
  836. ElMessage.warning("地址格式不正确,无法获取经纬度");
  837. return;
  838. }
  839. addressList.value.forEach((item: any) => {
  840. if (item.location == step2Form.address) {
  841. queryAddress.value = item.name;
  842. }
  843. });
  844. step2Form.storePositionLongitude = locationList[0]?.trim() || "";
  845. step2Form.storePositionLatitude = locationList[1]?.trim() || "";
  846. if (!step2Form.storePositionLongitude || !step2Form.storePositionLatitude) {
  847. ElMessage.warning("无法获取有效的经纬度信息");
  848. return;
  849. }
  850. latShow.value = true;
  851. };
  852. //文件上传
  853. const handleHttpUpload = async (options: UploadRequestOptions) => {
  854. let formData = new FormData();
  855. formData.append("file", options.file);
  856. try {
  857. const res: any = await uploadImg(formData);
  858. const fileUrl = res?.data?.fileUrl || res?.data?.[0] || res?.fileUrl;
  859. if (fileUrl) {
  860. options.onSuccess({ fileUrl });
  861. } else {
  862. throw new Error("上传失败:未获取到文件URL");
  863. }
  864. } catch (error) {
  865. options.onError(error as any);
  866. ElMessage.error("文件上传失败,请重试");
  867. }
  868. };
  869. // 自动调用 OCR 识别
  870. const autoOcrRecognition = async (ocrType: string, name: string) => {
  871. // 如果正在识别中,不重复调用
  872. if (isOcrProcessing.value) {
  873. return;
  874. }
  875. let imageUrls = "";
  876. let fileList: UploadUserFile[] = [];
  877. // 根据不同的 ocrType 获取对应的图片 URL
  878. if (ocrType === "ID_CARD") {
  879. // 身份证:需要检查正反面是否都已上传完成
  880. if (idCardFrontList.value.length === 0 || idCardBackList.value.length === 0) {
  881. return;
  882. }
  883. const frontFile = idCardFrontList.value[0];
  884. const backFile = idCardBackList.value[0];
  885. // 验证上传的文件是否成功
  886. if (frontFile.status !== "success" || !frontFile.url || backFile.status !== "success" || !backFile.url) {
  887. return;
  888. }
  889. // 获取身份证正反面的 URL
  890. const frontUrl = getFileUrls(idCardFrontList.value)[0] || "";
  891. const backUrl = getFileUrls(idCardBackList.value)[0] || "";
  892. if (!frontUrl || !backUrl) {
  893. return;
  894. }
  895. // 将正反面 URL 用逗号分隔
  896. imageUrls = `${frontUrl},${backUrl}`;
  897. } else if (ocrType === "BUSINESS_LICENSE") {
  898. // 营业执照或娱乐经营许可证:检查是否已上传
  899. // 优先检查营业执照,如果没有再检查娱乐经营许可证
  900. let fileList: UploadUserFile[] = [];
  901. if (name == "营业执照") {
  902. fileList = step2Form.businessLicenseAddress;
  903. } else if (name == "娱乐经营许可证") {
  904. fileList = step2Form.disportLicenceImgList;
  905. } else {
  906. return;
  907. }
  908. const file = fileList[0];
  909. if (file.status !== "success" || !file.url) {
  910. return;
  911. }
  912. const fileUrl = getFileUrls(fileList)[0] || "";
  913. if (!fileUrl) {
  914. return;
  915. }
  916. imageUrls = fileUrl;
  917. } else if (ocrType === "FOOD_MANAGE_LICENSE") {
  918. // 食品经营许可证:检查是否已上传
  919. if (step2Form.foodLicenceImgList.length === 0) {
  920. return;
  921. }
  922. const file = step2Form.foodLicenceImgList[0];
  923. if (file.status !== "success" || !file.url) {
  924. return;
  925. }
  926. const fileUrl = getFileUrls(step2Form.foodLicenceImgList)[0] || "";
  927. if (!fileUrl) {
  928. return;
  929. }
  930. imageUrls = fileUrl;
  931. } else {
  932. // 其他类型不进行 OCR
  933. return;
  934. }
  935. let params = {
  936. imageUrls: imageUrls,
  937. ocrType: ocrType,
  938. storeId: userInfo.storeId,
  939. storeUserId: userInfo.id
  940. };
  941. try {
  942. isOcrProcessing.value = true;
  943. const res: any = await ocrRequestUrl(params);
  944. if (res && (res.code === 200 || res.code === "200")) {
  945. // 只有身份证类型才需要保存识别结果到 ocrResult
  946. if (ocrType === "ID_CARD" && res.data && Array.isArray(res.data) && res.data.length > 0) {
  947. // 从第一个元素获取正面或反面数据
  948. const firstItem = res.data[0];
  949. const secondItem = res.data[1];
  950. // 根据数据键值判断哪一个是正面(face)或反面(back)
  951. const faceData = firstItem?.face?.data || secondItem?.face?.data;
  952. const backData = firstItem?.back?.data || secondItem?.back?.data;
  953. // 提取姓名(从正面数据中获取)
  954. const name = faceData?.name || "";
  955. // 提取身份证号(优先从正面数据中获取,如果没有则从反面获取)
  956. let idCard = faceData?.idNumber || backData?.idNumber || "";
  957. // 如果从 data 中获取不到,尝试从 prism_keyValueInfo 中查找
  958. if (!idCard) {
  959. // 先从正面查找
  960. if (firstItem?.face?.prism_keyValueInfo) {
  961. const idNumberInfo = firstItem.face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
  962. if (idNumberInfo) {
  963. idCard = idNumberInfo.value;
  964. }
  965. }
  966. // 如果正面没有,再从反面查找
  967. if (!idCard && secondItem?.face?.prism_keyValueInfo) {
  968. const idNumberInfo = secondItem.face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
  969. if (idNumberInfo) {
  970. idCard = idNumberInfo.value;
  971. }
  972. }
  973. }
  974. ocrResult.value = {
  975. name: name,
  976. idCard: idCard
  977. };
  978. // 更新本地存储中的用户信息
  979. const geekerUser = localGet("geeker-user");
  980. if (geekerUser && geekerUser.userInfo) {
  981. if (ocrResult.value.name) {
  982. geekerUser.userInfo.name = ocrResult.value.name;
  983. }
  984. if (ocrResult.value.idCard) {
  985. geekerUser.userInfo.idCard = ocrResult.value.idCard;
  986. }
  987. localSet("geeker-user", geekerUser);
  988. }
  989. ElMessage.success("身份证识别成功");
  990. } else if (ocrType === "BUSINESS_LICENSE") {
  991. // 判断是营业执照还是娱乐经营许可证
  992. const isBusinessLicense = step2Form.businessLicenseAddress.length > 0;
  993. // 提取 validToDate 字段(娱乐经营许可证)
  994. if (!isBusinessLicense && res.data && Array.isArray(res.data) && res.data.length > 0) {
  995. const validToDate = res.data[0]?.validToDate || "";
  996. if (validToDate) {
  997. entertainmentLicenceExpirationTime.value = formatDate(validToDate);
  998. console.log(entertainmentLicenceExpirationTime.value);
  999. }
  1000. }
  1001. if (name == "营业执照") {
  1002. businessLicenseOcrStatus.value = "success";
  1003. ElMessage.success("营业执照识别成功");
  1004. } else if (name == "娱乐经营许可证") {
  1005. entertainmentLicenseOcrStatus.value = "success";
  1006. ElMessage.success("娱乐经营许可证识别成功");
  1007. }
  1008. } else if (ocrType === "FOOD_MANAGE_LICENSE") {
  1009. // 提取食品经营许可证的 validToDate 字段
  1010. if (res.data && Array.isArray(res.data) && res.data.length > 0) {
  1011. const validToDate = res.data[0]?.validToDate || "";
  1012. if (validToDate) {
  1013. console.log(formatDate(validToDate));
  1014. foodLicenceExpirationTime.value = formatDate(validToDate);
  1015. console.log(foodLicenceExpirationTime.value);
  1016. }
  1017. }
  1018. foodLicenseOcrStatus.value = "success";
  1019. ElMessage.success("食品经营许可证识别成功");
  1020. }
  1021. } else {
  1022. console.warn("OCR 识别失败:", res?.msg);
  1023. if (name === "营业执照") {
  1024. businessLicenseOcrStatus.value = "failed";
  1025. } else if (name === "娱乐经营许可证") {
  1026. entertainmentLicenseOcrStatus.value = "failed";
  1027. } else if (ocrType === "FOOD_MANAGE_LICENSE") {
  1028. foodLicenseOcrStatus.value = "failed";
  1029. }
  1030. ElMessage.error(res?.msg || "识别失败,请重试");
  1031. }
  1032. } catch (error) {
  1033. if (name === "营业执照") {
  1034. businessLicenseOcrStatus.value = "failed";
  1035. } else if (name === "娱乐经营许可证") {
  1036. entertainmentLicenseOcrStatus.value = "failed";
  1037. } else if (ocrType === "FOOD_MANAGE_LICENSE") {
  1038. foodLicenseOcrStatus.value = "failed";
  1039. }
  1040. ElMessage.error("识别失败,请重试");
  1041. } finally {
  1042. isOcrProcessing.value = false;
  1043. }
  1044. };
  1045. // 文件上传成功回调
  1046. const handleUploadSuccess = (response: any, uploadFile: UploadUserFile, ocrType: string, name: string) => {
  1047. if (response?.fileUrl) {
  1048. uploadFile.url = response.fileUrl;
  1049. }
  1050. // 只有指定的类型才进行 OCR 识别
  1051. if (ocrType && (ocrType === "ID_CARD" || ocrType === "BUSINESS_LICENSE" || ocrType === "FOOD_MANAGE_LICENSE")) {
  1052. if (name === "营业执照") {
  1053. businessLicenseOcrStatus.value = "none";
  1054. } else if (name === "娱乐经营许可证") {
  1055. entertainmentLicenseOcrStatus.value = "none";
  1056. } else if (ocrType === "FOOD_MANAGE_LICENSE") {
  1057. foodLicenseOcrStatus.value = "none";
  1058. }
  1059. // 延迟一下,确保文件状态已更新,然后检查是否需要自动 OCR
  1060. setTimeout(() => {
  1061. autoOcrRecognition(ocrType, name);
  1062. }, 100);
  1063. }
  1064. };
  1065. // 图片预览处理函数
  1066. const handlePictureCardPreview = (file: UploadUserFile) => {
  1067. if (file.status === "uploading" && file.url) {
  1068. imageViewerUrlList.value = [file.url];
  1069. imageViewerInitialIndex.value = 0;
  1070. imageViewerVisible.value = true;
  1071. return;
  1072. }
  1073. let urlList: string[] = [];
  1074. let currentFileList: UploadUserFile[] = [];
  1075. // 判断是哪个上传组件的文件
  1076. if (idCardFrontList.value.some((f: UploadUserFile) => f.uid === file.uid)) {
  1077. currentFileList = idCardFrontList.value;
  1078. } else if (idCardBackList.value.some((f: UploadUserFile) => f.uid === file.uid)) {
  1079. currentFileList = idCardBackList.value;
  1080. } else if (step2Form.businessLicenseAddress.some((f: UploadUserFile) => f.uid === file.uid)) {
  1081. currentFileList = step2Form.businessLicenseAddress;
  1082. } else if (step2Form.contractImageList.some((f: UploadUserFile) => f.uid === file.uid)) {
  1083. currentFileList = step2Form.contractImageList;
  1084. } else if (step2Form.foodLicenceImgList.some((f: UploadUserFile) => f.uid === file.uid)) {
  1085. currentFileList = step2Form.foodLicenceImgList;
  1086. } else if (step2Form.disportLicenceImgList.some((f: UploadUserFile) => f.uid === file.uid)) {
  1087. currentFileList = step2Form.disportLicenceImgList;
  1088. }
  1089. urlList = currentFileList
  1090. .filter((item: UploadUserFile) => item.status === "success" && (item.url || (item.response as any)?.fileUrl))
  1091. .map((item: UploadUserFile) => item.url || (item.response as any)?.fileUrl);
  1092. const currentUrl = file.url || (file.response as any)?.fileUrl;
  1093. const currentIndex = urlList.findIndex((url: string) => url === currentUrl);
  1094. if (currentIndex < 0) {
  1095. ElMessage.warning("图片尚未上传完成,无法预览");
  1096. return;
  1097. }
  1098. imageViewerUrlList.value = urlList;
  1099. imageViewerInitialIndex.value = currentIndex;
  1100. imageViewerVisible.value = true;
  1101. };
  1102. // 文件移除处理
  1103. const handleRemove = (file: UploadUserFile) => {
  1104. // 文件移除时,清空 OCR 识别结果
  1105. ocrResult.value = {};
  1106. isOcrProcessing.value = false;
  1107. // 根据文件所属列表重置对应的OCR状态
  1108. if (step2Form.businessLicenseAddress.some((f: UploadUserFile) => f.uid === file.uid)) {
  1109. // 移除的是营业执照
  1110. businessLicenseOcrStatus.value = "none";
  1111. } else if (step2Form.foodLicenceImgList.some((f: UploadUserFile) => f.uid === file.uid)) {
  1112. // 移除的是食品经营许可证
  1113. foodLicenseOcrStatus.value = "none";
  1114. } else if (step2Form.disportLicenceImgList.some((f: UploadUserFile) => f.uid === file.uid)) {
  1115. // 移除的是娱乐经营许可证
  1116. entertainmentLicenseOcrStatus.value = "none";
  1117. }
  1118. };
  1119. // 提取文件列表中的URL
  1120. const getFileUrls = (fileList: UploadUserFile[]): string[] => {
  1121. return fileList
  1122. .map((file: UploadUserFile) => {
  1123. const response = file.response as any;
  1124. return file.url || response?.fileUrl || "";
  1125. })
  1126. .filter((url: string) => url);
  1127. };
  1128. // 根据adcode获取地区详细信息
  1129. const getDistrictInfo = async (adcode: string) => {
  1130. try {
  1131. const response: any = await getDistrict({ adCode: adcode } as any);
  1132. const district = response?.data?.districts?.[0];
  1133. if (district) {
  1134. return {
  1135. citycode: district.citycode ? [district.citycode] : [],
  1136. adcode: district.adcode,
  1137. level: district.level,
  1138. center: district.center,
  1139. name: district.name,
  1140. districts: []
  1141. };
  1142. }
  1143. } catch (error) {
  1144. console.error("获取地区信息失败:", error);
  1145. }
  1146. return null;
  1147. };
  1148. // 构建whereAddress数组
  1149. const buildWhereAddress = async (regionCodes: string[]) => {
  1150. const whereAddress: any[] = [];
  1151. if (regionCodes && regionCodes.length > 0) {
  1152. for (const code of regionCodes) {
  1153. const districtInfo = await getDistrictInfo(code);
  1154. if (districtInfo) {
  1155. whereAddress.push(districtInfo);
  1156. }
  1157. }
  1158. }
  1159. return whereAddress;
  1160. };
  1161. const handleAi = async () => {
  1162. const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
  1163. const contractImageUrls = getFileUrls(step2Form.contractImageList);
  1164. const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList);
  1165. const disportLicenceUrls = getFileUrls(step2Form.disportLicenceImgList);
  1166. const licenseImages = [...businessLicenseUrls, ...contractImageUrls, ...foodLicenceUrls, ...disportLicenceUrls].filter(Boolean);
  1167. const params: any = {
  1168. business_scope: step2Form.storeBlurb,
  1169. contact_email: "",
  1170. contact_name: userInfo.name,
  1171. contact_phone: userInfo.phone,
  1172. license_images: licenseImages,
  1173. merchant_name: step2Form.storeName,
  1174. userId: userInfo.id
  1175. };
  1176. const res: any = await getAiapprovestoreInfo(params);
  1177. if (res.code == 200) {
  1178. ElMessage.success(res.msg || "AI智能审核成功");
  1179. } else {
  1180. ElMessage.error(res?.msg || "AI智能审核失败");
  1181. }
  1182. };
  1183. // 提交
  1184. const handleSubmit = async () => {
  1185. if (!step2FormRef.value) return;
  1186. await step2FormRef.value.validate(async valid => {
  1187. if (valid) {
  1188. // 检查OCR识别状态,如果有识别失败的证照,提示用户重新上传
  1189. const failedLicenses: string[] = [];
  1190. if (step2Form.businessLicenseAddress.length > 0 && businessLicenseOcrStatus.value === "failed") {
  1191. failedLicenses.push("营业执照");
  1192. }
  1193. if (step2Form.foodLicenceImgList.length > 0 && foodLicenseOcrStatus.value === "failed") {
  1194. failedLicenses.push("食品经营许可证");
  1195. }
  1196. if (step2Form.disportLicenceImgList.length > 0 && entertainmentLicenseOcrStatus.value === "failed") {
  1197. failedLicenses.push("娱乐经营许可证");
  1198. }
  1199. if (failedLicenses.length > 0) {
  1200. ElMessage.warning("请重新上传证照");
  1201. return;
  1202. }
  1203. const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
  1204. // const contractImageUrls = getFileUrls(step2Form.contractImageList); // 已隐藏 (2026-01-17)
  1205. // const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList); // 已隐藏 (2026-01-17)
  1206. const contractImageUrls: string[] = []; // 空数组替代 (2026-01-17)
  1207. const foodLicenceUrls: string[] = []; // 空数组替代 (2026-01-17)
  1208. const disportLicenceUrls = getFileUrls(step2Form.disportLicenceImgList);
  1209. const whereAddress = await buildWhereAddress(step2Form.region);
  1210. let storeStatus = 1;
  1211. if (step2Form.businessType === "正常营业") {
  1212. storeStatus = 1;
  1213. } else if (step2Form.businessType === "暂停营业") {
  1214. storeStatus = 0;
  1215. } else if (step2Form.businessType === "筹建中") {
  1216. storeStatus = 2;
  1217. }
  1218. const storeAreaNum = typeof step2Form.storeArea === "string" ? parseInt(step2Form.storeArea) : step2Form.storeArea;
  1219. const addressObj = {
  1220. address: queryAddress.value || "",
  1221. longitude: parseFloat(step2Form.storePositionLongitude) || 0,
  1222. latitude: parseFloat(step2Form.storePositionLatitude) || 0
  1223. };
  1224. const storePosition =
  1225. step2Form.storePositionLongitude && step2Form.storePositionLatitude
  1226. ? `${step2Form.storePositionLongitude},${step2Form.storePositionLatitude}`
  1227. : "";
  1228. let fullStoreAddress = "";
  1229. if (whereAddress.length > 0) {
  1230. const provinceName = whereAddress[0]?.name || "";
  1231. const cityName = whereAddress[1]?.name || "";
  1232. const districtName = whereAddress[2]?.name || "";
  1233. fullStoreAddress = `${provinceName}${cityName}${districtName}`;
  1234. }
  1235. // 获取身份证正反面URL
  1236. const idCardFrontUrl = getFileUrls(idCardFrontList.value)[0] || "";
  1237. const idCardBackUrl = getFileUrls(idCardBackList.value)[0] || "";
  1238. // 处理经营种类和三级分类
  1239. // 根据用户需求:如果有三级分类,传经营板块、经营种类(二级分类)、三级分类
  1240. // 如果没有三级分类,传经营板块、经营种类(二级分类)
  1241. let finalBusinessTypes: string[] = [];
  1242. let businessClassifyList: string[] = [];
  1243. if (thirdLevelList.value.length > 0) {
  1244. // 有三级分类
  1245. // businessTypes 存储三级分类的 dictId(字符串)
  1246. if (step2Form.businessTypes) {
  1247. finalBusinessTypes = [step2Form.businessTypes];
  1248. businessClassifyList = [step2Form.businessTypes];
  1249. }
  1250. // 如果有三级分类,还需要传递二级分类(经营种类)
  1251. if (Array.isArray(step2Form.businessSecondLevel) && step2Form.businessSecondLevel.length > 0) {
  1252. // 如果 finalBusinessTypes 为空,使用二级分类
  1253. if (finalBusinessTypes.length === 0) {
  1254. finalBusinessTypes = step2Form.businessSecondLevel;
  1255. }
  1256. }
  1257. } else {
  1258. // 没有三级分类,使用二级分类(经营种类)
  1259. if (Array.isArray(step2Form.businessSecondLevel) && step2Form.businessSecondLevel.length > 0) {
  1260. finalBusinessTypes = step2Form.businessSecondLevel;
  1261. } else if (step2Form.businessTypes) {
  1262. // 如果没有选择二级分类,使用 businessTypes(可能是二级分类的 dictId)
  1263. finalBusinessTypes = [step2Form.businessTypes];
  1264. } else if (step2Form.businessTypesList.length > 0) {
  1265. // 使用旧的逻辑
  1266. finalBusinessTypes = step2Form.businessTypesList;
  1267. }
  1268. // 没有三级分类时,businessClassifyList 保持为空数组
  1269. }
  1270. const params = {
  1271. foodLicenceExpirationTime: foodLicenceExpirationTime.value, //食品经营许可证到期时间
  1272. entertainmentLicenceExpirationTime: entertainmentLicenceExpirationTime.value, //娱乐经营许可证到期时间
  1273. businessClassifyList: step2Form.businessSecondLevel, //三级分类
  1274. mealProvided: step2Form.businessSecondMeal, //是否提供餐食
  1275. entertainmentLicenseAddress: disportLicenceUrls, //娱乐经营许可证
  1276. storeTel: userInfo.phone,
  1277. storeName: step2Form.storeName,
  1278. storeCapacity: step2Form.storeCapacity,
  1279. storeArea: storeAreaNum,
  1280. isChain: step2Form.isChain,
  1281. // storeDetailAddress: step2Form.storeDetailAddress,
  1282. storeAddress: step2Form.storeDetailAddress,
  1283. storeBlurb: step2Form.storeBlurb,
  1284. businessSection: step2Form.businessSection,
  1285. businessTypesList: finalBusinessTypes,
  1286. storeStatus: storeStatus,
  1287. businessStatus: step2Form.businessStatus,
  1288. address: addressObj,
  1289. businessLicenseAddress: businessLicenseUrls,
  1290. contractImageList: contractImageUrls,
  1291. foodLicenceImgList: foodLicenceUrls,
  1292. disportLicenceUrls: disportLicenceUrls,
  1293. // storeAddress: fullStoreAddress,
  1294. whereAddress: whereAddress,
  1295. updatedTime: null,
  1296. queryAddress: queryAddress.value,
  1297. storePosition: storePosition,
  1298. storePositionLatitude: parseFloat(step2Form.storePositionLatitude) || 0,
  1299. storePositionLongitude: parseFloat(step2Form.storePositionLongitude) || 0,
  1300. businessSectionName: step2Form.businessSectionName,
  1301. businessTypes: finalBusinessTypes,
  1302. foodLicenceUrl: foodLicenceUrls.length > 0 ? foodLicenceUrls[0] : "",
  1303. userAccount: userInfo.id,
  1304. administrativeRegionProvinceAdcode: step2Form.administrativeRegionProvinceAdcode,
  1305. administrativeRegionCityAdcode: step2Form.administrativeRegionCityAdcode,
  1306. administrativeRegionDistrictAdcode: step2Form.administrativeRegionDistrictAdcode,
  1307. idCardFrontUrl: idCardFrontUrl,
  1308. idCardBackUrl: idCardBackUrl
  1309. };
  1310. ElMessageBox.confirm("确认提交入驻申请吗?", "提示", {
  1311. confirmButtonText: "确定",
  1312. cancelButtonText: "取消",
  1313. type: "warning"
  1314. })
  1315. .then(async () => {
  1316. try {
  1317. const res: any = await applyStore(params);
  1318. if (res && res.code == 200) {
  1319. storeApplicationStatus.value = 0;
  1320. ElMessage.success(res.msg);
  1321. callGetUserInfo();
  1322. setStep(0);
  1323. handleAi();
  1324. } else {
  1325. ElMessage.error(res.msg || "提交失败");
  1326. }
  1327. } catch (error) {
  1328. ElMessage.error("提交失败,请重试");
  1329. }
  1330. })
  1331. .catch(() => {
  1332. // 取消提交
  1333. });
  1334. } else {
  1335. ElMessage.error("请完善表单信息");
  1336. }
  1337. });
  1338. };
  1339. // 文件上传超出限制
  1340. const handleExceed = () => {
  1341. ElMessage.warning("文件数量超出限制");
  1342. };
  1343. </script>
  1344. <style scoped lang="scss">
  1345. // 表单页面样式
  1346. .form-container {
  1347. min-height: calc(100vh - 100px);
  1348. padding: 30px;
  1349. background: #ffffff;
  1350. border-radius: 8px;
  1351. .back-btn {
  1352. margin-bottom: 30px;
  1353. color: #606266;
  1354. border-color: #dcdfe6;
  1355. }
  1356. .progress-container {
  1357. margin-bottom: 40px;
  1358. :deep(.el-step__head.is-process .el-step__icon) {
  1359. color: #909399;
  1360. border-color: #909399 !important;
  1361. }
  1362. :deep(.el-steps) {
  1363. .is-finish {
  1364. .el-step__icon {
  1365. color: #ffffff;
  1366. background-color: #6c8ff8 !important;
  1367. border-color: #6c8ff8 !important;
  1368. }
  1369. }
  1370. .el-step__head {
  1371. .el-step__icon {
  1372. width: 30px;
  1373. height: 30px;
  1374. font-size: 16px;
  1375. font-weight: 600;
  1376. }
  1377. }
  1378. .el-step__title {
  1379. .step-title-wrapper {
  1380. display: flex;
  1381. flex-direction: column;
  1382. gap: 8px;
  1383. align-items: center;
  1384. .step-title {
  1385. font-size: 16px;
  1386. font-weight: 600;
  1387. color: #6c8ff8;
  1388. }
  1389. }
  1390. }
  1391. }
  1392. }
  1393. // 第一步内容样式
  1394. .step1-content {
  1395. .form-content {
  1396. max-width: 800px;
  1397. margin: 0 auto 40px;
  1398. .section-title {
  1399. margin-bottom: 30px;
  1400. font-size: 18px;
  1401. font-weight: 600;
  1402. color: #303133;
  1403. text-align: center;
  1404. }
  1405. .id-card-upload-container {
  1406. display: flex;
  1407. gap: 40px;
  1408. align-items: flex-start;
  1409. justify-content: center;
  1410. :deep(.el-upload-list--picture-card) {
  1411. width: 100%;
  1412. }
  1413. .upload-item {
  1414. flex: 1;
  1415. max-width: 300px;
  1416. .upload-label {
  1417. margin-bottom: 12px;
  1418. font-size: 14px;
  1419. color: #606266;
  1420. text-align: center;
  1421. }
  1422. .id-card-upload {
  1423. width: 100%;
  1424. :deep(.el-upload) {
  1425. position: relative;
  1426. display: flex;
  1427. align-items: center;
  1428. justify-content: center;
  1429. width: 100%;
  1430. height: 200px;
  1431. cursor: pointer;
  1432. background-color: #f5f7fa;
  1433. border: 1px solid #dcdfe6;
  1434. border-radius: 4px;
  1435. transition: all 0.3s;
  1436. &:hover {
  1437. border-color: #6c8ff8;
  1438. }
  1439. }
  1440. // 当上传完成后隐藏上传按钮
  1441. &.upload-complete {
  1442. :deep(.el-upload) {
  1443. display: none !important;
  1444. }
  1445. }
  1446. :deep(.el-upload-list) {
  1447. .el-upload-list__item {
  1448. width: 100%;
  1449. height: 200px;
  1450. margin: 0;
  1451. }
  1452. }
  1453. .upload-placeholder {
  1454. display: flex;
  1455. align-items: center;
  1456. justify-content: center;
  1457. width: 100%;
  1458. height: 100%;
  1459. .placeholder-text {
  1460. font-size: 14px;
  1461. color: #909399;
  1462. }
  1463. }
  1464. }
  1465. }
  1466. }
  1467. // OCR 识别结果展示样式
  1468. .ocr-result-container {
  1469. width: 670px;
  1470. padding: 20px;
  1471. margin: 60px auto;
  1472. .ocr-result-item {
  1473. display: flex;
  1474. align-items: center;
  1475. margin-bottom: 16px;
  1476. font-size: 16px;
  1477. line-height: 1.5;
  1478. &:last-child {
  1479. margin-bottom: 0;
  1480. }
  1481. .label {
  1482. min-width: 100px;
  1483. font-weight: 600;
  1484. color: #606266;
  1485. }
  1486. .value {
  1487. flex: 1;
  1488. color: #303133;
  1489. word-break: break-all;
  1490. }
  1491. }
  1492. .ocr-result-tip {
  1493. padding: 10px 0;
  1494. font-size: 14px;
  1495. color: #909399;
  1496. text-align: center;
  1497. }
  1498. }
  1499. }
  1500. }
  1501. .form-content {
  1502. max-width: 800px;
  1503. margin: 0 auto;
  1504. &.step2-form {
  1505. max-width: 100%;
  1506. .form-row {
  1507. display: flex;
  1508. gap: 40px;
  1509. .form-col {
  1510. flex: 1;
  1511. }
  1512. }
  1513. }
  1514. }
  1515. .form-actions {
  1516. display: flex;
  1517. gap: 20px;
  1518. justify-content: center;
  1519. padding-top: 30px;
  1520. margin-top: 40px;
  1521. border-top: 1px solid #e4e7ed;
  1522. .el-button {
  1523. width: 200px;
  1524. height: 44px;
  1525. font-size: 16px;
  1526. font-weight: 500;
  1527. color: #ffffff;
  1528. background: #6c8ff8;
  1529. border: none;
  1530. border-radius: 4px;
  1531. outline: none;
  1532. }
  1533. }
  1534. }
  1535. </style>