巴青农资商城

entry-apply.vue 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <template>
  2. <view class="entry-apply-wrap">
  3. <safe-nav-bar title="商家入驻" />
  4. <view class="entry-apply-page">
  5. <view v-if="!entryOpen" class="entry-closed">
  6. <text>{{ agreement.message || '商家入驻暂未开放' }}</text>
  7. </view>
  8. <view v-else-if="blocked" class="entry-closed">
  9. <text>您有待审核或公示中的申请,请先在「我的入驻申请」查看进度</text>
  10. <button class="mine-btn-outline entry-closed__btn" @click="goList">查看申请</button>
  11. </view>
  12. <view v-else-if="shopOnlyBlocked" class="entry-closed">
  13. <text>{{ entryContext.blockReason || '当前无法申请开设新店铺' }}</text>
  14. <button class="mine-btn-outline entry-closed__btn" @click="goList">查看申请</button>
  15. </view>
  16. <template v-else>
  17. <view v-if="shopOnlyMode" class="entry-shop-only-banner">
  18. <text class="entry-shop-only-banner__title">仅开店铺模式</text>
  19. <text class="entry-shop-only-banner__tip">您已是商户管理员,本次仅需填写拟开设店铺信息,主体与经营信息沿用已有商户。</text>
  20. <text v-if="entryContext.merchantName" class="entry-shop-only-banner__line">商户名称:{{ entryContext.merchantName }}</text>
  21. <text v-if="entryContext.subjectLabel" class="entry-shop-only-banner__line">主体:{{ entryContext.subjectLabel }}</text>
  22. </view>
  23. <view class="entry-steps">
  24. <text
  25. v-for="(s, i) in stepTitles"
  26. :key="s"
  27. :class="['entry-steps__item', { 'entry-steps__item--on': stepIndex === i }]"
  28. >{{ s }}</text>
  29. </view>
  30. <scroll-view class="entry-scroll" scroll-y>
  31. <!-- 主体类型(新主体申请) -->
  32. <view v-if="!shopOnlyMode && currentStep === 'type'" class="form-card">
  33. <view
  34. :class="['type-card', { 'type-card--on': form.merchantType === '1' }]"
  35. @click="selectType('1')"
  36. >
  37. <text class="type-card__title">个人入驻</text>
  38. <text class="type-card__sub">身份证 + 个人账户</text>
  39. </view>
  40. <view
  41. :class="['type-card', { 'type-card--on': form.merchantType === '2' }]"
  42. @click="selectType('2')"
  43. >
  44. <text class="type-card__title">企业入驻</text>
  45. <text class="type-card__sub">法人 + 企业对公账户</text>
  46. </view>
  47. </view>
  48. <!-- 个人:主体信息 -->
  49. <view v-else-if="isPerson && currentStep === 'subject'" class="form-card">
  50. <entry-person-subject :subject="form.subject" />
  51. </view>
  52. <view v-else-if="isPerson && currentStep === 'biz'" class="form-card">
  53. <entry-person-biz :biz="form.biz" :region="regionBiz" @update:region="onBizRegion" />
  54. </view>
  55. <!-- 企业 -->
  56. <view v-else-if="!isPerson && currentStep === 'subject'" class="form-card">
  57. <entry-enterprise-subject
  58. :subject="form.subject"
  59. :region-reg="regionReg"
  60. @update:region-reg="onRegRegion"
  61. />
  62. </view>
  63. <view v-else-if="!isPerson && currentStep === 'biz'" class="form-card">
  64. <entry-enterprise-biz :biz="form.biz" :region="regionBiz" @update:region="onBizRegion" />
  65. </view>
  66. <!-- 店铺 -->
  67. <view v-else-if="currentStep === 'shop'" class="form-card">
  68. <entry-shop-fields :shop="form.shop" />
  69. </view>
  70. <!-- 提交 -->
  71. <view v-else-if="currentStep === 'submit'" class="form-card">
  72. <text class="submit-tip">请确认信息无误后勾选协议并提交</text>
  73. <agreement-block
  74. v-model="form.agreementAccepted"
  75. :enabled="agreement.enabled"
  76. :checkbox-label="agreement.checkboxLabel"
  77. :agreement-title="agreement.agreementTitle"
  78. :version-label="agreement.versionLabel"
  79. :content="agreement.content"
  80. />
  81. </view>
  82. </scroll-view>
  83. <view class="entry-actions">
  84. <button v-if="stepIndex > 0" class="mine-btn-outline entry-actions__prev" @click="prevStep">
  85. 上一步
  86. </button>
  87. <button class="form-footer__btn entry-actions__next" :disabled="submitting" @click="nextStep">
  88. {{ nextBtnText }}
  89. </button>
  90. </view>
  91. </template>
  92. </view>
  93. </view>
  94. </template>
  95. <script setup>
  96. import { ref, reactive, computed } from 'vue'
  97. import SafeNavBar from '@/components/common/SafeNavBar.vue'
  98. import { onLoad } from '@dcloudio/uni-app'
  99. import { getEntryAgreement, getEntryStatus, getMyEntryApplies, getEntryContext, submitEntryApply } from '@/api/merchantEntry'
  100. import { ensureApiToken } from '@/utils/apiAuth'
  101. import { hasBlockingApply } from '@/utils/entryConstants'
  102. import {
  103. createEntryForm,
  104. validateEntryStep,
  105. buildEntrySubmitPayload,
  106. buildShopOnlySubmitPayload,
  107. applyRegion,
  108. applyRegRegion
  109. } from '@/utils/entryForm'
  110. import { MERCHANT_TYPE_PERSON } from '@/utils/entryConstants'
  111. import AgreementBlock from '@/components/account/AgreementBlock.vue'
  112. import EntryPersonSubject from '@/components/mine/entry/EntryPersonSubject.vue'
  113. import EntryPersonBiz from '@/components/mine/entry/EntryPersonBiz.vue'
  114. import EntryEnterpriseSubject from '@/components/mine/entry/EntryEnterpriseSubject.vue'
  115. import EntryEnterpriseBiz from '@/components/mine/entry/EntryEnterpriseBiz.vue'
  116. import EntryShopFields from '@/components/mine/entry/EntryShopFields.vue'
  117. import { PAGE_ENTRY_LIST, PAGE_PROFILE } from '@/utils/pageRoute'
  118. import { useUserStore } from '@/store/user'
  119. import { useActionGuard } from '@/utils/actionGuard'
  120. const submitGuard = useActionGuard()
  121. const submitting = submitGuard.locked
  122. const form = reactive(createEntryForm(MERCHANT_TYPE_PERSON))
  123. const agreement = reactive({
  124. enabled: false,
  125. message: '',
  126. agreementTitle: '',
  127. versionLabel: '',
  128. content: '',
  129. checkboxLabel: '我已阅读并同意《商城入驻协议》'
  130. })
  131. const stepIndex = ref(0)
  132. const entryOpen = ref(true)
  133. const blocked = ref(false)
  134. const entryContext = ref({
  135. shopOnlyMode: false,
  136. canShopOnlyApply: false,
  137. blockReason: '',
  138. merchantId: null,
  139. merchantType: '',
  140. subjectLabel: '',
  141. merchantName: ''
  142. })
  143. const regionBiz = ref({ regionCode: '', regionName: '', pathCodes: [] })
  144. const regionReg = ref({ regionCode: '', regionName: '', pathCodes: [] })
  145. const userStore = useUserStore()
  146. /** 可仅填店铺提交(路径 A2) */
  147. const shopOnlyMode = computed(() => !!entryContext.value.shopOnlyMode && !!entryContext.value.canShopOnlyApply)
  148. /** 已是商户管理员但不可仅开店铺 */
  149. const shopOnlyBlocked = computed(() => !!entryContext.value.shopOnlyMode && !entryContext.value.canShopOnlyApply)
  150. const stepKeys = computed(() => {
  151. if (shopOnlyMode.value) return ['shop', 'submit']
  152. return ['type', 'subject', 'biz', 'shop', 'submit']
  153. })
  154. const stepTitles = computed(() => {
  155. if (shopOnlyMode.value) return ['店铺', '提交']
  156. return ['类型', '主体', '经营', '店铺', '提交']
  157. })
  158. const currentStep = computed(() => stepKeys.value[stepIndex.value])
  159. const isPerson = computed(() => form.merchantType === MERCHANT_TYPE_PERSON)
  160. const nextBtnText = computed(() => {
  161. if (submitting.value) return '提交中…'
  162. return currentStep.value === 'submit' ? '提交申请' : '下一步'
  163. })
  164. onLoad(() => {
  165. if (!ensureApiToken()) return
  166. initPage()
  167. })
  168. function initPage() {
  169. Promise.all([getEntryAgreement(), getEntryStatus(), getMyEntryApplies(), getEntryContext()])
  170. .then(([agRes, stRes, myRes, ctxRes]) => {
  171. const ag = agRes.data || {}
  172. agreement.enabled = !!ag.enabled
  173. agreement.message = ag.message || ''
  174. agreement.agreementTitle = ag.agreementTitle || '商城入驻协议'
  175. agreement.versionLabel = ag.versionLabel || ''
  176. agreement.content = ag.content || ''
  177. agreement.checkboxLabel = ag.checkboxLabel || agreement.checkboxLabel
  178. entryOpen.value = stRes.data?.entryOpen !== false && agreement.enabled
  179. blocked.value = hasBlockingApply(myRes.data || [])
  180. entryContext.value = {
  181. shopOnlyMode: !!ctxRes.data?.shopOnlyMode,
  182. canShopOnlyApply: !!ctxRes.data?.canShopOnlyApply,
  183. blockReason: ctxRes.data?.blockReason || '',
  184. merchantId: ctxRes.data?.merchantId,
  185. merchantType: ctxRes.data?.merchantType || '',
  186. subjectLabel: ctxRes.data?.subjectLabel || '',
  187. merchantName: ctxRes.data?.merchantName || ''
  188. }
  189. if (shopOnlyMode.value) {
  190. form.merchantType = entryContext.value.merchantType || MERCHANT_TYPE_PERSON
  191. form.shop = createEntryForm(form.merchantType).shop
  192. form.agreementAccepted = false
  193. stepIndex.value = 0
  194. }
  195. if (entryOpen.value && !blocked.value && !shopOnlyBlocked.value) {
  196. ensureMemberNickName()
  197. }
  198. })
  199. .catch(() => {})
  200. }
  201. /** 入驻须会员昵称非空(经营账号登录名) */
  202. function ensureMemberNickName() {
  203. const cached = (userStore.state.nickName || '').trim()
  204. if (cached) return Promise.resolve(true)
  205. return userStore.fetchUserInfo().then(() => {
  206. const nick = (userStore.state.nickName || '').trim()
  207. if (nick) return true
  208. uni.showModal({
  209. title: '提示',
  210. content: '请先完善个人资料中的昵称,方可提交入驻申请',
  211. confirmText: '去完善',
  212. success: (r) => {
  213. if (r.confirm) uni.navigateTo({ url: PAGE_PROFILE })
  214. }
  215. })
  216. return false
  217. })
  218. .catch(() => false)
  219. }
  220. function selectType(type) {
  221. form.merchantType = type
  222. const next = createEntryForm(type)
  223. form.subject = next.subject
  224. form.biz = next.biz
  225. form.shop = next.shop
  226. form.agreementAccepted = false
  227. regionBiz.value = { regionCode: '', regionName: '', pathCodes: [] }
  228. regionReg.value = { regionCode: '', regionName: '', pathCodes: [] }
  229. }
  230. function onBizRegion(v) {
  231. regionBiz.value = v
  232. applyRegion(form.biz, v)
  233. }
  234. function onRegRegion(v) {
  235. regionReg.value = v
  236. applyRegRegion(form.subject, v)
  237. }
  238. function prevStep() {
  239. if (stepIndex.value > 0) stepIndex.value -= 1
  240. }
  241. function nextStep() {
  242. const key = currentStep.value
  243. const err = validateEntryStep(form, key)
  244. if (err) {
  245. uni.showToast({ title: err, icon: 'none' })
  246. return
  247. }
  248. if (key === 'submit') {
  249. doSubmit()
  250. return
  251. }
  252. if (stepIndex.value < stepKeys.value.length - 1) {
  253. stepIndex.value += 1
  254. }
  255. }
  256. function doSubmit() {
  257. const nick = (userStore.state.nickName || '').trim()
  258. if (!nick) {
  259. ensureMemberNickName()
  260. return
  261. }
  262. const confirmTip = shopOnlyMode.value
  263. ? '提交后不可修改,将复用已有商户并申请开设新店铺,是否确认?'
  264. : '提交后不可修改,是否确认?'
  265. uni.showModal({
  266. title: '确认提交',
  267. content: confirmTip,
  268. success: (res) => {
  269. if (!res.confirm) return
  270. submitGuard.run(async () => {
  271. const payload = shopOnlyMode.value
  272. ? buildShopOnlySubmitPayload(form)
  273. : buildEntrySubmitPayload(form)
  274. await submitEntryApply(payload)
  275. uni.showToast({ title: '提交成功,请等待审核', icon: 'none', duration: 2500 })
  276. setTimeout(() => {
  277. uni.redirectTo({ url: PAGE_ENTRY_LIST })
  278. }, 1500)
  279. })
  280. }
  281. })
  282. }
  283. function goList() {
  284. uni.navigateTo({ url: PAGE_ENTRY_LIST })
  285. }
  286. </script>
  287. <style lang="scss" scoped>
  288. @import '@/styles/mine.scss';
  289. .entry-apply-wrap {
  290. min-height: 100vh;
  291. display: flex;
  292. flex-direction: column;
  293. background: #f0ebe5;
  294. }
  295. .entry-apply-page {
  296. flex: 1;
  297. display: flex;
  298. flex-direction: column;
  299. min-height: 0;
  300. }
  301. .entry-closed {
  302. padding: 80rpx 40rpx;
  303. text-align: center;
  304. font-size: 28rpx;
  305. color: #666;
  306. }
  307. .entry-closed__btn {
  308. margin-top: 32rpx;
  309. width: 100%;
  310. }
  311. .entry-steps {
  312. display: flex;
  313. padding: 20rpx 16rpx;
  314. background: #fff;
  315. }
  316. .entry-steps__item {
  317. flex: 1;
  318. text-align: center;
  319. font-size: 24rpx;
  320. color: #999;
  321. }
  322. .entry-steps__item--on {
  323. color: #2e7d32;
  324. font-weight: 600;
  325. }
  326. .entry-scroll {
  327. flex: 1;
  328. // height: 0;
  329. padding: 24rpx;
  330. box-sizing: border-box;
  331. }
  332. .entry-actions {
  333. display: flex;
  334. gap: 20rpx;
  335. padding: 20rpx 24rpx;
  336. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  337. background: #fff;
  338. }
  339. .entry-actions__prev {
  340. flex: 1;
  341. height: 88rpx;
  342. line-height: 88rpx;
  343. }
  344. .entry-actions__next {
  345. flex: 2;
  346. height: 88rpx;
  347. line-height: 88rpx;
  348. }
  349. .type-card {
  350. margin-bottom: 20rpx;
  351. padding: 32rpx;
  352. border: 2rpx solid #e5ded6;
  353. border-radius: 16rpx;
  354. }
  355. .type-card--on {
  356. border-color: #2e7d32;
  357. background: #f1f8f4;
  358. }
  359. .type-card__title {
  360. display: block;
  361. font-size: 32rpx;
  362. font-weight: 600;
  363. color: #333;
  364. }
  365. .type-card__sub {
  366. display: block;
  367. margin-top: 8rpx;
  368. font-size: 24rpx;
  369. color: #999;
  370. }
  371. .submit-tip {
  372. display: block;
  373. padding: 16rpx 0 24rpx;
  374. font-size: 26rpx;
  375. color: #666;
  376. }
  377. .entry-shop-only-banner {
  378. margin: 16rpx 24rpx 0;
  379. padding: 24rpx;
  380. background: #e8f5e9;
  381. border-radius: 12rpx;
  382. }
  383. .entry-shop-only-banner__title {
  384. display: block;
  385. font-size: 28rpx;
  386. font-weight: 600;
  387. color: #2e7d32;
  388. }
  389. .entry-shop-only-banner__tip {
  390. display: block;
  391. margin-top: 8rpx;
  392. font-size: 24rpx;
  393. color: #4a6b4f;
  394. line-height: 1.5;
  395. }
  396. .entry-shop-only-banner__line {
  397. display: block;
  398. margin-top: 8rpx;
  399. font-size: 24rpx;
  400. color: #5c5652;
  401. }
  402. </style>