zfbIndex.vue 86 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477
  1. <template>
  2. <div class="zfb-step-page">
  3. <header class="step-header">
  4. <el-steps :active="zfbStepActive" align-center finish-status="process" class="zfb-steps">
  5. <el-step title="第一步 完善商家信息" />
  6. <el-step title="第二步 完善结算信息" />
  7. <el-step title="第三步 提交审核" />
  8. </el-steps>
  9. </header>
  10. <div class="page-layout">
  11. <div class="main-column">
  12. <el-form
  13. ref="formRef"
  14. class="zfb-form"
  15. :model="form"
  16. :rules="rules"
  17. label-width="200px"
  18. require-asterisk-position="left"
  19. label-position="right"
  20. >
  21. <!-- 主体信息 -->
  22. <section id="zfb-subject" class="form-block anchor-section">
  23. <h2 class="section-head">
  24. <span class="section-bar" aria-hidden="true" />
  25. 主体信息
  26. </h2>
  27. <el-form-item label="主体类型" prop="subjectType">
  28. <el-radio-group v-model="form.subjectType" class="radio-stack" @change="onSubjectTypeChange">
  29. <el-radio v-for="opt in subjectTypeOptions" :key="opt.value" :value="opt.value">
  30. {{ opt.label }}
  31. </el-radio>
  32. </el-radio-group>
  33. </el-form-item>
  34. <el-form-item prop="licenseUrl" v-if="form.subjectType === '01'">
  35. <template #label>
  36. <span class="label-with-icon"> 营业执照 </span>
  37. </template>
  38. <div class="license-upload-wrap">
  39. <el-upload
  40. v-model:file-list="form.licenseFileList"
  41. list-type="picture-card"
  42. :limit="1"
  43. accept=".jpg,.jpeg,.png,.bmp,.gif,image/jpeg,image/png,image/bmp,image/gif"
  44. :before-upload="beforeImageUpload"
  45. :http-request="opt => handleSingleUpload(opt, 'license')"
  46. :on-remove="(_f, fl) => onSingleRemove(fl, 'license')"
  47. >
  48. <el-icon><Plus /></el-icon>
  49. </el-upload>
  50. </div>
  51. <div class="upload-rules">
  52. <p>只支持.jpg、.png格式。</p>
  53. </div>
  54. </el-form-item>
  55. <el-form-item label="营业执照号" prop="creditCode" v-if="form.subjectType === '01'">
  56. <el-input
  57. v-model="form.creditCode"
  58. placeholder="请输入统一社会信用代码"
  59. clearable
  60. maxlength="32"
  61. style="max-width: 480px"
  62. />
  63. </el-form-item>
  64. <el-form-item prop="merchantShortName" v-if="form.subjectType === '01'">
  65. <template #label>
  66. <span class="label-with-icon"> 商户简称 </span>
  67. </template>
  68. <el-input v-model="form.merchantShortName" placeholder="请输入商户简称" clearable style="max-width: 480px" />
  69. </el-form-item>
  70. <el-form-item prop="businessCategory" v-if="form.subjectType === '01'">
  71. <template #label>
  72. <span class="label-with-icon"> 经营类目 </span>
  73. </template>
  74. <el-cascader
  75. v-model="form.businessCategory"
  76. :options="categoryCascaderOptions"
  77. :props="categoryCascaderProps"
  78. placeholder="请选择经营类目"
  79. clearable
  80. filterable
  81. style="width: 480px"
  82. />
  83. </el-form-item>
  84. <!--
  85. <template v-if="form.subjectType === '02'">
  86. <el-form-item prop="institutionCertUrl">
  87. <template #label>事业单位法人证书</template>
  88. <div class="license-upload-wrap">
  89. <el-upload
  90. v-model:file-list="form.institutionCertFileList"
  91. list-type="picture-card"
  92. :limit="1"
  93. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  94. :before-upload="beforeImageUploadInstJpgPng"
  95. :http-request="opt => handleSingleUpload(opt, 'institutionCert')"
  96. :on-remove="(_f, fl) => onSingleRemove(fl, 'institutionCert')"
  97. >
  98. <el-icon><Plus /></el-icon>
  99. </el-upload>
  100. </div>
  101. <div class="upload-rules">
  102. <p>仅支持 jpg、png 格式。</p>
  103. </div>
  104. </el-form-item>
  105. <el-form-item label="事业单位法人证书编号" prop="institutionCertNumber">
  106. <el-input
  107. v-model="form.institutionCertNumber"
  108. placeholder="请输入证书编号"
  109. clearable
  110. maxlength="64"
  111. style="max-width: 480px"
  112. />
  113. </el-form-item>
  114. <el-form-item prop="merchantShortName">
  115. <template #label>
  116. <span class="label-with-icon">
  117. 商户简称
  118. <el-tooltip placement="top" content="简称将展示给消费者,需与法人证书或品牌一致。">
  119. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  120. </el-tooltip>
  121. </span>
  122. </template>
  123. <el-input
  124. v-model="form.merchantShortName"
  125. placeholder="请输入商户简称"
  126. clearable
  127. maxlength="64"
  128. show-word-limit
  129. style="max-width: 480px"
  130. />
  131. </el-form-item>
  132. <el-form-item label="经营类目" prop="businessCategory">
  133. <el-cascader
  134. v-model="form.businessCategory"
  135. :options="categoryCascaderOptions"
  136. :props="categoryCascaderProps"
  137. placeholder="请选择经营类目"
  138. clearable
  139. filterable
  140. style="width: 480px"
  141. />
  142. </el-form-item>
  143. </template> -->
  144. <!-- <template v-if="form.subjectType === '03'">
  145. <el-form-item prop="otherOrgCertUrl">
  146. <template #label>民办非企业单位登记证书</template>
  147. <div class="license-upload-wrap">
  148. <el-upload
  149. v-model:file-list="form.otherOrgCertFileList"
  150. list-type="picture-card"
  151. :limit="1"
  152. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  153. :before-upload="beforeImageUploadInstJpgPng"
  154. :http-request="opt => handleSingleUpload(opt, 'otherOrgCert')"
  155. :on-remove="(_f, fl) => onSingleRemove(fl, 'otherOrgCert')"
  156. >
  157. <el-icon><Plus /></el-icon>
  158. </el-upload>
  159. </div>
  160. <div class="upload-rules">
  161. <p>只支持 jpg、png 格式。</p>
  162. </div>
  163. </el-form-item>
  164. <el-form-item label="民办非企业单位登记证书编号" prop="otherOrgCertNumber">
  165. <el-input
  166. v-model="form.otherOrgCertNumber"
  167. placeholder="请输入证书编号"
  168. clearable
  169. maxlength="64"
  170. style="max-width: 480px"
  171. />
  172. </el-form-item>
  173. <el-form-item prop="merchantShortName">
  174. <template #label>
  175. <span class="label-with-icon">
  176. 商户简称
  177. <el-tooltip placement="top" content="简称将展示给消费者,需与登记证书或品牌一致。">
  178. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  179. </el-tooltip>
  180. </span>
  181. </template>
  182. <el-input
  183. v-model="form.merchantShortName"
  184. placeholder="请输入商户简称"
  185. clearable
  186. maxlength="64"
  187. show-word-limit
  188. style="max-width: 480px"
  189. />
  190. </el-form-item>
  191. <el-form-item label="经营类目" prop="businessCategory">
  192. <el-cascader
  193. v-model="form.businessCategory"
  194. :options="categoryCascaderOptions"
  195. :props="categoryCascaderProps"
  196. placeholder="请选择经营类目"
  197. clearable
  198. filterable
  199. style="width: 480px"
  200. />
  201. </el-form-item>
  202. </template> -->
  203. <!-- <template v-if="form.subjectType === '04'">
  204. <el-form-item prop="socialCertUrl">
  205. <template #label>社会团体法人登记证书</template>
  206. <div class="license-upload-wrap">
  207. <el-upload
  208. v-model:file-list="form.socialCertFileList"
  209. list-type="picture-card"
  210. :limit="1"
  211. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  212. :before-upload="beforeImageUploadInstJpgPng"
  213. :http-request="opt => handleSingleUpload(opt, 'socialCert')"
  214. :on-remove="(_f, fl) => onSingleRemove(fl, 'socialCert')"
  215. >
  216. <el-icon><Plus /></el-icon>
  217. </el-upload>
  218. </div>
  219. <div class="upload-rules">
  220. <p>只支持 jpg、png 格式。</p>
  221. </div>
  222. </el-form-item>
  223. <el-form-item label="社会团体法人登记证书编号" prop="socialCertNumber">
  224. <el-input
  225. v-model="form.socialCertNumber"
  226. placeholder="请输入证书编号"
  227. clearable
  228. maxlength="64"
  229. style="max-width: 480px"
  230. />
  231. </el-form-item>
  232. <el-form-item prop="merchantShortName">
  233. <template #label>
  234. <span class="label-with-icon">
  235. 商户简称
  236. <el-tooltip placement="top" content="简称将展示给消费者,需与法人登记证书或品牌一致。">
  237. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  238. </el-tooltip>
  239. </span>
  240. </template>
  241. <el-input
  242. v-model="form.merchantShortName"
  243. placeholder="请输入商户简称"
  244. clearable
  245. maxlength="64"
  246. show-word-limit
  247. style="max-width: 480px"
  248. />
  249. </el-form-item>
  250. <el-form-item label="经营类目" prop="businessCategory">
  251. <el-cascader
  252. v-model="form.businessCategory"
  253. :options="categoryCascaderOptions"
  254. :props="categoryCascaderProps"
  255. placeholder="请选择经营类目"
  256. clearable
  257. filterable
  258. style="width: 480px"
  259. />
  260. </el-form-item>
  261. </template> -->
  262. <!-- <template v-if="form.subjectType === '05'">
  263. <el-form-item prop="govCertUrl">
  264. <template #label>
  265. <span class="label-wrap-multiline">党政机关批准设立文件/行政执法主体资格证</span>
  266. </template>
  267. <div class="license-upload-wrap">
  268. <el-upload
  269. v-model:file-list="form.govCertFileList"
  270. list-type="picture-card"
  271. :limit="1"
  272. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  273. :before-upload="beforeImageUploadInstJpgPng"
  274. :http-request="opt => handleSingleUpload(opt, 'govCert')"
  275. :on-remove="(_f, fl) => onSingleRemove(fl, 'govCert')"
  276. >
  277. <el-icon><Plus /></el-icon>
  278. </el-upload>
  279. </div>
  280. <div class="upload-rules">
  281. <p>只支持 jpg、png 格式。</p>
  282. </div>
  283. </el-form-item>
  284. <el-form-item prop="govCertNumber">
  285. <template #label>
  286. <span class="label-wrap-multiline">党政机关批准设立文件/行政执法主体资格证编号</span>
  287. </template>
  288. <el-input
  289. v-model="form.govCertNumber"
  290. placeholder="请输入编号"
  291. clearable
  292. maxlength="128"
  293. style="max-width: 480px"
  294. />
  295. </el-form-item>
  296. <el-form-item prop="merchantShortName">
  297. <template #label>
  298. <span class="label-with-icon">
  299. 商户简称
  300. <el-tooltip placement="top" content="简称将展示给消费者,需与批准文件或主体名称一致。">
  301. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  302. </el-tooltip>
  303. </span>
  304. </template>
  305. <el-input
  306. v-model="form.merchantShortName"
  307. placeholder="请输入商户简称"
  308. clearable
  309. maxlength="64"
  310. show-word-limit
  311. style="max-width: 480px"
  312. />
  313. </el-form-item>
  314. <el-form-item label="经营类目" prop="businessCategory">
  315. <el-cascader
  316. v-model="form.businessCategory"
  317. :options="categoryCascaderOptions"
  318. :props="categoryCascaderProps"
  319. placeholder="请选择经营类目"
  320. clearable
  321. filterable
  322. style="width: 480px"
  323. />
  324. </el-form-item>
  325. </template> -->
  326. <!-- <template v-if="form.subjectType === '06'">
  327. <el-alert
  328. class="individual-merchant-alert"
  329. type="info"
  330. :closable="false"
  331. show-icon
  332. >
  333. 使用当面付服务时,选择个人类型会有收款额度限制
  334. </el-alert>
  335. <el-form-item prop="idFrontUrl">
  336. <template #label>个人身份证</template>
  337. <div class="id-upload-row id-upload-row--four">
  338. <div class="id-upload-cell">
  339. <span class="id-side-label">上传人像面</span>
  340. <el-upload
  341. v-model:file-list="form.idFrontFileList"
  342. list-type="picture-card"
  343. :limit="1"
  344. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  345. :before-upload="beforeImageUploadInstJpgPng"
  346. :http-request="opt => handleSingleUpload(opt, 'idFront')"
  347. :on-remove="(_f, fl) => onSingleRemove(fl, 'idFront')"
  348. >
  349. <el-icon><Plus /></el-icon>
  350. </el-upload>
  351. </div>
  352. <div class="id-upload-cell">
  353. <span class="id-side-label">上传国徽面</span>
  354. <el-upload
  355. v-model:file-list="form.idBackFileList"
  356. list-type="picture-card"
  357. :limit="1"
  358. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  359. :before-upload="beforeImageUploadInstJpgPng"
  360. :http-request="opt => handleSingleUpload(opt, 'idBack')"
  361. :on-remove="(_f, fl) => onSingleRemove(fl, 'idBack')"
  362. >
  363. <el-icon><Plus /></el-icon>
  364. </el-upload>
  365. </div>
  366. </div>
  367. <div class="upload-rules">
  368. <p>
  369. 说明:仅支持 jpg、png 格式。如果没有证件原件照片,可以上传证件照片复印件。
  370. </p>
  371. </div>
  372. </el-form-item>
  373. <el-form-item label="个人身份证号码" prop="idNumber">
  374. <el-input v-model="form.idNumber" placeholder="请输入身份证号码" clearable style="max-width: 480px" />
  375. </el-form-item>
  376. <el-form-item prop="merchantShortName">
  377. <template #label>
  378. <span class="label-with-icon">
  379. 商户简称
  380. <el-tooltip placement="top" content="简称将展示给消费者,需与身份证或品牌一致。">
  381. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  382. </el-tooltip>
  383. </span>
  384. </template>
  385. <el-input
  386. v-model="form.merchantShortName"
  387. placeholder="请输入商户简称"
  388. clearable
  389. maxlength="64"
  390. show-word-limit
  391. style="max-width: 480px"
  392. />
  393. </el-form-item>
  394. <el-form-item label="经营类目" prop="businessCategory">
  395. <el-cascader
  396. v-model="form.businessCategory"
  397. :options="categoryCascaderOptions"
  398. :props="categoryCascaderProps"
  399. placeholder="请选择经营类目"
  400. clearable
  401. filterable
  402. style="width: 480px"
  403. />
  404. </el-form-item>
  405. </template> -->
  406. <template v-if="form.subjectType === '07'">
  407. <el-form-item prop="licenseUrl">
  408. <template #label> 营业执照 </template>
  409. <div class="license-upload-wrap">
  410. <el-upload
  411. v-model:file-list="form.licenseFileList"
  412. list-type="picture-card"
  413. :limit="1"
  414. accept=".jpg,.jpeg,.png,image/jpeg,image/png"
  415. :before-upload="beforeImageUploadInstJpgPng"
  416. :http-request="opt => handleSingleUpload(opt, 'license')"
  417. :on-remove="(_f, fl) => onSingleRemove(fl, 'license')"
  418. >
  419. <el-icon><Plus /></el-icon>
  420. </el-upload>
  421. </div>
  422. <div class="upload-rules">
  423. <p>只支持 jpg、png 格式。</p>
  424. </div>
  425. </el-form-item>
  426. <el-form-item label="营业执照编号" prop="creditCode">
  427. <el-input
  428. v-model="form.creditCode"
  429. placeholder="请输入营业执照编号"
  430. clearable
  431. maxlength="32"
  432. style="max-width: 480px"
  433. />
  434. </el-form-item>
  435. <el-form-item label="营业执照名称" prop="householdLicenseName">
  436. <el-input
  437. v-model="form.householdLicenseName"
  438. placeholder="请输入营业执照名称"
  439. clearable
  440. maxlength="128"
  441. style="max-width: 480px"
  442. />
  443. </el-form-item>
  444. <el-form-item prop="merchantShortName">
  445. <template #label>
  446. <span class="label-with-icon">
  447. 商户简称
  448. <el-tooltip placement="top" content="简称将展示给消费者,需与营业执照或品牌一致。">
  449. <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
  450. </el-tooltip>
  451. </span>
  452. </template>
  453. <el-input v-model="form.merchantShortName" placeholder="请输入商户简称" clearable style="max-width: 480px" />
  454. </el-form-item>
  455. <el-form-item label="经营类目" prop="businessCategory">
  456. <el-cascader
  457. v-model="form.businessCategory"
  458. :options="categoryCascaderOptions"
  459. :props="categoryCascaderProps"
  460. placeholder="请选择经营类目"
  461. clearable
  462. filterable
  463. style="width: 480px"
  464. />
  465. </el-form-item>
  466. </template>
  467. </section>
  468. <!-- 法人信息 -->
  469. <section id="zfb-legal" class="form-block anchor-section" v-if="form.subjectType === '01' || form.subjectType === '07'">
  470. <h2 class="section-head">
  471. <span class="section-bar" aria-hidden="true" />
  472. 法人信息
  473. </h2>
  474. <el-form-item label="法定代表人/负责人证件类型" prop="legalIdType">
  475. <el-select v-model="form.legalIdType" placeholder="请选择" clearable style="width: 260px">
  476. <el-option label="居民身份证" value="id_card" />
  477. <el-option label="护照" value="passport" />
  478. <el-option label="港澳居民来往内地通行证" value="hk_macau_pass" />
  479. <el-option label="台湾居民来往大陆通行证" value="taiwan_pass" />
  480. </el-select>
  481. </el-form-item>
  482. <el-form-item prop="idFrontUrl">
  483. <template #label> 上传证件照片 </template>
  484. <div class="id-upload-row">
  485. <div class="id-upload-cell">
  486. <div class="license-upload-wrap">
  487. <el-upload
  488. v-model:file-list="form.idFrontFileList"
  489. list-type="picture-card"
  490. :limit="1"
  491. accept=".jpg,.jpeg,.png,.bmp,.gif,image/jpeg,image/png,image/bmp,image/gif"
  492. :before-upload="beforeImageUpload"
  493. :http-request="opt => handleSingleUpload(opt, 'idFront')"
  494. :on-remove="(_f, fl) => onSingleRemove(fl, 'idFront')"
  495. >
  496. <div class="id-upload-trigger">
  497. <el-icon><Plus /></el-icon>
  498. <div class="id-side-label">上传证件正面</div>
  499. </div>
  500. </el-upload>
  501. </div>
  502. </div>
  503. <div class="id-upload-cell">
  504. <div class="license-upload-wrap">
  505. <el-upload
  506. v-model:file-list="form.idBackFileList"
  507. list-type="picture-card"
  508. :limit="1"
  509. accept=".jpg,.jpeg,.png,.bmp,.gif,image/jpeg,image/png,image/bmp,image/gif"
  510. :before-upload="beforeImageUpload"
  511. :http-request="opt => handleSingleUpload(opt, 'idBack')"
  512. :on-remove="(_f, fl) => onSingleRemove(fl, 'idBack')"
  513. >
  514. <div class="id-upload-trigger">
  515. <el-icon><Plus /></el-icon>
  516. <div class="id-side-label">上传证件反面</div>
  517. </div>
  518. </el-upload>
  519. </div>
  520. </div>
  521. </div>
  522. <div class="upload-rules">
  523. <p>说明:仅支持.jpg、.png格式,如果没有证件原件照片,可以上传证件照片复印件。</p>
  524. </div>
  525. </el-form-item>
  526. <el-form-item label="证件号" prop="idNumber">
  527. <el-input v-model="form.idNumber" placeholder="请输入证件号码" clearable style="max-width: 480px" />
  528. </el-form-item>
  529. <el-form-item label="法定代表人/负责人姓名" prop="legalName">
  530. <el-input v-model="form.legalName" placeholder="请输入姓名" clearable style="max-width: 480px" />
  531. </el-form-item>
  532. </section>
  533. <!-- 经营信息 -->
  534. <section id="zfb-business" class="form-block anchor-section">
  535. <h2 class="section-head">
  536. <span class="section-bar" aria-hidden="true" />
  537. 经营信息
  538. </h2>
  539. <el-form-item label="接入服务类型" prop="serviceTypes">
  540. <div class="field-stack">
  541. <el-checkbox-group v-model="form.serviceTypes" class="service-checks">
  542. <el-checkbox v-for="s in serviceTypeOptions" :key="s.value" :label="s.value">
  543. {{ s.label }}
  544. </el-checkbox>
  545. </el-checkbox-group>
  546. <p class="field-tip">
  547. 请勾选实际售卖商品/提供服务场景(至少一项),已便为你开通所需支付权限。建议只勾选目前必须的场景,以便尽快通过入驻审核,其他支付权限可在入驻后再根据需要发起申请
  548. </p>
  549. </div>
  550. </el-form-item>
  551. <el-form-item prop="storefrontUrl" v-if="form.serviceTypes.includes('当面付')">
  552. <template #label> 上传门头照 </template>
  553. <div class="license-upload-wrap">
  554. <el-upload
  555. v-model:file-list="form.storefrontFileList"
  556. list-type="picture-card"
  557. :limit="1"
  558. accept=".jpg,.jpeg,.png,.bmp,.gif,image/jpeg,image/png,image/bmp,image/gif"
  559. :before-upload="beforeImageUpload"
  560. :http-request="opt => handleSingleUpload(opt, 'storefront')"
  561. :on-remove="(_f, fl) => onSingleRemove(fl, 'storefront')"
  562. >
  563. <el-icon><Plus /></el-icon>
  564. </el-upload>
  565. </div>
  566. <div class="upload-rules">
  567. <p>说明:仅支持jpg、png格式,图片需清晰完整,最多上传3张。</p>
  568. </div>
  569. </el-form-item>
  570. <el-form-item prop="interiorUrl" v-if="form.serviceTypes.includes('当面付')">
  571. <template #label> 上传内景照 </template>
  572. <div class="license-upload-wrap">
  573. <el-upload
  574. v-model:file-list="form.interiorFileList"
  575. list-type="picture-card"
  576. :limit="1"
  577. accept=".jpg,.jpeg,.png,.bmp,.gif,image/jpeg,image/png,image/bmp,image/gif"
  578. :before-upload="beforeImageUpload"
  579. :http-request="opt => handleSingleUpload(opt, 'interior')"
  580. :on-remove="(_f, fl) => onSingleRemove(fl, 'interior')"
  581. >
  582. <el-icon><Plus /></el-icon>
  583. </el-upload>
  584. </div>
  585. <div class="upload-rules">
  586. <p>说明:仅支持jpg、png格式,图片需清晰完整,最多上传3张。</p>
  587. </div>
  588. </el-form-item>
  589. <el-form-item label="店铺地址" prop="region" v-if="form.serviceTypes.includes('当面付')">
  590. <div class="address-row">
  591. <el-cascader
  592. v-model="form.region"
  593. :options="regionCascaderOptions"
  594. placeholder="省 / 市"
  595. filterable
  596. clearable
  597. style="width: 320px"
  598. />
  599. <el-input
  600. v-model="form.detailAddress"
  601. class="address-detail"
  602. placeholder="街道、门牌号等"
  603. clearable
  604. maxlength="200"
  605. />
  606. </div>
  607. </el-form-item>
  608. <el-form-item label="APP名称" prop="appName" v-if="form.serviceTypes.includes('App支付')">
  609. <el-input
  610. v-model="form.appName"
  611. placeholder="请输入APP名称"
  612. clearable
  613. maxlength="64"
  614. show-word-limit
  615. style="max-width: 480px"
  616. />
  617. </el-form-item>
  618. <el-form-item label="联系人姓名" prop="contactName">
  619. <el-input v-model="form.contactName" placeholder="请输入联系人姓名" clearable style="max-width: 480px" />
  620. </el-form-item>
  621. <el-form-item label="联系人手机号" prop="contactPhone">
  622. <el-input
  623. v-model="form.contactPhone"
  624. placeholder="请输入手机号"
  625. clearable
  626. maxlength="11"
  627. style="max-width: 480px"
  628. />
  629. </el-form-item>
  630. <el-form-item label="联系人邮箱" prop="contactEmail">
  631. <el-input v-model="form.contactEmail" placeholder="请输入邮箱" clearable style="max-width: 480px" />
  632. </el-form-item>
  633. <el-form-item label="客服电话" prop="servicePhone">
  634. <el-input
  635. v-model="form.servicePhone"
  636. placeholder="请输入客服电话"
  637. clearable
  638. maxlength="20"
  639. style="max-width: 480px"
  640. />
  641. </el-form-item>
  642. <el-form-item prop="settlementAlipayAccount">
  643. <template #label> 签约支付宝账号 </template>
  644. <div class="field-stack">
  645. <el-input
  646. v-model="form.settlementAlipayAccount"
  647. placeholder="请输入用于收款的支付宝账号"
  648. clearable
  649. style="max-width: 480px"
  650. />
  651. <p class="field-tip">如商家无支付宝账号,请引导商家至支付宝注册。</p>
  652. </div>
  653. </el-form-item>
  654. </section>
  655. </el-form>
  656. <div class="footer-actions">
  657. <el-button type="primary" class="btn-primary-zfb" :loading="nextLoading" @click="onNext"> 下一步 </el-button>
  658. </div>
  659. </div>
  660. <aside class="anchor-column">
  661. <el-affix :offset="88">
  662. <div class="anchor-card">
  663. <el-anchor container=".el-main" :offset="72" :bound="48" direction="vertical">
  664. <el-anchor-link href="#zfb-subject" title="主体信息" />
  665. <el-anchor-link href="#zfb-legal" title="法人信息" />
  666. <el-anchor-link href="#zfb-business" title="经营信息" />
  667. </el-anchor>
  668. </div>
  669. </el-affix>
  670. </aside>
  671. </div>
  672. </div>
  673. </template>
  674. <script setup lang="ts">
  675. import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
  676. import { useRoute, useRouter } from "vue-router";
  677. import { ElMessage } from "element-plus";
  678. import type { FormInstance, FormRules } from "element-plus";
  679. import type { UploadRequestOptions, UploadUserFile } from "element-plus";
  680. import { Plus, InfoFilled } from "@element-plus/icons-vue";
  681. import { getOcrRequestByBase64 } from "@/api/modules/businessInfo";
  682. import {
  683. uploadBusinessInfoImageToOss,
  684. uploadBusinessInfoImageWithPageOcr,
  685. filterOutUploadUserFileByUid,
  686. failBusinessInfoUploadCleanup
  687. } from "@/utils/businessInfoImageUpload";
  688. import { localGet, localSet } from "@/utils/index";
  689. import cityJson from "@/assets/json/city.json";
  690. import categoryJson from "@/views/businessInfo/category.json";
  691. const ALIPAY_JSON_KEY = "alipayJson";
  692. const GEEKER_USER_KEY = "geeker-user";
  693. /** 同时勾选当面付 + App 支付时,APP 名称默认与进件 sites[0].SiteName 一致 */
  694. const DEFAULT_ALIPAY_APP_SITE_NAME = "应用2.0签约2025092468860692";
  695. /** 进件 externalId:16 位随机数字(0–9) */
  696. function randomAlipayExternalId16(): string {
  697. const buf = new Uint8Array(16);
  698. crypto.getRandomValues(buf);
  699. let s = "";
  700. for (let i = 0; i < 16; i++) {
  701. s += String(buf[i]! % 10);
  702. }
  703. return s;
  704. }
  705. const formRef = ref<FormInstance>();
  706. const nextLoading = ref(false);
  707. const router = useRouter();
  708. const route = useRoute();
  709. function resolveAlipayUploadStoreId(): string | number | null {
  710. const q = route.query.storeId;
  711. if (q !== undefined && q !== null && String(q).trim() !== "") {
  712. return Array.isArray(q) ? q[0]! : q;
  713. }
  714. const geeker = localGet(GEEKER_USER_KEY) as { userInfo?: { storeId?: string | number | null } } | null | undefined;
  715. const sid = geeker?.userInfo?.storeId ?? localGet("createdId");
  716. if (sid === undefined || sid === null || sid === "") return null;
  717. return sid;
  718. }
  719. /** 支付宝进件步骤:0 商家信息 / 1 结算信息 / 2 提交审核 */
  720. const zfbStepActive = ref(0);
  721. type CategoryJsonNode = {
  722. code?: string;
  723. name?: string;
  724. parentCode?: string;
  725. parentName?: string;
  726. level?: number;
  727. display?: boolean;
  728. status?: string;
  729. sortNum?: number;
  730. childrenNodes?: CategoryJsonNode[];
  731. };
  732. type CategoryCascaderNode = { value: string; label: string; children?: CategoryCascaderNode[] };
  733. /**
  734. * 二级经营类目:一级为 level=1(name/code),二级为其 childrenNodes 中 level=2 的 name/code
  735. */
  736. function buildCategoryCascaderOptions(root: unknown): CategoryCascaderNode[] {
  737. const list = (root as { data?: { childrenNodes?: CategoryJsonNode[] } })?.data?.childrenNodes;
  738. if (!Array.isArray(list)) return [];
  739. return list
  740. .filter(n => n && typeof n === "object" && n.level === 1 && n.code && n.name && n.status === "VALID" && n.display !== false)
  741. .sort((a, b) => (a.sortNum ?? 0) - (b.sortNum ?? 0))
  742. .map(parent => {
  743. const children = (parent.childrenNodes || [])
  744. .filter(
  745. ch =>
  746. ch && typeof ch === "object" && ch.level === 2 && ch.code && ch.name && ch.status === "VALID" && ch.display !== false
  747. )
  748. .sort((a, b) => (a.sortNum ?? 0) - (b.sortNum ?? 0))
  749. .map(ch => ({
  750. value: String(ch.code),
  751. label: String(ch.name)
  752. }));
  753. return {
  754. value: String(parent.code),
  755. label: String(parent.name),
  756. children
  757. };
  758. })
  759. .filter(p => p.children && p.children.length > 0);
  760. }
  761. const categoryCascaderOptions = buildCategoryCascaderOptions(categoryJson);
  762. const categoryCascaderProps = {
  763. value: "value",
  764. label: "label",
  765. children: "children",
  766. emitPath: true
  767. };
  768. const subjectTypeOptions = [
  769. { label: "企业", value: "01" },
  770. // { label: "事业单位", value: "02" },
  771. // { label: "民办非企业组织", value: "03" },
  772. // { label: "社会团体", value: "04" },
  773. // { label: "党政及国家机关", value: "05" },
  774. // { label: "个人商户", value: "06" },
  775. { label: "个体工商户", value: "07" }
  776. ];
  777. const serviceTypeOptions = [
  778. { label: "当面付", value: "当面付" },
  779. { label: "App支付", value: "App支付" }
  780. ];
  781. type CityJsonEntry = { name: string; adCode: string; cityCode: string };
  782. type CityJsonRoot = { cityList: { letter: string; list: CityJsonEntry[] }[] };
  783. const PROVINCE_ROWS: [string, string][] = [
  784. ["11", "北京市"],
  785. ["12", "天津市"],
  786. ["13", "河北省"],
  787. ["14", "山西省"],
  788. ["15", "内蒙古自治区"],
  789. ["21", "辽宁省"],
  790. ["22", "吉林省"],
  791. ["23", "黑龙江省"],
  792. ["31", "上海市"],
  793. ["32", "江苏省"],
  794. ["33", "浙江省"],
  795. ["34", "安徽省"],
  796. ["35", "福建省"],
  797. ["36", "江西省"],
  798. ["37", "山东省"],
  799. ["41", "河南省"],
  800. ["42", "湖北省"],
  801. ["43", "湖南省"],
  802. ["44", "广东省"],
  803. ["45", "广西壮族自治区"],
  804. ["46", "海南省"],
  805. ["50", "重庆市"],
  806. ["51", "四川省"],
  807. ["52", "贵州省"],
  808. ["53", "云南省"],
  809. ["54", "西藏自治区"],
  810. ["61", "陕西省"],
  811. ["62", "甘肃省"],
  812. ["63", "青海省"],
  813. ["64", "宁夏回族自治区"],
  814. ["65", "新疆维吾尔自治区"],
  815. ["71", "台湾省"],
  816. ["81", "香港特别行政区"],
  817. ["82", "澳门特别行政区"]
  818. ];
  819. const PROVINCE_BY_PREFIX = Object.fromEntries(PROVINCE_ROWS) as Record<string, string>;
  820. const PROVINCE_PREFIX_ORDER = PROVINCE_ROWS.map(([k]) => k);
  821. function buildProvinceCityOptions(): {
  822. label: string;
  823. value: string;
  824. cities: { label: string; value: string }[];
  825. }[] {
  826. const root = cityJson as CityJsonRoot;
  827. const flat = root.cityList.flatMap(g => g.list);
  828. const bucket = new Map<string, Map<string, CityJsonEntry>>();
  829. for (const c of flat) {
  830. const digits = String(c.adCode ?? "").replace(/\D/g, "");
  831. const code = digits.length >= 6 ? digits.slice(-6) : digits.padStart(6, "0");
  832. if (code.length !== 6) continue;
  833. const prefix = code.slice(0, 2);
  834. if (!PROVINCE_BY_PREFIX[prefix]) continue;
  835. if (!bucket.has(prefix)) bucket.set(prefix, new Map());
  836. bucket.get(prefix)!.set(c.adCode, c);
  837. }
  838. const out = [...bucket.entries()].map(([prefix, cityMap]) => ({
  839. value: prefix,
  840. label: PROVINCE_BY_PREFIX[prefix],
  841. cities: [...cityMap.values()]
  842. .sort((a, b) => a.name.localeCompare(b.name, "zh-CN"))
  843. .map(ci => ({ label: ci.name, value: ci.adCode }))
  844. }));
  845. out.sort((a, b) => {
  846. const ia = PROVINCE_PREFIX_ORDER.indexOf(a.value);
  847. const ib = PROVINCE_PREFIX_ORDER.indexOf(b.value);
  848. return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib);
  849. });
  850. return out;
  851. }
  852. const provinceCityOptions = buildProvinceCityOptions();
  853. const regionCascaderOptions = computed(() =>
  854. provinceCityOptions.map(p => ({
  855. value: p.value,
  856. label: p.label,
  857. children: p.cities.map(c => ({ value: c.value, label: c.label }))
  858. }))
  859. );
  860. type UploadKind =
  861. | "license"
  862. | "idFront"
  863. | "idBack"
  864. | "storefront"
  865. | "interior"
  866. | "institutionCert"
  867. | "otherOrgCert"
  868. | "socialCert"
  869. | "govCert";
  870. const form = reactive<{
  871. subjectType: string;
  872. licenseFileList: UploadUserFile[];
  873. licenseUrl: string;
  874. /** 营业执照上传接口返回的 image_id,供 alipayJson.certImage */
  875. licenseImageId: string;
  876. creditCode: string;
  877. householdLicenseName: string;
  878. merchantShortName: string;
  879. businessTermRange: [string, string] | undefined;
  880. institutionCertFileList: UploadUserFile[];
  881. institutionCertUrl: string;
  882. institutionCertNumber: string;
  883. otherOrgCertFileList: UploadUserFile[];
  884. otherOrgCertUrl: string;
  885. otherOrgCertNumber: string;
  886. socialCertFileList: UploadUserFile[];
  887. socialCertUrl: string;
  888. socialCertNumber: string;
  889. govCertFileList: UploadUserFile[];
  890. govCertUrl: string;
  891. govCertNumber: string;
  892. businessCategory: string[];
  893. legalIdType: string;
  894. idFrontFileList: UploadUserFile[];
  895. idFrontUrl: string;
  896. idBackFileList: UploadUserFile[];
  897. idBackUrl: string;
  898. idNumber: string;
  899. legalName: string;
  900. serviceTypes: string[];
  901. storefrontFileList: UploadUserFile[];
  902. storefrontUrl: string;
  903. /** 上传接口返回的 image_id,供 alipayJson.outDoorImages */
  904. storefrontImageId: string;
  905. interiorFileList: UploadUserFile[];
  906. interiorUrl: string;
  907. /** 上传接口返回的 image_id,供 alipayJson.inDoorImages */
  908. interiorImageId: string;
  909. institutionStorefrontFileList: UploadUserFile[];
  910. institutionStorefrontUrls: string[];
  911. institutionInteriorFileList: UploadUserFile[];
  912. institutionInteriorUrls: string[];
  913. region: string[];
  914. detailAddress: string;
  915. appName: string;
  916. contactName: string;
  917. contactPhone: string;
  918. contactEmail: string;
  919. servicePhone: string;
  920. settlementAlipayAccount: string;
  921. }>({
  922. subjectType: "01",
  923. licenseFileList: [],
  924. licenseUrl: "",
  925. licenseImageId: "",
  926. creditCode: "",
  927. householdLicenseName: "",
  928. merchantShortName: "",
  929. businessTermRange: undefined,
  930. institutionCertFileList: [],
  931. institutionCertUrl: "",
  932. institutionCertNumber: "",
  933. otherOrgCertFileList: [],
  934. otherOrgCertUrl: "",
  935. otherOrgCertNumber: "",
  936. socialCertFileList: [],
  937. socialCertUrl: "",
  938. socialCertNumber: "",
  939. govCertFileList: [],
  940. govCertUrl: "",
  941. govCertNumber: "",
  942. businessCategory: [],
  943. legalIdType: "",
  944. idFrontFileList: [],
  945. idFrontUrl: "",
  946. idBackFileList: [],
  947. idBackUrl: "",
  948. idNumber: "",
  949. legalName: "",
  950. serviceTypes: [],
  951. storefrontFileList: [],
  952. storefrontUrl: "",
  953. storefrontImageId: "",
  954. interiorFileList: [],
  955. interiorUrl: "",
  956. interiorImageId: "",
  957. institutionStorefrontFileList: [],
  958. institutionStorefrontUrls: [],
  959. institutionInteriorFileList: [],
  960. institutionInteriorUrls: [],
  961. region: [],
  962. detailAddress: "",
  963. appName: "",
  964. contactName: "",
  965. contactPhone: "",
  966. contactEmail: "",
  967. servicePhone: "",
  968. settlementAlipayAccount: ""
  969. });
  970. watch(
  971. () => form.serviceTypes.slice().sort().join("\0"),
  972. () => {
  973. const hasFace = form.serviceTypes.includes("当面付");
  974. const hasApp = form.serviceTypes.includes("App支付");
  975. if (hasFace && hasApp && !String(form.appName || "").trim()) {
  976. form.appName = DEFAULT_ALIPAY_APP_SITE_NAME;
  977. }
  978. }
  979. );
  980. /** 用户切换到个体工商户时清空本页字段,避免企业侧已填数据带入个体户(与初始 form 一致,不改 subjectType) */
  981. function clearZfbFormExceptSubjectType() {
  982. form.licenseFileList = [];
  983. form.licenseUrl = "";
  984. form.licenseImageId = "";
  985. form.creditCode = "";
  986. form.householdLicenseName = "";
  987. form.merchantShortName = "";
  988. form.businessTermRange = undefined;
  989. form.institutionCertFileList = [];
  990. form.institutionCertUrl = "";
  991. form.institutionCertNumber = "";
  992. form.otherOrgCertFileList = [];
  993. form.otherOrgCertUrl = "";
  994. form.otherOrgCertNumber = "";
  995. form.socialCertFileList = [];
  996. form.socialCertUrl = "";
  997. form.socialCertNumber = "";
  998. form.govCertFileList = [];
  999. form.govCertUrl = "";
  1000. form.govCertNumber = "";
  1001. form.businessCategory = [];
  1002. form.legalIdType = "";
  1003. form.idFrontFileList = [];
  1004. form.idFrontUrl = "";
  1005. form.idBackFileList = [];
  1006. form.idBackUrl = "";
  1007. form.idNumber = "";
  1008. form.legalName = "";
  1009. form.serviceTypes = [];
  1010. form.storefrontFileList = [];
  1011. form.storefrontUrl = "";
  1012. form.storefrontImageId = "";
  1013. form.interiorFileList = [];
  1014. form.interiorUrl = "";
  1015. form.interiorImageId = "";
  1016. form.institutionStorefrontFileList = [];
  1017. form.institutionStorefrontUrls = [];
  1018. form.institutionInteriorFileList = [];
  1019. form.institutionInteriorUrls = [];
  1020. form.region = [];
  1021. form.detailAddress = "";
  1022. form.appName = "";
  1023. form.contactName = "";
  1024. form.contactPhone = "";
  1025. form.contactEmail = "";
  1026. form.servicePhone = "";
  1027. form.settlementAlipayAccount = "";
  1028. }
  1029. function onSubjectTypeChange(val: string | number | boolean | undefined) {
  1030. if (String(val ?? "") !== "07") return;
  1031. clearZfbFormExceptSubjectType();
  1032. nextTick(() => formRef.value?.clearValidate());
  1033. }
  1034. /** 商户端图片上传统一上限 20MB(若支付宝接口仍限 10MB,超大文件可能在服务端被拒) */
  1035. const MAX_ALIPAY_IMAGE_BYTES = 20 * 1024 * 1024;
  1036. /** 事业单位材料:仅 jpg / png(与页面说明一致) */
  1037. function beforeImageUploadInstJpgPng(file: File) {
  1038. const name = file.name?.toLowerCase() || "";
  1039. const okExt = /\.(jpe?g|png)$/i.test(name);
  1040. const okMime = ["image/jpeg", "image/png"].includes(file.type);
  1041. if (!okExt && !okMime) {
  1042. ElMessage.error("仅支持 jpg、png 格式");
  1043. return false;
  1044. }
  1045. if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
  1046. ElMessage.error("文件大小不能超过 20M");
  1047. return false;
  1048. }
  1049. return true;
  1050. }
  1051. function beforeImageUpload(file: File) {
  1052. const name = file.name?.toLowerCase() || "";
  1053. const okExt = /\.(jpe?g|png|bmp|gif)$/i.test(name);
  1054. const okMime = ["image/jpeg", "image/png", "image/bmp", "image/gif"].includes(file.type);
  1055. if (!okExt && !okMime) {
  1056. ElMessage.error("图片只支持 JPG、BMP、PNG、GIF 格式");
  1057. return false;
  1058. }
  1059. if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
  1060. ElMessage.error("文件大小不能超过 20M");
  1061. return false;
  1062. }
  1063. return true;
  1064. }
  1065. /** 解析 image_type:扩展名 3~16 字符,jpeg 归一为 jpg */
  1066. function resolveAlipayImageType(file: File): string | null {
  1067. const name = file.name || "";
  1068. const extMatch = /\.([a-zA-Z0-9]{1,16})$/i.exec(name);
  1069. let ext = extMatch ? extMatch[1].toLowerCase() : "";
  1070. if (ext === "jpeg") ext = "jpg";
  1071. if (ext && ext.length >= 3 && ext.length <= 16) return ext;
  1072. const mime = file.type;
  1073. if (mime === "image/jpeg") return "jpg";
  1074. if (mime === "image/png") return "png";
  1075. if (mime === "image/gif") return "gif";
  1076. if (mime === "image/bmp") return "bmp";
  1077. return null;
  1078. }
  1079. /** 解析支付宝/通用上传返回;兼容 data 为 JSON 字符串(直付通 ant_merchant_expand_indirect_image_upload_response) */
  1080. function parseMediaUploadResult(res: any): { fileUrl: string; mediaId: string; errMsg: string } {
  1081. const errMsg = String(res?.msg ?? "").trim();
  1082. let payload = res?.data;
  1083. if (payload === undefined) payload = res;
  1084. if (typeof payload === "string") {
  1085. const s = payload.trim();
  1086. if (/^https?:\/\//i.test(s)) return { fileUrl: s, mediaId: "", errMsg };
  1087. if ((s.startsWith("{") || s.startsWith("[")) && s.length > 1) {
  1088. try {
  1089. payload = JSON.parse(s);
  1090. } catch {
  1091. if (s) return { fileUrl: "", mediaId: s, errMsg };
  1092. return { fileUrl: "", mediaId: "", errMsg };
  1093. }
  1094. } else if (s) {
  1095. return { fileUrl: "", mediaId: s, errMsg };
  1096. } else {
  1097. return { fileUrl: "", mediaId: "", errMsg };
  1098. }
  1099. }
  1100. if (!payload || typeof payload !== "object") {
  1101. return { fileUrl: "", mediaId: "", errMsg };
  1102. }
  1103. let body: any = payload;
  1104. if ("data" in payload && payload.data !== undefined) {
  1105. if (typeof payload.data === "string") {
  1106. const inner = payload.data.trim();
  1107. if (/^https?:\/\//i.test(inner)) return { fileUrl: inner, mediaId: "", errMsg };
  1108. if ((inner.startsWith("{") || inner.startsWith("[")) && inner.length > 1) {
  1109. try {
  1110. body = JSON.parse(inner);
  1111. } catch {
  1112. if (inner) return { fileUrl: "", mediaId: inner, errMsg };
  1113. body = payload;
  1114. }
  1115. } else if (inner) {
  1116. return { fileUrl: "", mediaId: inner, errMsg };
  1117. }
  1118. } else if (payload.data && typeof payload.data === "object") {
  1119. body = payload.data;
  1120. }
  1121. }
  1122. if (body && typeof body === "object" && "data" in body && body.data !== undefined) {
  1123. if (typeof body.data === "string") {
  1124. const inner = body.data.trim();
  1125. if (/^https?:\/\//i.test(inner)) return { fileUrl: inner, mediaId: "", errMsg };
  1126. if ((inner.startsWith("{") || inner.startsWith("[")) && inner.length > 1) {
  1127. try {
  1128. body = JSON.parse(inner);
  1129. } catch {
  1130. if (inner) return { fileUrl: "", mediaId: inner, errMsg };
  1131. }
  1132. } else if (inner) {
  1133. return { fileUrl: "", mediaId: inner, errMsg };
  1134. }
  1135. } else if (body.data && typeof body.data === "object") {
  1136. body = body.data;
  1137. }
  1138. }
  1139. const antUpload =
  1140. body?.ant_merchant_expand_indirect_image_upload_response ?? body?.antMerchantExpandIndirectImageUploadResponse;
  1141. const fileUrl = String(
  1142. body?.url ?? body?.fileUrl ?? body?.imageUrl ?? body?.image_url ?? body?.picUrl ?? body?.ossUrl ?? body?.file_path ?? ""
  1143. ).trim();
  1144. const mediaId = String(
  1145. antUpload?.image_id ??
  1146. antUpload?.imageId ??
  1147. body?.image_id ??
  1148. body?.imageId ??
  1149. body?.resource_id ??
  1150. body?.resourceId ??
  1151. body?.media_id ??
  1152. body?.mediaId ??
  1153. body?.outBizNo ??
  1154. body?.pictureId ??
  1155. ""
  1156. ).trim();
  1157. return { fileUrl, mediaId, errMsg };
  1158. }
  1159. /** 与支付宝上传返回常见的 object / 文件名一致 */
  1160. const ALIPAY_UPLOAD_IMAGE_FILE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(jpe?g|png)$/i;
  1161. function extractAlipayFilenameFromImageUrl(url: string): string {
  1162. const s = String(url || "").trim();
  1163. if (!s || !/^https?:\/\//i.test(s)) return "";
  1164. try {
  1165. const seg = decodeURIComponent(new URL(s).pathname.split("/").filter(Boolean).pop() || "");
  1166. if (ALIPAY_UPLOAD_IMAGE_FILE_RE.test(seg) || /\.(jpe?g|png)$/i.test(seg)) return seg;
  1167. } catch {
  1168. /* ignore */
  1169. }
  1170. return "";
  1171. }
  1172. /** 深度遍历响应,取 image_id 等字段(兼容多层 data / 字段名差异) */
  1173. function deepPickAlipayImageId(node: any, depth = 0): string {
  1174. if (depth > 14 || node === null || node === undefined) return "";
  1175. if (typeof node === "string") {
  1176. const t = node.trim();
  1177. if (ALIPAY_UPLOAD_IMAGE_FILE_RE.test(t)) return t;
  1178. if (t.startsWith("{") && t.includes("image")) {
  1179. try {
  1180. return deepPickAlipayImageId(JSON.parse(t), depth + 1);
  1181. } catch {
  1182. return "";
  1183. }
  1184. }
  1185. return "";
  1186. }
  1187. if (typeof node !== "object") return "";
  1188. if (Array.isArray(node)) {
  1189. for (const x of node) {
  1190. const r = deepPickAlipayImageId(x, depth + 1);
  1191. if (r) return r;
  1192. }
  1193. return "";
  1194. }
  1195. const keys = ["image_id", "imageId", "ImageId", "fileName", "file_name", "objectName", "object_name", "key", "picName"];
  1196. for (const k of keys) {
  1197. if (!(k in node) || node[k] == null) continue;
  1198. const v = String(node[k]).trim();
  1199. if (!v) continue;
  1200. if (/^https?:\/\//i.test(v)) {
  1201. const fromUrl = extractAlipayFilenameFromImageUrl(v);
  1202. if (fromUrl) return fromUrl;
  1203. } else {
  1204. const base = v.split(/[/\\]/).pop() || v;
  1205. if (base) return base.trim();
  1206. }
  1207. }
  1208. for (const v of Object.values(node)) {
  1209. const r = deepPickAlipayImageId(v, depth + 1);
  1210. if (r) return r;
  1211. }
  1212. return "";
  1213. }
  1214. /** 上传成功后得到进件用图片名:扁平解析 → 深度查找 → 从返回的图片 URL 截取 */
  1215. function resolveAlipayUploadImageId(res: any, fileUrl: string, mediaId: string): string {
  1216. let id = String(mediaId || "").trim();
  1217. if (id) return id.split(/[/\\]/).pop() || id;
  1218. id = deepPickAlipayImageId(res);
  1219. if (id) return id.split(/[/\\]/).pop() || id;
  1220. return extractAlipayFilenameFromImageUrl(fileUrl);
  1221. }
  1222. function isAlipayUploadHttpSuccess(res: any): boolean {
  1223. return res?.code === 200 || res?.code === "200";
  1224. }
  1225. /** 与 subjectInfo:营业执照 OCR 响应解析 */
  1226. function pickBusinessLicenseOcrFields(res: any) {
  1227. const raw = res?.data ?? res;
  1228. let node: any = raw;
  1229. if (node && typeof node === "object" && "data" in node && node.data !== undefined) {
  1230. node = node.data;
  1231. }
  1232. const item = Array.isArray(node) ? node[0] : node;
  1233. if (!item || typeof item !== "object") {
  1234. return { creditCode: "", companyName: "", legalPerson: "" };
  1235. }
  1236. return {
  1237. creditCode: String(item.creditCode ?? item.credit_code ?? "").trim(),
  1238. companyName: String(item.companyName ?? item.company_name ?? "").trim(),
  1239. legalPerson: String(item.legalPerson ?? item.legal_person ?? "").trim()
  1240. };
  1241. }
  1242. /** 事业单位法人证书 OCR:证书编号、单位名称(回显商户简称) */
  1243. function pickInstitutionCertOcrFields(res: any): { certNumber: string; orgName: string } {
  1244. const base = pickBusinessLicenseOcrFields(res);
  1245. const raw = res?.data ?? res;
  1246. let node: any = raw;
  1247. if (node && typeof node === "object" && "data" in node && node.data !== undefined) {
  1248. node = node.data;
  1249. }
  1250. const item = Array.isArray(node) ? node[0] : node;
  1251. let certNumber = base.creditCode;
  1252. let orgName = base.companyName;
  1253. if (item && typeof item === "object") {
  1254. if (!certNumber) {
  1255. certNumber = String(
  1256. item.certNumber ??
  1257. item.certificateNumber ??
  1258. item.cert_no ??
  1259. item.certificateNo ??
  1260. item.registrationNumber ??
  1261. item.registration_no ??
  1262. ""
  1263. ).trim();
  1264. }
  1265. if (!orgName) {
  1266. orgName = String(item.companyName ?? item.company_name ?? item.orgName ?? item.unitName ?? item.name ?? "").trim();
  1267. }
  1268. }
  1269. return { certNumber, orgName };
  1270. }
  1271. /** 身份证有效期 → YYYY-MM-DD(与 subjectInfo 一致) */
  1272. function splitIdCardValidPeriod(raw: string): { begin: string; end: string } {
  1273. const s = (raw || "").trim();
  1274. if (!s) return { begin: "", end: "" };
  1275. if (/长期|长期有效/.test(s)) return { begin: "", end: "长期" };
  1276. const norm = s
  1277. .replace(/年|月|日/g, ".")
  1278. .replace(/\.+/g, ".")
  1279. .trim();
  1280. const parts = norm.split(/[-至~~]/);
  1281. const normOne = (p: string) => {
  1282. const m = p.trim().match(/(\d{4})\D*(\d{1,2})\D*(\d{1,2})/);
  1283. if (m) return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
  1284. return p.trim();
  1285. };
  1286. if (parts.length >= 2) {
  1287. return { begin: normOne(parts[0]), end: normOne(parts[1]) };
  1288. }
  1289. return { begin: "", end: "" };
  1290. }
  1291. /** 与 subjectInfo:身份证 OCR 响应解析 */
  1292. function pickIdCardPortraitOcrFields(res: any): {
  1293. name: string;
  1294. idNumber: string;
  1295. idCardAddress: string;
  1296. cardPeriodBegin: string;
  1297. cardPeriodEnd: string;
  1298. } {
  1299. const raw = res?.data ?? res;
  1300. let node: any = raw;
  1301. if (node && typeof node === "object" && "data" in node && node.data !== undefined) {
  1302. node = node.data;
  1303. }
  1304. const arr = Array.isArray(node) ? node : [node];
  1305. const item = arr[0];
  1306. const item2 = arr[1];
  1307. if (!item || typeof item !== "object") {
  1308. return { name: "", idNumber: "", idCardAddress: "", cardPeriodBegin: "", cardPeriodEnd: "" };
  1309. }
  1310. let name = String(item.name ?? item.userName ?? "").trim();
  1311. let idNumber = String(item.idNumber ?? item.id_number ?? item.idCard ?? item.id_card ?? "").trim();
  1312. const faceData = item.face?.data || item2?.face?.data;
  1313. const backData = item.back?.data || item2?.back?.data;
  1314. if (faceData && typeof faceData === "object") {
  1315. if (!name) name = String(faceData.name ?? "").trim();
  1316. if (!idNumber) idNumber = String(faceData.idNumber ?? faceData.id_number ?? "").trim();
  1317. }
  1318. let idCardAddress = "";
  1319. if (backData && typeof backData === "object") {
  1320. idCardAddress = String(backData.address ?? backData.registeredAddress ?? backData.issueAuthority ?? "").trim();
  1321. }
  1322. if (faceData && typeof faceData === "object" && !idCardAddress) {
  1323. idCardAddress = String(faceData.address ?? "").trim();
  1324. }
  1325. const faceKv = item.face?.prism_keyValueInfo || item2?.face?.prism_keyValueInfo;
  1326. if (Array.isArray(faceKv)) {
  1327. for (const row of faceKv) {
  1328. if (row?.key === "name" && row?.value && !name) name = String(row.value).trim();
  1329. if (row?.key === "idNumber" && row?.value && !idNumber) idNumber = String(row.value).trim();
  1330. if (row?.key === "address" && row?.value && !idCardAddress) {
  1331. idCardAddress = String(row.value).trim();
  1332. }
  1333. }
  1334. }
  1335. const backKv = item.back?.prism_keyValueInfo || item2?.back?.prism_keyValueInfo;
  1336. let validPeriodFromKv = "";
  1337. if (Array.isArray(backKv)) {
  1338. for (const row of backKv) {
  1339. if (row?.key === "address" && row?.value && !idCardAddress) {
  1340. idCardAddress = String(row.value).trim();
  1341. }
  1342. if (row?.value) {
  1343. const keyRaw = String(row.key || "");
  1344. if (/有效期限|有效期|失效日期|签发日期|validPeriod|validDate|valid_period/i.test(keyRaw)) {
  1345. const v = String(row.value).trim();
  1346. if (v && !validPeriodFromKv) validPeriodFromKv = v;
  1347. }
  1348. }
  1349. }
  1350. }
  1351. let vp = String(
  1352. (backData && (backData.validPeriod ?? backData.validDate)) ||
  1353. (faceData && (faceData.validPeriod ?? faceData.validDate)) ||
  1354. (item && (item.validPeriod ?? item.validDate)) ||
  1355. (item2 && (item2.validPeriod ?? item2.validDate)) ||
  1356. ""
  1357. ).trim();
  1358. if (!vp && validPeriodFromKv) vp = validPeriodFromKv;
  1359. const { begin: cardPeriodBegin, end: cardPeriodEnd } = splitIdCardValidPeriod(vp);
  1360. return { name, idNumber, idCardAddress, cardPeriodBegin, cardPeriodEnd };
  1361. }
  1362. function isOcrSuccess(res: any): boolean {
  1363. return res?.code === 200 || res?.code === "200";
  1364. }
  1365. /** 上传成功后按类型调用 OCR 并写入表单(与 subjectInfo 请求体一致:imageFile + ocrType) */
  1366. async function runOcrAfterZfbUpload(kind: UploadKind, file: File) {
  1367. if (kind === "license") {
  1368. const formData = new FormData();
  1369. formData.append("imageFile", file, file.name || "license.jpg");
  1370. formData.append("ocrType", "BUSINESS_LICENSE");
  1371. const res: any = await getOcrRequestByBase64(formData);
  1372. if (!isOcrSuccess(res)) {
  1373. throw new Error(String(res?.msg || "营业执照识别未通过,请核对照片清晰度"));
  1374. }
  1375. const fields = pickBusinessLicenseOcrFields(res);
  1376. if (fields.creditCode) form.creditCode = fields.creditCode;
  1377. if (form.subjectType === "07") {
  1378. if (fields.companyName) {
  1379. form.householdLicenseName = fields.companyName;
  1380. form.merchantShortName = fields.companyName;
  1381. }
  1382. } else {
  1383. if (fields.companyName) form.merchantShortName = fields.companyName;
  1384. if (fields.legalPerson) form.legalName = fields.legalPerson;
  1385. }
  1386. return;
  1387. }
  1388. if (kind === "institutionCert") {
  1389. const formData = new FormData();
  1390. formData.append("imageFile", file, file.name || "institution-cert.jpg");
  1391. formData.append("ocrType", "BUSINESS_LICENSE");
  1392. const res: any = await getOcrRequestByBase64(formData);
  1393. if (!isOcrSuccess(res)) {
  1394. throw new Error(String(res?.msg || "事业单位法人证书识别未通过,请核对照片清晰度"));
  1395. }
  1396. const f = pickInstitutionCertOcrFields(res);
  1397. if (f.certNumber) form.institutionCertNumber = f.certNumber;
  1398. if (f.orgName) form.merchantShortName = f.orgName;
  1399. return;
  1400. }
  1401. if (kind === "idFront") {
  1402. const formData = new FormData();
  1403. formData.append("imageFile", file, file.name || "id-portrait.jpg");
  1404. formData.append("ocrType", "ID_CARD");
  1405. const res: any = await getOcrRequestByBase64(formData);
  1406. if (!isOcrSuccess(res)) {
  1407. throw new Error(String(res?.msg || "身份证人像面识别未通过,请核对照片清晰度"));
  1408. }
  1409. const f = pickIdCardPortraitOcrFields(res);
  1410. if (f.name) form.legalName = f.name;
  1411. if (f.idNumber) form.idNumber = f.idNumber;
  1412. if (f.cardPeriodBegin && f.cardPeriodEnd) {
  1413. form.businessTermRange = [f.cardPeriodBegin, f.cardPeriodEnd];
  1414. }
  1415. return;
  1416. }
  1417. if (kind === "idBack") {
  1418. const formData = new FormData();
  1419. formData.append("imageFile", file, file.name || "id-emblem.jpg");
  1420. formData.append("ocrType", "ID_CARD");
  1421. const res: any = await getOcrRequestByBase64(formData);
  1422. if (!isOcrSuccess(res)) {
  1423. throw new Error(String(res?.msg || "身份证国徽面识别未通过,请核对照片清晰度"));
  1424. }
  1425. const f = pickIdCardPortraitOcrFields(res);
  1426. if (f.cardPeriodBegin && f.cardPeriodEnd) {
  1427. form.businessTermRange = [f.cardPeriodBegin, f.cardPeriodEnd];
  1428. } else if (!f.cardPeriodBegin && !f.cardPeriodEnd) {
  1429. throw new Error("未识别到有效期限,请确认国徽面照片清晰完整");
  1430. }
  1431. }
  1432. }
  1433. function urlFieldFor(kind: UploadKind): keyof typeof form {
  1434. const map: Record<UploadKind, keyof typeof form> = {
  1435. license: "licenseUrl",
  1436. idFront: "idFrontUrl",
  1437. idBack: "idBackUrl",
  1438. storefront: "storefrontUrl",
  1439. interior: "interiorUrl",
  1440. institutionCert: "institutionCertUrl",
  1441. otherOrgCert: "otherOrgCertUrl",
  1442. socialCert: "socialCertUrl",
  1443. govCert: "govCertUrl"
  1444. };
  1445. return map[kind];
  1446. }
  1447. function fileListForUploadKind(kind: UploadKind): UploadUserFile[] {
  1448. const map: Record<UploadKind, UploadUserFile[]> = {
  1449. license: form.licenseFileList,
  1450. idFront: form.idFrontFileList,
  1451. idBack: form.idBackFileList,
  1452. storefront: form.storefrontFileList,
  1453. interior: form.interiorFileList,
  1454. institutionCert: form.institutionCertFileList,
  1455. otherOrgCert: form.otherOrgCertFileList,
  1456. socialCert: form.socialCertFileList,
  1457. govCert: form.govCertFileList
  1458. };
  1459. return map[kind] ?? [];
  1460. }
  1461. function setFileListForUploadKind(kind: UploadKind, list: UploadUserFile[]) {
  1462. if (kind === "license") form.licenseFileList = list;
  1463. else if (kind === "idFront") form.idFrontFileList = list;
  1464. else if (kind === "idBack") form.idBackFileList = list;
  1465. else if (kind === "storefront") form.storefrontFileList = list;
  1466. else if (kind === "interior") form.interiorFileList = list;
  1467. else if (kind === "institutionCert") form.institutionCertFileList = list;
  1468. else if (kind === "otherOrgCert") form.otherOrgCertFileList = list;
  1469. else if (kind === "socialCert") form.socialCertFileList = list;
  1470. else if (kind === "govCert") form.govCertFileList = list;
  1471. }
  1472. async function handleSingleUpload(options: UploadRequestOptions, kind: UploadKind) {
  1473. const uploadFileItem = options.file as UploadUserFile;
  1474. const uid = uploadFileItem.uid;
  1475. const raw = uploadFileItem.raw || uploadFileItem;
  1476. const file = raw instanceof File ? raw : null;
  1477. const field = urlFieldFor(kind);
  1478. if (!file) {
  1479. setFileListForUploadKind(kind, filterOutUploadUserFileByUid(fileListForUploadKind(kind), uid));
  1480. options.onError(new Error("无效文件") as any);
  1481. return;
  1482. }
  1483. uploadFileItem.status = "uploading";
  1484. try {
  1485. if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
  1486. setFileListForUploadKind(kind, filterOutUploadUserFileByUid(fileListForUploadKind(kind), uid));
  1487. ElMessage.error("文件大小不能超过 20M");
  1488. options.onError(new Error("文件大小不能超过 20M") as any);
  1489. return;
  1490. }
  1491. const needOcr = kind === "license" || kind === "idFront" || kind === "idBack" || kind === "institutionCert";
  1492. const { fileUrl } = needOcr
  1493. ? await uploadBusinessInfoImageWithPageOcr(file, f => runOcrAfterZfbUpload(kind, f), {
  1494. showUploadOverlay: true
  1495. })
  1496. : await uploadBusinessInfoImageToOss(file, { showUploadOverlay: true });
  1497. const resolvedImageId = extractAlipayFilenameFromImageUrl(fileUrl) || fileUrl;
  1498. if (needOcr) {
  1499. nextTick(() => {
  1500. const f = formRef.value;
  1501. if (!f) return;
  1502. if (kind === "license") {
  1503. f.validateField("creditCode").catch(() => {});
  1504. f.validateField("merchantShortName").catch(() => {});
  1505. if (form.subjectType === "07") {
  1506. f.validateField("householdLicenseName").catch(() => {});
  1507. } else {
  1508. f.validateField("legalName").catch(() => {});
  1509. }
  1510. } else if (kind === "institutionCert") {
  1511. f.validateField("institutionCertNumber").catch(() => {});
  1512. f.validateField("merchantShortName").catch(() => {});
  1513. } else if (kind === "idFront") {
  1514. f.validateField("legalName").catch(() => {});
  1515. f.validateField("idNumber").catch(() => {});
  1516. f.validateField("businessTermRange").catch(() => {});
  1517. } else if (kind === "idBack") {
  1518. f.validateField("businessTermRange").catch(() => {});
  1519. }
  1520. });
  1521. }
  1522. uploadFileItem.status = "success";
  1523. uploadFileItem.url = fileUrl;
  1524. uploadFileItem.response = { media_id: resolvedImageId, url: fileUrl };
  1525. (form as any)[field] = fileUrl;
  1526. if (kind === "storefront") {
  1527. form.storefrontImageId = resolvedImageId;
  1528. }
  1529. if (kind === "interior") {
  1530. form.interiorImageId = resolvedImageId;
  1531. }
  1532. if (kind === "license") {
  1533. form.licenseImageId = resolvedImageId;
  1534. }
  1535. options.onSuccess({ url: fileUrl } as any);
  1536. } catch (err) {
  1537. failBusinessInfoUploadCleanup({
  1538. fileList: fileListForUploadKind(kind),
  1539. setFileList: list => setFileListForUploadKind(kind, list),
  1540. uid,
  1541. onClear: () => clearZfbUploadFieldsForKind(kind),
  1542. error: err
  1543. });
  1544. options.onError(err as any);
  1545. }
  1546. formRef.value?.validateField(field as string).catch(() => {});
  1547. }
  1548. function clearZfbUploadFieldsForKind(kind: UploadKind) {
  1549. const field = urlFieldFor(kind);
  1550. (form as any)[field] = "";
  1551. if (kind === "storefront") form.storefrontImageId = "";
  1552. if (kind === "interior") form.interiorImageId = "";
  1553. if (kind === "license") form.licenseImageId = "";
  1554. }
  1555. function onSingleRemove(fileList: UploadUserFile[], kind: UploadKind) {
  1556. const field = urlFieldFor(kind);
  1557. if (!fileList.length) {
  1558. (form as any)[field] = "";
  1559. if (kind === "storefront") form.storefrontImageId = "";
  1560. if (kind === "interior") form.interiorImageId = "";
  1561. if (kind === "license") form.licenseImageId = "";
  1562. } else {
  1563. const first = fileList[0] as UploadUserFile & {
  1564. url?: string;
  1565. response?: { url?: string; media_id?: string };
  1566. };
  1567. (form as any)[field] = first?.url || first?.response?.url || "";
  1568. if (kind === "storefront") {
  1569. form.storefrontImageId = String(first?.response?.media_id || "").trim();
  1570. }
  1571. if (kind === "interior") {
  1572. form.interiorImageId = String(first?.response?.media_id || "").trim();
  1573. }
  1574. if (kind === "license") {
  1575. form.licenseImageId = String(first?.response?.media_id || "").trim();
  1576. }
  1577. }
  1578. formRef.value?.validateField(field as string).catch(() => {});
  1579. }
  1580. /** alipayJson.service 仅传接入类型的 value(当面付 / App支付),不把 face_to_face、app 等内部码传给接口 */
  1581. function serviceValuesForAlipayPayload(selected: string[]): string[] {
  1582. const codeToValue: Record<string, string> = {
  1583. face_to_face: "当面付",
  1584. app: "App支付"
  1585. };
  1586. const allowed = new Set(serviceTypeOptions.map(o => o.value));
  1587. const out: string[] = [];
  1588. const seen = new Set<string>();
  1589. for (const s of selected) {
  1590. const v = codeToValue[s] ?? (allowed.has(s) ? s : "");
  1591. if (v && !seen.has(v)) {
  1592. seen.add(v);
  1593. out.push(v);
  1594. }
  1595. }
  1596. return out;
  1597. }
  1598. /** 店铺地址写入 alipayJson;行政区划码由省前缀、市 adCode 推导(仅省/市二级时 DistrictCode 与 CityCode 相同) */
  1599. function buildBusinessAddressForAlipayJson():
  1600. | {
  1601. address: string;
  1602. districtCode: string;
  1603. cityCode: string;
  1604. provinceCode: string;
  1605. }
  1606. | undefined {
  1607. const region = form.region;
  1608. if (!Array.isArray(region) || region.length < 2) return undefined;
  1609. const provPrefix = String(region[0] ?? "").replace(/\D/g, "");
  1610. const cityAd = String(region[1] ?? "").replace(/\D/g, "");
  1611. if (!provPrefix || cityAd.length < 6) return undefined;
  1612. const prov = provinceCityOptions.find(p => p.value === region[0]);
  1613. const provinceName = prov?.label ?? "";
  1614. const cityName = prov?.cities.find(c => c.value === region[1])?.label ?? "";
  1615. const detail = String(form.detailAddress || "").trim();
  1616. const address = `${provinceName}${cityName}${detail}`.trim();
  1617. const provinceCode = provPrefix.padEnd(6, "0").slice(0, 6);
  1618. const cityCode = cityAd.padStart(6, "0").slice(0, 6);
  1619. const districtCode = cityCode;
  1620. return { address, districtCode, cityCode, provinceCode };
  1621. }
  1622. /** 进件用图片标识:支付宝 image_id 或 OSS 地址中的文件名(*.jpg / *.png) */
  1623. function alipayImageIdFromUploadOnly(imageIdField: string): string {
  1624. const s = String(imageIdField || "").trim();
  1625. if (!s) return "";
  1626. if (/^https?:\/\//i.test(s)) {
  1627. return extractAlipayFilenameFromImageUrl(s) || "";
  1628. }
  1629. const lower = s.toLowerCase();
  1630. if (lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png")) return s;
  1631. return "";
  1632. }
  1633. /** 进件接口要求数组:有图为单元素数组,无图为 [] */
  1634. function alipayImageIdArrayForPayload(imageIdField: string): string[] {
  1635. const id = alipayImageIdFromUploadOnly(imageIdField);
  1636. return id ? [id] : [];
  1637. }
  1638. /** 从 localStorage 读取的 alipayJson 可能是对象或历史字符串 */
  1639. function normalizeAlipayJsonFromStorage(raw: unknown): Record<string, unknown> | null {
  1640. if (raw == null || raw === "") return null;
  1641. if (typeof raw === "string") {
  1642. try {
  1643. const o = JSON.parse(raw) as unknown;
  1644. return typeof o === "object" && o !== null && !Array.isArray(o) ? (o as Record<string, unknown>) : null;
  1645. } catch {
  1646. return null;
  1647. }
  1648. }
  1649. if (typeof raw === "object" && !Array.isArray(raw)) return raw as Record<string, unknown>;
  1650. return null;
  1651. }
  1652. function findCategoryPathByMcc(mcc: string): string[] | undefined {
  1653. const m = String(mcc || "").trim();
  1654. if (!m) return undefined;
  1655. for (const p of categoryCascaderOptions) {
  1656. const ch = p.children?.find(c => c.value === m);
  1657. if (ch) return [p.value, ch.value];
  1658. }
  1659. return undefined;
  1660. }
  1661. function firstImageIdFromAlipayField(val: unknown): string {
  1662. if (Array.isArray(val) && val.length > 0) return String(val[0] ?? "").trim();
  1663. if (typeof val === "string") return val.trim();
  1664. return "";
  1665. }
  1666. /** 将 businessAddress 反填为省市区 cascader + 详细地址(与 buildBusinessAddressForAlipayJson 对称) */
  1667. function regionAndDetailFromBusinessAddress(ba: unknown): { region: string[]; detailAddress: string } | undefined {
  1668. if (!ba || typeof ba !== "object") return undefined;
  1669. const o = ba as Record<string, unknown>;
  1670. const provinceCode = String(o.provinceCode ?? "").replace(/\D/g, "");
  1671. const cityCode = String(o.cityCode ?? "").replace(/\D/g, "");
  1672. const address = String(o.address ?? "").trim();
  1673. if (provinceCode.length < 2 || cityCode.length < 6) return undefined;
  1674. const prefix = provinceCode.slice(0, 2);
  1675. const prov = provinceCityOptions.find(p => p.value === prefix);
  1676. if (!prov) return undefined;
  1677. const normCity = cityCode.padStart(6, "0").slice(0, 6);
  1678. const city = prov.cities.find(c => {
  1679. const ad = String(c.value ?? "").replace(/\D/g, "");
  1680. const tail = ad.length >= 6 ? ad.slice(-6) : ad;
  1681. return tail.padStart(6, "0") === normCity;
  1682. });
  1683. if (!city) return undefined;
  1684. let detail = address;
  1685. const prefixStr = `${prov.label}${city.label}`;
  1686. if (address.startsWith(prefixStr)) detail = address.slice(prefixStr.length).trim();
  1687. return { region: [prefix, city.value], detailAddress: detail };
  1688. }
  1689. /** alipayJson.service 为「当面付」「App支付」等;兼容旧内部码 */
  1690. function serviceTypesFromAlipayService(val: unknown): string[] {
  1691. const allowed = new Set(serviceTypeOptions.map(o => o.value));
  1692. const legacy: Record<string, string> = {
  1693. face_to_face: "当面付",
  1694. app: "App支付"
  1695. };
  1696. const arr = Array.isArray(val) ? val : typeof val === "string" && val.trim() ? [val] : [];
  1697. const out: string[] = [];
  1698. const seen = new Set<string>();
  1699. for (const item of arr) {
  1700. let s = String(item ?? "").trim();
  1701. if (s.toLowerCase() === "app 支付") s = "App支付";
  1702. const mapped = legacy[s] ?? s;
  1703. if (mapped && allowed.has(mapped) && !seen.has(mapped)) {
  1704. seen.add(mapped);
  1705. out.push(mapped);
  1706. }
  1707. }
  1708. return out;
  1709. }
  1710. /** 进入本页时把缓存中的 alipayJson 写回第一步表单 */
  1711. function applyAlipayJsonCacheToForm(data: Record<string, unknown>) {
  1712. const st = String(data.merchantType ?? "").trim();
  1713. if (st) form.subjectType = st;
  1714. const name = String(data.name ?? data.aliasName ?? "").trim();
  1715. if (name) form.merchantShortName = name;
  1716. const mcc = String(data.mcc ?? "").trim();
  1717. const path = findCategoryPathByMcc(mcc);
  1718. if (path) form.businessCategory = path;
  1719. const certNo = String(data.certNo ?? "").trim();
  1720. if (certNo) form.creditCode = certNo;
  1721. const certImage = String(data.certImage ?? "").trim();
  1722. if (certImage) form.licenseImageId = certImage;
  1723. const legalName = String(data.legalName ?? "").trim();
  1724. if (legalName) form.legalName = legalName;
  1725. const legalCertNo = String(data.legalCertNo ?? "").trim();
  1726. if (legalCertNo) form.idNumber = legalCertNo;
  1727. const certName = String(data.certName ?? "").trim();
  1728. if (certName && form.subjectType === "07") form.householdLicenseName = certName;
  1729. const alipayId = String(data.alipayLogonId ?? data.bindingAlipayLogonId ?? data.BindingAlipayLogonId ?? "").trim();
  1730. if (alipayId) form.settlementAlipayAccount = alipayId;
  1731. const ci = data.contactInfos;
  1732. if (Array.isArray(ci) && ci.length > 0) {
  1733. const c0 = ci[0] as Record<string, unknown>;
  1734. const cn = String(c0?.name ?? "").trim();
  1735. const cm = String(c0?.mobile ?? "").trim();
  1736. if (cn) form.contactName = cn;
  1737. if (cm) form.contactPhone = cm;
  1738. }
  1739. const svc = serviceTypesFromAlipayService(data.service);
  1740. if (svc.length) form.serviceTypes = svc;
  1741. const inId = firstImageIdFromAlipayField(data.inDoorImages);
  1742. if (inId) form.interiorImageId = inId;
  1743. const outId = firstImageIdFromAlipayField(data.outDoorImages);
  1744. if (outId) form.storefrontImageId = outId;
  1745. const ba = regionAndDetailFromBusinessAddress(data.businessAddress);
  1746. if (ba) {
  1747. form.region = ba.region;
  1748. form.detailAddress = ba.detailAddress;
  1749. }
  1750. const sites = data.sites;
  1751. if (Array.isArray(sites) && sites.length > 0) {
  1752. const s0 = sites[0] as Record<string, unknown>;
  1753. const sn = String(s0?.siteName ?? s0?.siteName ?? "").trim();
  1754. if (sn) form.appName = sn;
  1755. }
  1756. }
  1757. /** 下一步写入 localStorage,供后续支付宝进件等使用 */
  1758. function buildAlipayJsonPayload() {
  1759. const mcc =
  1760. Array.isArray(form.businessCategory) && form.businessCategory.length >= 2
  1761. ? form.businessCategory[1]
  1762. : Array.isArray(form.businessCategory) && form.businessCategory.length === 1
  1763. ? form.businessCategory[0]
  1764. : "";
  1765. const businessAddress = buildBusinessAddressForAlipayJson();
  1766. const certName = form.subjectType === "07" ? String(form.householdLicenseName || "").trim() : "";
  1767. const alipayLogonIdVal = String(form.settlementAlipayAccount || "").trim();
  1768. const hasFaceToFace = form.serviceTypes.includes("当面付");
  1769. const hasAppPay = form.serviceTypes.includes("App支付");
  1770. const siteNameForPayload = String(form.appName || "").trim() || DEFAULT_ALIPAY_APP_SITE_NAME;
  1771. return {
  1772. type: 0,
  1773. externalId: randomAlipayExternalId16(),
  1774. name: form.merchantShortName,
  1775. aliasName: form.merchantShortName,
  1776. merchantType: form.subjectType,
  1777. mcc,
  1778. certNo: form.creditCode,
  1779. certType: 201,
  1780. certImage: alipayImageIdFromUploadOnly(form.licenseImageId),
  1781. legalName: form.legalName,
  1782. legalCertNo: form.idNumber,
  1783. certName,
  1784. alipayLogonId: alipayLogonIdVal,
  1785. contactInfos: [
  1786. {
  1787. name: String(form.contactName || "").trim(),
  1788. mobile: String(form.contactPhone || "").trim()
  1789. }
  1790. ],
  1791. service: serviceValuesForAlipayPayload(form.serviceTypes),
  1792. inDoorImages: alipayImageIdArrayForPayload(form.interiorImageId),
  1793. outDoorImages: alipayImageIdArrayForPayload(form.storefrontImageId),
  1794. ...(businessAddress ? { businessAddress } : {}),
  1795. ...(hasFaceToFace && hasAppPay
  1796. ? {
  1797. sites: [{ siteType: "02", siteName: siteNameForPayload }]
  1798. }
  1799. : {})
  1800. };
  1801. }
  1802. onMounted(async () => {
  1803. const cached = normalizeAlipayJsonFromStorage(localGet(ALIPAY_JSON_KEY));
  1804. if (cached) applyAlipayJsonCacheToForm(cached);
  1805. await nextTick();
  1806. formRef.value?.clearValidate();
  1807. });
  1808. const rules: FormRules = {
  1809. subjectType: [{ required: true, message: "请选择主体类型", trigger: "change" }],
  1810. serviceTypes: [
  1811. {
  1812. type: "array",
  1813. required: true,
  1814. min: 1,
  1815. message: "请选择接入服务类型",
  1816. trigger: "change"
  1817. }
  1818. ],
  1819. licenseUrl: [
  1820. {
  1821. validator: (_r, _v, cb) => {
  1822. if (form.subjectType !== "01" && form.subjectType !== "07") return cb();
  1823. if (!String(form.licenseUrl || "").trim()) cb(new Error("请上传营业执照"));
  1824. else cb();
  1825. },
  1826. trigger: "change"
  1827. }
  1828. ],
  1829. creditCode: [
  1830. {
  1831. validator: (_r, _v, cb) => {
  1832. if (form.subjectType !== "01" && form.subjectType !== "07") return cb();
  1833. const v = String(form.creditCode || "").trim();
  1834. if (!v) {
  1835. cb(new Error(form.subjectType === "07" ? "请输入营业执照编号" : "请输入营业执照号"));
  1836. } else if (v.length < 15 || v.length > 32) cb(new Error("长度一般为 18 位统一社会信用代码"));
  1837. else cb();
  1838. },
  1839. trigger: "blur"
  1840. }
  1841. ],
  1842. householdLicenseName: [
  1843. {
  1844. validator: (_r, _v, cb) => {
  1845. if (form.subjectType !== "07") return cb();
  1846. if (!String(form.householdLicenseName || "").trim()) cb(new Error("请输入营业执照名称"));
  1847. else cb();
  1848. },
  1849. trigger: "blur"
  1850. }
  1851. ],
  1852. merchantShortName: [
  1853. {
  1854. validator: (_r, _v, cb) => {
  1855. if (!["01", "02", "03", "04", "05", "06", "07"].includes(form.subjectType)) return cb();
  1856. if (!String(form.merchantShortName || "").trim()) cb(new Error("请输入商户简称"));
  1857. else cb();
  1858. },
  1859. trigger: "blur"
  1860. }
  1861. ],
  1862. institutionCertUrl: [
  1863. {
  1864. validator: (_r, _v, cb) => {
  1865. if (form.subjectType !== "institution") return cb();
  1866. if (!String(form.institutionCertUrl || "").trim()) cb(new Error("请上传事业单位法人证书"));
  1867. else cb();
  1868. },
  1869. trigger: "change"
  1870. }
  1871. ],
  1872. institutionCertNumber: [
  1873. {
  1874. validator: (_r, _v, cb) => {
  1875. if (form.subjectType !== "institution") return cb();
  1876. if (!String(form.institutionCertNumber || "").trim()) cb(new Error("请输入事业单位法人证书编号"));
  1877. else cb();
  1878. },
  1879. trigger: "blur"
  1880. }
  1881. ],
  1882. otherOrgCertUrl: [
  1883. {
  1884. validator: (_r, _v, cb) => {
  1885. if (form.subjectType !== "other_org") return cb();
  1886. if (!String(form.otherOrgCertUrl || "").trim()) cb(new Error("请上传民办非企业单位登记证书"));
  1887. else cb();
  1888. },
  1889. trigger: "change"
  1890. }
  1891. ],
  1892. otherOrgCertNumber: [
  1893. {
  1894. validator: (_r, _v, cb) => {
  1895. if (form.subjectType !== "other_org") return cb();
  1896. if (!String(form.otherOrgCertNumber || "").trim()) cb(new Error("请输入民办非企业单位登记证书编号"));
  1897. else cb();
  1898. },
  1899. trigger: "blur"
  1900. }
  1901. ],
  1902. socialCertUrl: [
  1903. {
  1904. validator: (_r, _v, cb) => {
  1905. if (form.subjectType !== "social") return cb();
  1906. if (!String(form.socialCertUrl || "").trim()) cb(new Error("请上传社会团体法人登记证书"));
  1907. else cb();
  1908. },
  1909. trigger: "change"
  1910. }
  1911. ],
  1912. socialCertNumber: [
  1913. {
  1914. validator: (_r, _v, cb) => {
  1915. if (form.subjectType !== "social") return cb();
  1916. if (!String(form.socialCertNumber || "").trim()) cb(new Error("请输入社会团体法人登记证书编号"));
  1917. else cb();
  1918. },
  1919. trigger: "blur"
  1920. }
  1921. ],
  1922. govCertUrl: [
  1923. {
  1924. validator: (_r, _v, cb) => {
  1925. if (form.subjectType !== "gov") return cb();
  1926. if (!String(form.govCertUrl || "").trim()) {
  1927. cb(new Error("请上传党政机关批准设立文件/行政执法主体资格证"));
  1928. } else cb();
  1929. },
  1930. trigger: "change"
  1931. }
  1932. ],
  1933. govCertNumber: [
  1934. {
  1935. validator: (_r, _v, cb) => {
  1936. if (form.subjectType !== "gov") return cb();
  1937. if (!String(form.govCertNumber || "").trim()) {
  1938. cb(new Error("请输入党政机关批准设立文件/行政执法主体资格证编号"));
  1939. } else cb();
  1940. },
  1941. trigger: "blur"
  1942. }
  1943. ],
  1944. businessCategory: [
  1945. {
  1946. validator: (_r, _v, cb) => {
  1947. if (!["01", "02", "03", "04", "05", "06", "07"].includes(form.subjectType)) return cb();
  1948. const bc = form.businessCategory;
  1949. if (!Array.isArray(bc) || bc.length < 2 || !String(bc[0] || "").trim() || !String(bc[1] || "").trim()) {
  1950. cb(new Error("请选择经营类目(需选择到二级分类)"));
  1951. } else cb();
  1952. },
  1953. trigger: "change"
  1954. }
  1955. ],
  1956. legalIdType: [
  1957. {
  1958. validator: (_r, _v, cb) => {
  1959. if (!["01", "institution", "other_org", "social"].includes(form.subjectType)) return cb();
  1960. if (!String(form.legalIdType || "").trim()) cb(new Error("请选择证件类型"));
  1961. else cb();
  1962. },
  1963. trigger: "change"
  1964. }
  1965. ],
  1966. idFrontUrl: [
  1967. {
  1968. validator: (_r, _v, cb) => {
  1969. if (!["01", "institution", "other_org", "social", "individual_merchant"].includes(form.subjectType)) return cb();
  1970. if (!String(form.idFrontUrl || "").trim()) {
  1971. cb(new Error(form.subjectType === "individual_merchant" ? "请上传身份证人像面" : "请上传证件正面"));
  1972. } else cb();
  1973. },
  1974. trigger: "change"
  1975. }
  1976. ],
  1977. idBackUrl: [
  1978. {
  1979. validator: (_r, _v, cb) => {
  1980. if (!["01", "institution", "other_org", "social", "individual_merchant"].includes(form.subjectType)) return cb();
  1981. if (!String(form.idBackUrl || "").trim()) {
  1982. cb(new Error(form.subjectType === "individual_merchant" ? "请上传身份证国徽面" : "请上传证件反面"));
  1983. } else cb();
  1984. },
  1985. trigger: "change"
  1986. }
  1987. ],
  1988. idNumber: [
  1989. {
  1990. validator: (_r, _v, cb) => {
  1991. if (!["01", "institution", "other_org", "social", "individual_merchant"].includes(form.subjectType)) return cb();
  1992. if (!String(form.idNumber || "").trim()) {
  1993. cb(new Error(form.subjectType === "individual_merchant" ? "请输入个人身份证号码" : "请输入证件号"));
  1994. } else cb();
  1995. },
  1996. trigger: "blur"
  1997. }
  1998. ],
  1999. legalName: [
  2000. {
  2001. validator: (_r, _v, cb) => {
  2002. if (!["01", "institution", "other_org", "social"].includes(form.subjectType)) return cb();
  2003. if (!String(form.legalName || "").trim()) cb(new Error("请输入法定代表人/负责人姓名"));
  2004. else cb();
  2005. },
  2006. trigger: "blur"
  2007. }
  2008. ],
  2009. storefrontUrl: [{ required: true, message: "请上传门头照", trigger: "change" }],
  2010. interiorUrl: [{ required: true, message: "请上传内景照", trigger: "change" }],
  2011. region: [{ type: "array", required: true, min: 2, message: "请选择省、市", trigger: "change" }],
  2012. detailAddress: [{ required: true, message: "请输入详细地址", trigger: "blur" }],
  2013. contactName: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
  2014. contactPhone: [
  2015. { required: true, message: "请输入联系人手机号", trigger: "blur" },
  2016. { pattern: /^1\d{10}$/, message: "请输入有效手机号", trigger: "blur" }
  2017. ],
  2018. contactEmail: [
  2019. { required: true, message: "请输入联系人邮箱", trigger: "blur" },
  2020. { type: "email", message: "邮箱格式不正确", trigger: "blur" }
  2021. ],
  2022. servicePhone: [
  2023. { required: true, message: "请输入客服电话", trigger: "blur" },
  2024. { pattern: /^[\d\-+\s]{5,20}$/, message: "请输入有效电话号码", trigger: "blur" }
  2025. ],
  2026. settlementAlipayAccount: [{ required: true, message: "请输入签约支付宝账号", trigger: "blur" }]
  2027. };
  2028. async function onNext() {
  2029. if (!formRef.value) return;
  2030. nextLoading.value = true;
  2031. try {
  2032. localSet(ALIPAY_JSON_KEY, buildAlipayJsonPayload());
  2033. await router.push({ path: "/businessInfo/zfbIndexTwo", query: { ...route.query } });
  2034. } catch {
  2035. ElMessage.warning("请完善必填信息");
  2036. } finally {
  2037. nextLoading.value = false;
  2038. }
  2039. }
  2040. </script>
  2041. <style scoped lang="scss">
  2042. $zfb-blue: #1677ff;
  2043. .zfb-step-page {
  2044. box-sizing: border-box;
  2045. min-height: 100%;
  2046. padding: 24px 24px 48px;
  2047. margin: 0 auto;
  2048. background: #ffffff;
  2049. }
  2050. .step-header {
  2051. margin-right: -24px;
  2052. margin-bottom: 28px;
  2053. margin-left: -24px;
  2054. }
  2055. .zfb-steps {
  2056. width: 100%;
  2057. margin: 0;
  2058. }
  2059. .step-header :deep(.zfb-steps.el-steps--horizontal) {
  2060. .el-step__icon {
  2061. width: 36px;
  2062. height: 36px;
  2063. font-size: 16px;
  2064. font-weight: 600;
  2065. background: #ffffff !important;
  2066. }
  2067. .el-step__head.is-process .el-step__icon {
  2068. color: #ffffff !important;
  2069. background: #1677ff !important;
  2070. border-color: $zfb-blue !important;
  2071. }
  2072. .el-step__head.is-wait .el-step__icon {
  2073. color: #c0c4cc !important;
  2074. background: #ffffff !important;
  2075. border: 2px solid #dcdfe6 !important;
  2076. }
  2077. .el-step__head.is-finish .el-step__icon {
  2078. color: #ffffff !important;
  2079. background: $zfb-blue !important;
  2080. border-color: $zfb-blue !important;
  2081. }
  2082. .el-step__line {
  2083. background-color: #e4e7ed !important;
  2084. }
  2085. .el-step__line-inner {
  2086. border-width: 1px !important;
  2087. }
  2088. .el-step__title {
  2089. font-size: 14px;
  2090. line-height: 36px;
  2091. color: #606266;
  2092. }
  2093. .el-step__head.is-process + .el-step__main .el-step__title {
  2094. font-weight: 600;
  2095. color: #303133;
  2096. }
  2097. .el-step__head.is-wait + .el-step__main .el-step__title {
  2098. color: #c0c4cc;
  2099. }
  2100. .el-step__head.is-finish + .el-step__main .el-step__title {
  2101. font-weight: 500;
  2102. color: #606266;
  2103. }
  2104. }
  2105. .page-layout {
  2106. display: flex;
  2107. gap: 32px;
  2108. align-items: flex-start;
  2109. }
  2110. .main-column {
  2111. flex: 1;
  2112. min-width: 0;
  2113. /* 滚到底时 max(scrollTop) 往往仍小于「经营信息」的锚点判定线,侧栏会一直停在前一节;底部留白增大可滚动距离 */
  2114. padding-bottom: clamp(140px, 22vh, 400px);
  2115. }
  2116. .anchor-column {
  2117. flex-shrink: 0;
  2118. width: 160px;
  2119. }
  2120. .anchor-card {
  2121. padding: 16px 12px;
  2122. }
  2123. :deep(.el-anchor__list) {
  2124. border-left: 1px solid #ebeef5;
  2125. }
  2126. .anchor-title {
  2127. margin-bottom: 12px;
  2128. font-size: 13px;
  2129. font-weight: 600;
  2130. color: #303133;
  2131. }
  2132. .zfb-form {
  2133. :deep(.el-form-item__label) {
  2134. font-weight: 500;
  2135. color: #606266;
  2136. }
  2137. }
  2138. .form-block {
  2139. padding-bottom: 8px;
  2140. margin-bottom: 28px;
  2141. scroll-margin-top: 72px;
  2142. border-bottom: 1px solid #ebeef5;
  2143. &:last-of-type {
  2144. border-bottom: none;
  2145. }
  2146. }
  2147. .section-head {
  2148. display: flex;
  2149. align-items: center;
  2150. margin: 0 0 16px;
  2151. font-size: 16px;
  2152. font-weight: 600;
  2153. color: #303133;
  2154. }
  2155. .section-bar {
  2156. display: inline-block;
  2157. flex-shrink: 0;
  2158. width: 4px;
  2159. height: 16px;
  2160. margin-right: 8px;
  2161. background: $zfb-blue;
  2162. border-radius: 2px;
  2163. }
  2164. .radio-stack {
  2165. display: flex;
  2166. gap: 10px;
  2167. align-items: flex-start;
  2168. }
  2169. .label-with-icon {
  2170. display: inline-flex;
  2171. gap: 4px;
  2172. align-items: center;
  2173. }
  2174. .label-wrap-multiline {
  2175. display: inline-block;
  2176. max-width: 196px;
  2177. line-height: 1.45;
  2178. white-space: normal;
  2179. vertical-align: middle;
  2180. }
  2181. .individual-merchant-alert {
  2182. max-width: 410px;
  2183. margin-bottom: 20px;
  2184. margin-left: 200px;
  2185. }
  2186. .info-icon {
  2187. color: #909399;
  2188. cursor: help;
  2189. }
  2190. .license-upload-wrap {
  2191. display: flex;
  2192. flex-wrap: wrap;
  2193. gap: 16px;
  2194. align-items: flex-start;
  2195. }
  2196. .license-example {
  2197. display: flex;
  2198. flex-direction: column;
  2199. gap: 6px;
  2200. }
  2201. .license-example-label {
  2202. font-size: 12px;
  2203. color: #909399;
  2204. }
  2205. .license-example-thumb {
  2206. position: relative;
  2207. display: flex;
  2208. align-items: center;
  2209. justify-content: center;
  2210. width: 148px;
  2211. height: 148px;
  2212. overflow: hidden;
  2213. background: #f5f7fa;
  2214. border: 1px dashed #dcdfe6;
  2215. border-radius: 6px;
  2216. }
  2217. .license-example-placeholder {
  2218. font-size: 12px;
  2219. color: #c0c4cc;
  2220. }
  2221. .id-example-thumb {
  2222. position: relative;
  2223. }
  2224. .license-example-badge {
  2225. position: absolute;
  2226. top: 6px;
  2227. right: 6px;
  2228. padding: 2px 6px;
  2229. font-size: 11px;
  2230. color: #ffffff;
  2231. background: rgb(0 0 0 / 45%);
  2232. border-radius: 4px;
  2233. }
  2234. .id-upload-row {
  2235. display: flex;
  2236. flex-wrap: wrap;
  2237. gap: 24px;
  2238. }
  2239. .id-upload-cell {
  2240. display: flex;
  2241. flex-direction: column;
  2242. gap: 8px;
  2243. }
  2244. .id-upload-trigger {
  2245. display: flex;
  2246. flex-direction: column;
  2247. gap: 6px;
  2248. align-items: center;
  2249. justify-content: center;
  2250. line-height: 1.2;
  2251. }
  2252. .id-side-label {
  2253. margin-top: 10px;
  2254. font-size: 13px;
  2255. color: #606266;
  2256. }
  2257. .id-upload-trigger .id-side-label {
  2258. font-size: 12px;
  2259. line-height: 1.3;
  2260. text-align: center;
  2261. }
  2262. .upload-rules {
  2263. margin-top: 12px;
  2264. font-size: 12px;
  2265. line-height: 1.65;
  2266. color: #909399;
  2267. p {
  2268. margin: 0;
  2269. }
  2270. }
  2271. .field-stack {
  2272. width: 100%;
  2273. }
  2274. .field-tip {
  2275. margin: 8px 0 0;
  2276. font-size: 12px;
  2277. line-height: 1.65;
  2278. color: #909399;
  2279. }
  2280. .service-checks {
  2281. display: flex;
  2282. flex-wrap: wrap;
  2283. gap: 12px 20px;
  2284. }
  2285. .address-row {
  2286. display: flex;
  2287. flex-wrap: wrap;
  2288. gap: 12px;
  2289. align-items: center;
  2290. width: 100%;
  2291. max-width: 720px;
  2292. }
  2293. .address-detail {
  2294. flex: 1;
  2295. min-width: 200px;
  2296. }
  2297. .footer-actions {
  2298. display: flex;
  2299. flex-wrap: wrap;
  2300. gap: 16px;
  2301. align-items: center;
  2302. justify-content: center;
  2303. padding-top: 24px;
  2304. margin-top: 8px;
  2305. border-top: 1px solid #ebeef5;
  2306. }
  2307. .btn-primary-zfb {
  2308. min-width: 120px;
  2309. background: $zfb-blue;
  2310. border-color: $zfb-blue;
  2311. &:hover {
  2312. background: #4096ff;
  2313. border-color: #4096ff;
  2314. }
  2315. }
  2316. :deep(.el-upload--picture-card) {
  2317. --el-upload-picture-card-size: 148px;
  2318. }
  2319. :deep(.el-upload-list--picture-card .el-upload-list__item) {
  2320. width: 148px;
  2321. height: 148px;
  2322. }
  2323. :deep(.el-form-item__content) {
  2324. display: block;
  2325. }
  2326. :deep(.anchor-card .el-anchor) {
  2327. /* 实际滚动在 layout 的 el-main 上,不设 container 时监听 window,高亮不会随滚动更新 */
  2328. --el-anchor-color: #606266;
  2329. --el-anchor-active-color: #{$zfb-blue};
  2330. --el-anchor-marker-bg-color: #{$zfb-blue};
  2331. background: transparent;
  2332. }
  2333. :deep(.anchor-card .el-anchor__link) {
  2334. padding: 6px 0;
  2335. font-size: 13px;
  2336. }
  2337. :deep(.anchor-card .el-anchor__link.is-active) {
  2338. font-weight: 600;
  2339. }
  2340. </style>