go-flow.vue 54 KB

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