go-flow.vue 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252
  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="handleUploadSuccess"
  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="handleUploadSuccess"
  57. :on-preview="handlePictureCardPreview"
  58. :on-remove="handleRemove"
  59. accept="image/*"
  60. class="id-card-upload"
  61. :class="{ 'upload-complete': isIdCardUploadComplete }"
  62. >
  63. <template v-if="idCardBackList.length === 0 && !isIdCardUploadComplete">
  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="businessSecondMeal">
  129. <el-radio-group v-model="step2Form.businessSecondMeal">
  130. <el-radio v-for="item in secondMealList" :value="item.value" :key="item.key">
  131. {{ item.dictDetail }}
  132. </el-radio>
  133. </el-radio-group>
  134. </el-form-item>
  135. <el-form-item label="经营板块" prop="businessSection">
  136. <el-radio-group v-model="step2Form.businessSection" @change="changeBusinessSector">
  137. <el-radio v-for="businessSection in businessSectionList" :value="businessSection.id" :key="businessSection.id">
  138. {{ businessSection.dictDetail }}
  139. </el-radio>
  140. </el-radio-group>
  141. </el-form-item>
  142. <el-form-item label="经营种类" prop="businessSecondLevel" v-if="secondLevelList.length > 0">
  143. <el-radio-group v-model="step2Form.businessSecondLevel" @change="changeBusinessSecondLevel">
  144. <el-radio v-for="item in secondLevelList" :value="item.dictId" :key="item.dictId">
  145. {{ item.dictDetail }}
  146. </el-radio>
  147. </el-radio-group>
  148. </el-form-item>
  149. <!-- 如果没有三级分类,则显示二级分类作为经营种类 -->
  150. <el-form-item label="分类" prop="businessTypes" v-if="secondLevelList.length > 0 && thirdLevelList.length === 0">
  151. <el-checkbox-group v-model="step2Form.businessTypes">
  152. <el-checkbox v-for="item in secondLevelList" :key="item.dictId" :label="item.dictDetail" :value="item.dictId" />
  153. </el-checkbox-group>
  154. </el-form-item>
  155. </div>
  156. <!-- 右列 -->
  157. <div class="form-col">
  158. <el-form-item label="门店营业状态" prop="businessType">
  159. <el-radio-group v-model="step2Form.businessType">
  160. <el-radio label="正常营业"> 正常营业 </el-radio>
  161. <el-radio label="暂停营业"> 暂停营业 </el-radio>
  162. <el-radio label="筹建中"> 筹建中 </el-radio>
  163. </el-radio-group>
  164. </el-form-item>
  165. <el-form-item label="经度" prop="storePositionLongitude" v-show="latShow">
  166. <el-input disabled v-model="step2Form.storePositionLongitude" placeholder="请填写经度" clearable />
  167. </el-form-item>
  168. <el-form-item label="纬度" prop="storePositionLatitude" v-show="latShow">
  169. <el-input disabled v-model="step2Form.storePositionLatitude" placeholder="请填写纬度" clearable />
  170. </el-form-item>
  171. <el-form-item label="经纬度查询" prop="address">
  172. <el-select
  173. v-model="step2Form.address"
  174. filterable
  175. placeholder="请输入地址进行查询"
  176. remote
  177. reserve-keyword
  178. :remote-method="getLonAndLat"
  179. @change="selectAddress"
  180. >
  181. <el-option v-for="item in addressList" :key="item.id" :label="item.name" :value="item.location">
  182. <span style="float: left">{{ item.name }}</span>
  183. <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{ item.district }}</span>
  184. </el-option>
  185. </el-select>
  186. </el-form-item>
  187. <el-form-item label="营业执照" prop="businessLicenseAddress">
  188. <el-upload
  189. v-model:file-list="step2Form.businessLicenseAddress"
  190. :http-request="handleHttpUpload"
  191. list-type="picture-card"
  192. :limit="1"
  193. :on-exceed="handleExceed"
  194. :on-success="handleUploadSuccess"
  195. :on-preview="handlePictureCardPreview"
  196. >
  197. <el-icon><Plus /></el-icon>
  198. <template #tip>
  199. <div class="el-upload__tip">({{ step2Form.businessLicenseAddress.length }}/1)</div>
  200. </template>
  201. </el-upload>
  202. </el-form-item>
  203. <el-form-item label="合同图片" prop="contractImageList">
  204. <el-upload
  205. v-model:file-list="step2Form.contractImageList"
  206. :http-request="handleHttpUpload"
  207. list-type="picture-card"
  208. :limit="20"
  209. :on-exceed="handleExceed"
  210. :on-success="handleUploadSuccess"
  211. :on-preview="handlePictureCardPreview"
  212. >
  213. <el-icon><Plus /></el-icon>
  214. <template #tip>
  215. <div class="el-upload__tip">({{ step2Form.contractImageList.length }}/20)</div>
  216. </template>
  217. </el-upload>
  218. </el-form-item>
  219. <el-form-item label="食品经营许可证" prop="foodLicenceImgList">
  220. <el-upload
  221. v-model:file-list="step2Form.foodLicenceImgList"
  222. :http-request="handleHttpUpload"
  223. list-type="picture-card"
  224. :limit="1"
  225. :on-exceed="handleExceed"
  226. :on-success="handleUploadSuccess"
  227. :on-preview="handlePictureCardPreview"
  228. >
  229. <el-icon><Plus /></el-icon>
  230. <template #tip>
  231. <div class="el-upload__tip">({{ step2Form.foodLicenceImgList.length }}/1)</div>
  232. </template>
  233. </el-upload>
  234. </el-form-item>
  235. </div>
  236. </div>
  237. </el-form>
  238. </div>
  239. <!-- 按钮 -->
  240. <div class="form-actions">
  241. <el-button type="primary" size="large" @click="handleSubmit"> 提交 </el-button>
  242. </div>
  243. </div>
  244. </div>
  245. <!-- 图片预览 -->
  246. <el-image-viewer
  247. v-if="imageViewerVisible"
  248. :url-list="imageViewerUrlList"
  249. :initial-index="imageViewerInitialIndex"
  250. @close="imageViewerVisible = false"
  251. />
  252. </template>
  253. <script setup lang="ts">
  254. import { ref, reactive, watch, onMounted, computed } from "vue";
  255. import {
  256. ElMessage,
  257. ElMessageBox,
  258. type FormInstance,
  259. type FormRules,
  260. UploadProps,
  261. UploadUserFile,
  262. UploadRequestOptions
  263. } from "element-plus";
  264. import { Plus } from "@element-plus/icons-vue";
  265. import {
  266. applyStore,
  267. getMerchantByPhone,
  268. getFirstLevelList,
  269. getSecondLevelList,
  270. getThirdLevelList
  271. } from "@/api/modules/homeEntry";
  272. import { getInputPrompt, getDistrict, uploadImg, ocrRequestUrl } from "@/api/modules/newLoginApi";
  273. import { localGet, localSet } from "@/utils/index";
  274. import { useAuthStore } from "@/stores/modules/auth";
  275. const authStore = useAuthStore();
  276. const userInfo = localGet("geeker-user")?.userInfo || {};
  277. const latShow = ref(false);
  278. // 图片预览相关
  279. const imageViewerVisible = ref(false);
  280. const imageViewerUrlList = ref<string[]>([]);
  281. const imageViewerInitialIndex = ref(0);
  282. const entryList = ref([
  283. {
  284. title: "个人实名"
  285. },
  286. {
  287. title: "填写信息"
  288. },
  289. {
  290. title: "等待审核"
  291. },
  292. {
  293. title: "入驻成功"
  294. }
  295. ]);
  296. // 身份证正反面上传列表
  297. const idCardFrontList = ref<UploadUserFile[]>([]);
  298. const idCardBackList = ref<UploadUserFile[]>([]);
  299. // OCR 识别结果
  300. const ocrResult = ref<{
  301. name?: string;
  302. idCard?: string;
  303. }>({});
  304. // 是否正在识别中
  305. const isOcrProcessing = ref(false);
  306. // 计算是否已上传完成(正反面都上传完成)
  307. const isIdCardUploadComplete = computed(() => {
  308. return idCardFrontList.value.length > 0 && idCardBackList.value.length > 0;
  309. });
  310. // 下一步 - 验证身份证正反面是否已上传
  311. const handleNextStep = async () => {
  312. // 识别成功,进入下一步
  313. setStep(2);
  314. };
  315. const secondMealList = ref([
  316. { key: 0, value: 0, dictDetail: "提供" },
  317. { key: 1, value: 1, dictDetail: "不提供" }
  318. ]);
  319. const step2Rules: FormRules = {
  320. storeName: [{ required: true, message: "请输入店铺名称", trigger: "blur" }],
  321. storeCapacity: [{ required: true, message: "请输入容纳人数", trigger: "blur" }],
  322. storeArea: [{ required: true, message: "请选择门店面积", trigger: "change" }],
  323. storeBlurb: [{ required: true, message: "请输入门店简介", trigger: "change" }],
  324. storeIntro: [{ required: true, message: "请输入门店简介", trigger: "blur" }],
  325. businessSecondMeal: [{ required: true, message: "请选择是否提供餐食", trigger: "change" }],
  326. businessSection: [{ required: true, message: "请选择经营板块", trigger: "change" }],
  327. businessTypes: [
  328. {
  329. required: true,
  330. message: "请选择经营种类",
  331. trigger: "change",
  332. validator: (rule: any, value: any, callback: any) => {
  333. if (!value || value.length === 0) {
  334. callback(new Error("请选择经营种类"));
  335. } else {
  336. callback();
  337. }
  338. }
  339. }
  340. ],
  341. address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
  342. businessLicenseAddress: [{ required: true, message: "请上传营业执照", trigger: "change" }],
  343. contractImageList: [{ required: true, message: "请上传合同图片", trigger: "change" }],
  344. foodLicenceImgList: [{ required: true, message: "请上传食品经营许可证", trigger: "change" }]
  345. };
  346. //地址集合
  347. const addressList = ref<any[]>([]);
  348. //查询地址名称
  349. const queryAddress = ref<string>("");
  350. const props = defineProps({
  351. currentStep: {
  352. type: Number,
  353. default: 1
  354. },
  355. storeApplicationStatus: {
  356. type: Number,
  357. default: 0
  358. }
  359. });
  360. const emit = defineEmits(["update:currentStep", "update:get-user-info"]);
  361. // 调用父组件的 getUserInfo 方法
  362. const callGetUserInfo = () => {
  363. emit("update:get-user-info");
  364. };
  365. // 内部步骤状态,和父组件同步
  366. const currentStep = ref<number>(props.currentStep || 1);
  367. const storeApplicationStatus = ref<number>(props.storeApplicationStatus || 0);
  368. watch(
  369. () => props.currentStep,
  370. val => {
  371. if (typeof val === "number") currentStep.value = val;
  372. }
  373. );
  374. watch(
  375. () => props.storeApplicationStatus,
  376. val => {
  377. if (typeof val === "number") storeApplicationStatus.value = val;
  378. }
  379. );
  380. // 隐藏财务管理菜单的函数
  381. const hideFinancialManagementMenu = () => {
  382. const hideMenus = (menuList: any[]) => {
  383. menuList.forEach(menu => {
  384. if (menu.name && menu.name === "financialManagement") {
  385. menu.meta.isHide = true;
  386. }
  387. if (menu.children && menu.children.length > 0) {
  388. hideMenus(menu.children);
  389. }
  390. });
  391. };
  392. if (authStore.authMenuList && authStore.authMenuList.length > 0) {
  393. hideMenus(authStore.authMenuList);
  394. }
  395. };
  396. // 显示财务管理菜单的函数
  397. const showFinancialManagementMenu = () => {
  398. const showMenus = (menuList: any[]) => {
  399. menuList.forEach(menu => {
  400. if (menu.name && menu.name === "financialManagement") {
  401. menu.meta.isHide = false;
  402. }
  403. if (menu.children && menu.children.length > 0) {
  404. showMenus(menu.children);
  405. }
  406. });
  407. };
  408. if (authStore.authMenuList && authStore.authMenuList.length > 0) {
  409. showMenus(authStore.authMenuList);
  410. }
  411. };
  412. // 更新缓存中的 storeId
  413. const updateStoreIdInCache = async () => {
  414. try {
  415. const geekerUser = localGet("geeker-user");
  416. if (!geekerUser || !geekerUser.userInfo || !geekerUser.userInfo.phone) {
  417. console.error("用户信息不存在");
  418. return;
  419. }
  420. const phone = geekerUser.userInfo.phone;
  421. const res: any = await getMerchantByPhone({ phone });
  422. if (res && res.code == 200 && res.data && res.data.storeId) {
  423. geekerUser.userInfo.storeId = res.data.storeId;
  424. localSet("geeker-user", geekerUser);
  425. if (res.data.storeId) {
  426. localSet("createdId", res.data.storeId);
  427. }
  428. }
  429. } catch (error) {
  430. console.error("更新 storeId 缓存失败:", error);
  431. }
  432. };
  433. // 监听步骤和审核状态
  434. watch([() => currentStep.value, () => storeApplicationStatus.value], ([step, status]) => {
  435. if (step === 3 && (status === 0 || status === 2)) {
  436. updateStoreIdInCache();
  437. }
  438. if (status === 2) {
  439. hideFinancialManagementMenu();
  440. }
  441. if (status === 1) {
  442. showFinancialManagementMenu();
  443. }
  444. });
  445. // 监听菜单列表变化
  446. watch(
  447. () => authStore.authMenuList.length,
  448. newLength => {
  449. if (newLength > 0) {
  450. if (storeApplicationStatus.value === 2) {
  451. hideFinancialManagementMenu();
  452. }
  453. if (storeApplicationStatus.value === 1) {
  454. showFinancialManagementMenu();
  455. }
  456. }
  457. }
  458. );
  459. onMounted(() => {
  460. getBusinessSectionList();
  461. callGetUserInfo();
  462. if (currentStep.value === 3 && (storeApplicationStatus.value === 0 || storeApplicationStatus.value === 2)) {
  463. updateStoreIdInCache();
  464. }
  465. if (storeApplicationStatus.value === 2) {
  466. hideFinancialManagementMenu();
  467. } else if (storeApplicationStatus.value === 1) {
  468. showFinancialManagementMenu();
  469. }
  470. });
  471. const setStep = (val: number) => {
  472. currentStep.value = val;
  473. emit("update:currentStep", val);
  474. };
  475. // 第二步表单
  476. const step2FormRef = ref<FormInstance>();
  477. const step2Form = reactive({
  478. businessSecondMeal: 0,
  479. storeName: "",
  480. storeCapacity: 1,
  481. storeArea: "1",
  482. isChain: 0,
  483. storeDetailAddress: "",
  484. region: [],
  485. administrativeRegionProvinceAdcode: "",
  486. administrativeRegionCityAdcode: "",
  487. administrativeRegionDistrictAdcode: "",
  488. storeAddress: "",
  489. storeBlurb: "",
  490. businessSection: "",
  491. businessSectionName: "",
  492. businessSecondLevel: "",
  493. businessTypes: [],
  494. businessTypesList: [],
  495. businessStatus: 0,
  496. storeStatus: 1,
  497. businessType: "正常营业",
  498. storePositionLongitude: "",
  499. storePositionLatitude: "",
  500. businessLicenseAddress: [] as UploadUserFile[],
  501. contractImageList: [] as UploadUserFile[],
  502. foodLicenceImgList: [] as UploadUserFile[],
  503. address: ""
  504. });
  505. // 返回按钮
  506. const handleBack = () => {
  507. if (currentStep.value === 1) {
  508. setStep(0);
  509. } else if (currentStep.value === 2) {
  510. setStep(1);
  511. } else if (currentStep.value === 3) {
  512. setStep(2);
  513. }
  514. };
  515. // 地区选择
  516. const areaProps: any = {
  517. lazy: true,
  518. async lazyLoad(node, resolve) {
  519. const { level } = node;
  520. try {
  521. let param = { adCode: node.data.adCode ? node.data.adCode : "" };
  522. const response: any = await getDistrict(param as any);
  523. const nodes = (response?.data?.districts?.[0]?.districts || []).map((item: any) => ({
  524. value: item.adcode,
  525. adCode: item.adcode,
  526. label: item.name,
  527. leaf: level >= 2
  528. }));
  529. resolve(nodes);
  530. } catch (error) {
  531. resolve([]);
  532. }
  533. }
  534. };
  535. watch(
  536. () => step2Form.region,
  537. (newVal: any[]) => {
  538. if (newVal.length > 0) {
  539. step2Form.administrativeRegionProvinceAdcode = newVal[0];
  540. step2Form.administrativeRegionCityAdcode = newVal[1];
  541. step2Form.administrativeRegionDistrictAdcode = newVal[2];
  542. }
  543. }
  544. );
  545. //经营板块 - 一级分类
  546. const businessSectionList = ref<any[]>([]);
  547. const getBusinessSectionList = async () => {
  548. try {
  549. const res: any = await getFirstLevelList({});
  550. if (res && res.code === 200 && res.data) {
  551. businessSectionList.value = res.data;
  552. }
  553. } catch (error) {
  554. console.error("获取一级分类失败:", error);
  555. ElMessage.error("获取经营板块失败");
  556. }
  557. };
  558. // 二级分类列表
  559. const secondLevelList = ref<any[]>([]);
  560. // 三级分类列表
  561. const thirdLevelList = ref<any[]>([]);
  562. // 一级分类变化时,加载二级分类
  563. const changeBusinessSector = async (dictId: string | number | boolean | undefined) => {
  564. const dictIdStr = String(dictId || "");
  565. if (!dictIdStr) {
  566. secondLevelList.value = [];
  567. thirdLevelList.value = [];
  568. step2Form.businessSecondLevel = "";
  569. step2Form.businessTypes = [];
  570. step2Form.businessTypesList = [];
  571. return;
  572. }
  573. // 更新一级分类信息
  574. const selectedSection = businessSectionList.value.find((item: any) => item.dictId === dictIdStr);
  575. if (selectedSection) {
  576. step2Form.businessSection = selectedSection.dictId;
  577. step2Form.businessSectionName = selectedSection.dictDetail;
  578. }
  579. // 清空二级和三级分类
  580. secondLevelList.value = [];
  581. thirdLevelList.value = [];
  582. step2Form.businessSecondLevel = "";
  583. step2Form.businessTypes = [];
  584. step2Form.businessTypesList = [];
  585. // 加载二级分类
  586. try {
  587. const res: any = await getSecondLevelList({ parentDictId: dictIdStr });
  588. if (res && res.code === 200 && res.data) {
  589. secondLevelList.value = res.data;
  590. }
  591. } catch (error) {
  592. console.error("获取二级分类失败:", error);
  593. ElMessage.error("获取经营种类失败");
  594. }
  595. };
  596. // 二级分类变化时,加载三级分类
  597. const changeBusinessSecondLevel = async (dictId: string | number | boolean | undefined) => {
  598. const dictIdStr = String(dictId || "");
  599. if (!dictIdStr) {
  600. thirdLevelList.value = [];
  601. step2Form.businessTypes = [];
  602. step2Form.businessTypesList = [];
  603. return;
  604. }
  605. // 清空三级分类
  606. thirdLevelList.value = [];
  607. step2Form.businessTypes = [];
  608. step2Form.businessTypesList = [];
  609. // 加载三级分类
  610. try {
  611. const res: any = await getThirdLevelList({ parentDictId: dictIdStr });
  612. if (res && res.code === 200 && res.data) {
  613. thirdLevelList.value = res.data;
  614. }
  615. } catch (error) {
  616. console.error("获取三级分类失败:", error);
  617. // 如果没有三级分类,不显示错误,因为可能该二级分类下没有三级分类
  618. }
  619. };
  620. // 经纬度查询
  621. const getLonAndLat = async (keyword: string) => {
  622. if (keyword) {
  623. let param = {
  624. addressName: keyword
  625. };
  626. let res: any = await getInputPrompt(param as any);
  627. if (res.code == "200") {
  628. addressList.value = res?.data?.tips || [];
  629. } else {
  630. ElMessage.error("查询失败!");
  631. }
  632. } else {
  633. addressList.value = [];
  634. }
  635. };
  636. const selectAddress = async (param: any) => {
  637. if (!step2Form.address || typeof step2Form.address !== "string") {
  638. ElMessage.warning("地址格式不正确,请重新选择");
  639. return;
  640. }
  641. if (!step2Form.address.includes(",")) {
  642. ElMessage.warning("地址格式不正确,缺少经纬度信息");
  643. return;
  644. }
  645. let locationList = step2Form.address.split(",");
  646. if (locationList.length < 2) {
  647. ElMessage.warning("地址格式不正确,无法获取经纬度");
  648. return;
  649. }
  650. addressList.value.forEach((item: any) => {
  651. if (item.location == step2Form.address) {
  652. queryAddress.value = item.name;
  653. }
  654. });
  655. step2Form.storePositionLongitude = locationList[0]?.trim() || "";
  656. step2Form.storePositionLatitude = locationList[1]?.trim() || "";
  657. if (!step2Form.storePositionLongitude || !step2Form.storePositionLatitude) {
  658. ElMessage.warning("无法获取有效的经纬度信息");
  659. return;
  660. }
  661. latShow.value = true;
  662. };
  663. //文件上传
  664. const handleHttpUpload = async (options: UploadRequestOptions) => {
  665. let formData = new FormData();
  666. formData.append("file", options.file);
  667. try {
  668. const res: any = await uploadImg(formData);
  669. const fileUrl = res?.data?.fileUrl || res?.data?.[0] || res?.fileUrl;
  670. if (fileUrl) {
  671. options.onSuccess({ fileUrl });
  672. } else {
  673. throw new Error("上传失败:未获取到文件URL");
  674. }
  675. } catch (error) {
  676. options.onError(error as any);
  677. ElMessage.error("文件上传失败,请重试");
  678. }
  679. };
  680. // 自动调用 OCR 识别
  681. const autoOcrRecognition = async () => {
  682. // 检查正反面是否都已上传完成
  683. if (idCardFrontList.value.length === 0 || idCardBackList.value.length === 0) {
  684. return;
  685. }
  686. const frontFile = idCardFrontList.value[0];
  687. const backFile = idCardBackList.value[0];
  688. // 验证上传的文件是否成功
  689. if (frontFile.status !== "success" || !frontFile.url || backFile.status !== "success" || !backFile.url) {
  690. return;
  691. }
  692. // 获取身份证正反面的 URL
  693. const frontUrl = getFileUrls(idCardFrontList.value)[0] || "";
  694. const backUrl = getFileUrls(idCardBackList.value)[0] || "";
  695. if (!frontUrl || !backUrl) {
  696. return;
  697. }
  698. // 如果正在识别中,不重复调用
  699. if (isOcrProcessing.value) {
  700. return;
  701. }
  702. // 将正反面 URL 用逗号分隔
  703. const imageUrls = `${frontUrl},${backUrl}`;
  704. let params = {
  705. imageUrls: imageUrls,
  706. ocrType: "ID_CARD",
  707. storeId: userInfo.storeId,
  708. storeUserId: userInfo.id
  709. };
  710. try {
  711. isOcrProcessing.value = true;
  712. const res: any = await ocrRequestUrl(params);
  713. if (res && (res.code === 200 || res.code === "200")) {
  714. // 保存识别结果
  715. if (res.data) {
  716. console.log(res.data[0]);
  717. ocrResult.value = {
  718. name: res.data.name || res.data.realName || "",
  719. idCard: res.data.idCard || res.data.idNumber || res.data.idNo || ""
  720. };
  721. // 更新本地存储中的用户信息
  722. const geekerUser = localGet("geeker-user");
  723. if (geekerUser && geekerUser.userInfo) {
  724. if (ocrResult.value.name) {
  725. geekerUser.userInfo.name = ocrResult.value.name;
  726. }
  727. if (ocrResult.value.idCard) {
  728. geekerUser.userInfo.idCard = ocrResult.value.idCard;
  729. }
  730. localSet("geeker-user", geekerUser);
  731. }
  732. ElMessage.success("身份证识别成功");
  733. }
  734. } else {
  735. console.warn("OCR 识别失败:", res?.msg);
  736. }
  737. } catch (error) {
  738. console.error("身份证识别失败:", error);
  739. } finally {
  740. isOcrProcessing.value = false;
  741. }
  742. };
  743. // 文件上传成功回调
  744. const handleUploadSuccess = (response: any, uploadFile: UploadUserFile) => {
  745. if (response?.fileUrl) {
  746. uploadFile.url = response.fileUrl;
  747. }
  748. // 延迟一下,确保文件状态已更新,然后检查是否需要自动 OCR
  749. setTimeout(() => {
  750. autoOcrRecognition();
  751. }, 100);
  752. };
  753. // 图片预览处理函数
  754. const handlePictureCardPreview = (file: UploadUserFile) => {
  755. if (file.status === "uploading" && file.url) {
  756. imageViewerUrlList.value = [file.url];
  757. imageViewerInitialIndex.value = 0;
  758. imageViewerVisible.value = true;
  759. return;
  760. }
  761. let urlList: string[] = [];
  762. let currentFileList: UploadUserFile[] = [];
  763. // 判断是哪个上传组件的文件
  764. if (idCardFrontList.value.some((f: UploadUserFile) => f.uid === file.uid)) {
  765. currentFileList = idCardFrontList.value;
  766. } else if (idCardBackList.value.some((f: UploadUserFile) => f.uid === file.uid)) {
  767. currentFileList = idCardBackList.value;
  768. } else if (step2Form.businessLicenseAddress.some((f: UploadUserFile) => f.uid === file.uid)) {
  769. currentFileList = step2Form.businessLicenseAddress;
  770. } else if (step2Form.contractImageList.some((f: UploadUserFile) => f.uid === file.uid)) {
  771. currentFileList = step2Form.contractImageList;
  772. } else if (step2Form.foodLicenceImgList.some((f: UploadUserFile) => f.uid === file.uid)) {
  773. currentFileList = step2Form.foodLicenceImgList;
  774. }
  775. urlList = currentFileList
  776. .filter((item: UploadUserFile) => item.status === "success" && (item.url || (item.response as any)?.fileUrl))
  777. .map((item: UploadUserFile) => item.url || (item.response as any)?.fileUrl);
  778. const currentUrl = file.url || (file.response as any)?.fileUrl;
  779. const currentIndex = urlList.findIndex((url: string) => url === currentUrl);
  780. if (currentIndex < 0) {
  781. ElMessage.warning("图片尚未上传完成,无法预览");
  782. return;
  783. }
  784. imageViewerUrlList.value = urlList;
  785. imageViewerInitialIndex.value = currentIndex;
  786. imageViewerVisible.value = true;
  787. };
  788. // 文件移除处理
  789. const handleRemove = (file: UploadUserFile) => {
  790. // 文件移除时,清空 OCR 识别结果
  791. ocrResult.value = {};
  792. isOcrProcessing.value = false;
  793. };
  794. // 提取文件列表中的URL
  795. const getFileUrls = (fileList: UploadUserFile[]): string[] => {
  796. return fileList
  797. .map((file: UploadUserFile) => {
  798. const response = file.response as any;
  799. return file.url || response?.fileUrl || "";
  800. })
  801. .filter((url: string) => url);
  802. };
  803. // 根据adcode获取地区详细信息
  804. const getDistrictInfo = async (adcode: string) => {
  805. try {
  806. const response: any = await getDistrict({ adCode: adcode } as any);
  807. const district = response?.data?.districts?.[0];
  808. if (district) {
  809. return {
  810. citycode: district.citycode ? [district.citycode] : [],
  811. adcode: district.adcode,
  812. level: district.level,
  813. center: district.center,
  814. name: district.name,
  815. districts: []
  816. };
  817. }
  818. } catch (error) {
  819. console.error("获取地区信息失败:", error);
  820. }
  821. return null;
  822. };
  823. // 构建whereAddress数组
  824. const buildWhereAddress = async (regionCodes: string[]) => {
  825. const whereAddress: any[] = [];
  826. if (regionCodes && regionCodes.length > 0) {
  827. for (const code of regionCodes) {
  828. const districtInfo = await getDistrictInfo(code);
  829. if (districtInfo) {
  830. whereAddress.push(districtInfo);
  831. }
  832. }
  833. }
  834. return whereAddress;
  835. };
  836. // 提交
  837. const handleSubmit = async () => {
  838. if (!step2FormRef.value) return;
  839. await step2FormRef.value.validate(async valid => {
  840. if (valid) {
  841. const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
  842. const contractImageUrls = getFileUrls(step2Form.contractImageList);
  843. const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList);
  844. const whereAddress = await buildWhereAddress(step2Form.region);
  845. let storeStatus = 1;
  846. if (step2Form.businessType === "正常营业") {
  847. storeStatus = 1;
  848. } else if (step2Form.businessType === "暂停营业") {
  849. storeStatus = 0;
  850. } else if (step2Form.businessType === "筹建中") {
  851. storeStatus = 2;
  852. }
  853. const storeAreaNum = typeof step2Form.storeArea === "string" ? parseInt(step2Form.storeArea) : step2Form.storeArea;
  854. const addressObj = {
  855. address: queryAddress.value || "",
  856. longitude: parseFloat(step2Form.storePositionLongitude) || 0,
  857. latitude: parseFloat(step2Form.storePositionLatitude) || 0
  858. };
  859. const storePosition =
  860. step2Form.storePositionLongitude && step2Form.storePositionLatitude
  861. ? `${step2Form.storePositionLongitude},${step2Form.storePositionLatitude}`
  862. : "";
  863. let fullStoreAddress = "";
  864. if (whereAddress.length > 0) {
  865. const provinceName = whereAddress[0]?.name || "";
  866. const cityName = whereAddress[1]?.name || "";
  867. const districtName = whereAddress[2]?.name || "";
  868. fullStoreAddress = `${provinceName}${cityName}${districtName}`;
  869. }
  870. // 获取身份证正反面URL
  871. const idCardFrontUrl = getFileUrls(idCardFrontList.value)[0] || "";
  872. const idCardBackUrl = getFileUrls(idCardBackList.value)[0] || "";
  873. // 处理经营种类:优先使用三级分类,如果没有三级分类则使用二级分类
  874. let finalBusinessTypes: string[] = [];
  875. if (step2Form.businessTypes.length > 0) {
  876. // 有三级分类选择
  877. finalBusinessTypes = step2Form.businessTypes;
  878. } else if (step2Form.businessSecondLevel) {
  879. // 没有三级分类,使用二级分类
  880. finalBusinessTypes = [step2Form.businessSecondLevel];
  881. } else {
  882. // 都没有,使用旧的逻辑
  883. finalBusinessTypes = step2Form.businessTypesList;
  884. }
  885. const params = {
  886. storeTel: userInfo.phone,
  887. storeName: step2Form.storeName,
  888. storeCapacity: step2Form.storeCapacity,
  889. storeArea: storeAreaNum,
  890. isChain: step2Form.isChain,
  891. storeDetailAddress: step2Form.storeDetailAddress,
  892. storeBlurb: step2Form.storeBlurb,
  893. businessSection: step2Form.businessSection,
  894. businessTypesList: finalBusinessTypes,
  895. storeStatus: storeStatus,
  896. businessStatus: step2Form.businessStatus,
  897. address: addressObj,
  898. businessLicenseAddress: businessLicenseUrls,
  899. contractImageList: contractImageUrls,
  900. foodLicenceImgList: foodLicenceUrls,
  901. disportLicenceUrls: disportLicenceUrls,
  902. storeAddress: fullStoreAddress,
  903. whereAddress: whereAddress,
  904. updatedTime: null,
  905. queryAddress: queryAddress.value,
  906. storePosition: storePosition,
  907. storePositionLatitude: parseFloat(step2Form.storePositionLatitude) || 0,
  908. storePositionLongitude: parseFloat(step2Form.storePositionLongitude) || 0,
  909. businessSectionName: step2Form.businessSectionName,
  910. businessTypes: finalBusinessTypes,
  911. foodLicenceUrl: foodLicenceUrls.length > 0 ? foodLicenceUrls[0] : "",
  912. userAccount: userInfo.id,
  913. administrativeRegionProvinceAdcode: step2Form.administrativeRegionProvinceAdcode,
  914. administrativeRegionCityAdcode: step2Form.administrativeRegionCityAdcode,
  915. administrativeRegionDistrictAdcode: step2Form.administrativeRegionDistrictAdcode,
  916. idCardFrontUrl: idCardFrontUrl,
  917. idCardBackUrl: idCardBackUrl
  918. };
  919. ElMessageBox.confirm("确认提交入驻申请吗?", "提示", {
  920. confirmButtonText: "确定",
  921. cancelButtonText: "取消",
  922. type: "warning"
  923. })
  924. .then(async () => {
  925. try {
  926. const res: any = await applyStore(params);
  927. if (res && res.code == 200) {
  928. storeApplicationStatus.value = 0;
  929. ElMessage.success(res.msg);
  930. callGetUserInfo();
  931. setStep(0);
  932. } else {
  933. ElMessage.error(res.msg || "提交失败");
  934. }
  935. } catch (error) {
  936. ElMessage.error("提交失败,请重试");
  937. }
  938. })
  939. .catch(() => {
  940. // 取消提交
  941. });
  942. } else {
  943. ElMessage.error("请完善表单信息");
  944. }
  945. });
  946. };
  947. // 文件上传超出限制
  948. const handleExceed = () => {
  949. ElMessage.warning("文件数量超出限制");
  950. };
  951. </script>
  952. <style scoped lang="scss">
  953. // 表单页面样式
  954. .form-container {
  955. min-height: calc(100vh - 100px);
  956. padding: 30px;
  957. background: #ffffff;
  958. border-radius: 8px;
  959. .back-btn {
  960. margin-bottom: 30px;
  961. color: #606266;
  962. border-color: #dcdfe6;
  963. }
  964. .progress-container {
  965. margin-bottom: 40px;
  966. :deep(.el-step__head.is-process .el-step__icon) {
  967. color: #909399;
  968. border-color: #909399 !important;
  969. }
  970. :deep(.el-steps) {
  971. .is-finish {
  972. .el-step__icon {
  973. color: #ffffff;
  974. background-color: #6c8ff8 !important;
  975. border-color: #6c8ff8 !important;
  976. }
  977. }
  978. .el-step__head {
  979. .el-step__icon {
  980. width: 30px;
  981. height: 30px;
  982. font-size: 16px;
  983. font-weight: 600;
  984. }
  985. }
  986. .el-step__title {
  987. .step-title-wrapper {
  988. display: flex;
  989. flex-direction: column;
  990. gap: 8px;
  991. align-items: center;
  992. .step-title {
  993. font-size: 16px;
  994. font-weight: 600;
  995. color: #6c8ff8;
  996. }
  997. }
  998. }
  999. }
  1000. }
  1001. // 第一步内容样式
  1002. .step1-content {
  1003. .form-content {
  1004. max-width: 800px;
  1005. margin: 0 auto 40px;
  1006. .section-title {
  1007. margin-bottom: 30px;
  1008. font-size: 18px;
  1009. font-weight: 600;
  1010. color: #303133;
  1011. text-align: center;
  1012. }
  1013. .id-card-upload-container {
  1014. display: flex;
  1015. gap: 40px;
  1016. align-items: flex-start;
  1017. justify-content: center;
  1018. :deep(.el-upload-list--picture-card) {
  1019. width: 100%;
  1020. }
  1021. .upload-item {
  1022. flex: 1;
  1023. max-width: 300px;
  1024. .upload-label {
  1025. margin-bottom: 12px;
  1026. font-size: 14px;
  1027. color: #606266;
  1028. text-align: center;
  1029. }
  1030. .id-card-upload {
  1031. width: 100%;
  1032. :deep(.el-upload) {
  1033. position: relative;
  1034. display: flex;
  1035. align-items: center;
  1036. justify-content: center;
  1037. width: 100%;
  1038. height: 200px;
  1039. cursor: pointer;
  1040. background-color: #f5f7fa;
  1041. border: 1px solid #dcdfe6;
  1042. border-radius: 4px;
  1043. transition: all 0.3s;
  1044. &:hover {
  1045. border-color: #6c8ff8;
  1046. }
  1047. }
  1048. // 当上传完成后隐藏上传按钮
  1049. &.upload-complete {
  1050. :deep(.el-upload) {
  1051. display: none !important;
  1052. }
  1053. }
  1054. :deep(.el-upload-list) {
  1055. .el-upload-list__item {
  1056. width: 100%;
  1057. height: 200px;
  1058. margin: 0;
  1059. }
  1060. }
  1061. .upload-placeholder {
  1062. display: flex;
  1063. align-items: center;
  1064. justify-content: center;
  1065. width: 100%;
  1066. height: 100%;
  1067. .placeholder-text {
  1068. font-size: 14px;
  1069. color: #909399;
  1070. }
  1071. }
  1072. }
  1073. }
  1074. }
  1075. // OCR 识别结果展示样式
  1076. .ocr-result-container {
  1077. width: 635px;
  1078. padding: 20px;
  1079. margin: 60px auto;
  1080. background-color: #f5f7fa;
  1081. border: 1px solid #e4e7ed;
  1082. border-radius: 8px;
  1083. .ocr-result-item {
  1084. display: flex;
  1085. align-items: center;
  1086. margin-bottom: 16px;
  1087. font-size: 16px;
  1088. line-height: 1.5;
  1089. &:last-child {
  1090. margin-bottom: 0;
  1091. }
  1092. .label {
  1093. min-width: 100px;
  1094. font-weight: 600;
  1095. color: #606266;
  1096. }
  1097. .value {
  1098. flex: 1;
  1099. color: #303133;
  1100. word-break: break-all;
  1101. }
  1102. }
  1103. .ocr-result-tip {
  1104. padding: 10px 0;
  1105. font-size: 14px;
  1106. color: #909399;
  1107. text-align: center;
  1108. }
  1109. }
  1110. }
  1111. }
  1112. .form-content {
  1113. max-width: 800px;
  1114. margin: 0 auto;
  1115. &.step2-form {
  1116. max-width: 100%;
  1117. .form-row {
  1118. display: flex;
  1119. gap: 40px;
  1120. .form-col {
  1121. flex: 1;
  1122. }
  1123. }
  1124. }
  1125. }
  1126. .form-actions {
  1127. display: flex;
  1128. gap: 20px;
  1129. justify-content: center;
  1130. padding-top: 30px;
  1131. margin-top: 40px;
  1132. border-top: 1px solid #e4e7ed;
  1133. .el-button {
  1134. width: 200px;
  1135. height: 44px;
  1136. font-size: 16px;
  1137. font-weight: 500;
  1138. color: #ffffff;
  1139. background: #6c8ff8;
  1140. border: none;
  1141. border-radius: 4px;
  1142. outline: none;
  1143. }
  1144. }
  1145. }
  1146. </style>