Selaa lähdekoodia

feat(hdApp): 新增拉卡拉收款账户管理

- 在个人中心增加收款账户入口与拉卡拉页面路由
- 支持进件草稿、资料 OCR、电子合同、结算卡变更及认证指引
- API: 新增 /lakala-account 下的账户与进件接口封装
- Database: 无
- Breaking Changes: 无
shizhongqi 1 kuukausi sitten
vanhempi
commit
9ff26b7b66

+ 3 - 0
hdApp/src/admin/home/me.vue

@@ -281,6 +281,7 @@ export default {
       menuList: [
         { key: "address", name: "修改门店", icon: "modify_shop" },
         { key: "book", name: "使用教程", icon: "my-book" },
+        { key: "lakala", name: "收款账户管理", icon: "pay-code" },
         { key: "password", name: "密码修改", icon: "my-password" },
         { key: "cashSet", name: "提现账号", icon: "my-bell" },
         { key: "logout", name: "退出登录", icon: "my-logout" }
@@ -372,6 +373,8 @@ export default {
         this.$util.pageTo({ url: "/admin/shop/add?id=" + shopId });
       } else if (item.key == "book") {
         this.$util.pageTo({ url: "/admin/book/index" });
+      } else if (item.key == "lakala") {
+        this.$util.pageTo({ url: "/admin/lakala/lakala" });
       } else if (item.key == "password") {
         this.$util.pageTo({ url: "/admin/shop/password" });
       } else if (item.key == "cashSet") {

+ 30 - 0
hdApp/src/admin/lakala/lakala-components/AttachmentCard.vue

@@ -0,0 +1,30 @@
+<template>
+  <view class="attachment" @click="$emit('upload')">
+    <view>
+      <text class="attach-title">{{ title }}</text>
+      <text class="attach-tip">请上传{{ title }}</text>
+    </view>
+    <image v-if="item && item.ossPath" :src="imageSrc" mode="aspectFill" />
+    <view v-else class="camera">+</view>
+  </view>
+</template>
+<script>
+export default {
+  props: { title: { type: String, default: '' }, imgType: { type: String, default: '' }, items: { type: Array, default: () => [] } },
+  computed: {
+    item() { return this.items.find(v => v.imgType === this.imgType); },
+    imageSrc() {
+      const path = this.item.ossPath || '';
+      if (/^https?:\/\//.test(path)) return path;
+      return `${this.$constant.imgUrl}/${path}`;
+    }
+  }
+};
+</script>
+<style scoped>
+.attachment { margin: 0 24upx 24upx; padding: 24upx; background: #fff; border-radius: 18upx; min-height: 180upx; display: flex; align-items: center; justify-content: space-between; }
+.attach-title { display: block; font-size: 32upx; color: #111; margin-bottom: 12upx; }
+.attach-tip { color: #999; font-size: 28upx; }
+.attachment image, .camera { width: 260upx; height: 160upx; border-radius: 12upx; background: #eaf3ee; }
+.camera { color: #09c567; font-size: 60upx; display: flex; align-items: center; justify-content: center; }
+</style>

+ 17 - 0
hdApp/src/admin/lakala/lakala-components/DateInput.vue

@@ -0,0 +1,17 @@
+<template>
+  <picker mode="date" :value="value" @change="$emit('input', $event.detail.value)">
+    <view class="form-row">
+      <text class="form-label required">{{ label }}</text>
+      <text class="form-input">{{ value || '请选择日期' }}</text>
+    </view>
+  </picker>
+</template>
+<script>
+export default { props: { value: { default: '' }, label: { type: String, default: '' } } };
+</script>
+<style scoped>
+.form-row { min-height: 88upx; display: flex; align-items: center; border-bottom: 1upx solid #eee; }
+.form-label { width: 270upx; flex-shrink: 0; color: #34445c; font-size: 28upx; }
+.required:before { content: '*'; color: #f33; margin-right: 6upx; }
+.form-input { flex: 1; text-align: right; color: #333; font-size: 28upx; }
+</style>

+ 15 - 0
hdApp/src/admin/lakala/lakala-components/FormInput.vue

@@ -0,0 +1,15 @@
+<template>
+  <view class="form-row">
+    <text class="form-label" :class="{ required: required }">{{ label }}</text>
+    <input class="form-input" :value="value" @input="$emit('input', $event.detail.value)" :placeholder="'请输入' + label" />
+  </view>
+</template>
+<script>
+export default { props: { value: { default: '' }, label: { type: String, default: '' }, required: { type: Boolean, default: false } } };
+</script>
+<style scoped>
+.form-row { min-height: 88upx; display: flex; align-items: center; border-bottom: 1upx solid #eee; }
+.form-label { width: 270upx; flex-shrink: 0; color: #34445c; font-size: 28upx; }
+.required:before { content: '*'; color: #f33; margin-right: 6upx; }
+.form-input { flex: 1; text-align: right; color: #333; font-size: 28upx; }
+</style>

+ 92 - 0
hdApp/src/admin/lakala/lakala-components/OptionPicker.vue

@@ -0,0 +1,92 @@
+<!--
+  拉卡拉进件通用选项弹层
+  用于省市区、支行等超长列表选择;替代 showActionSheet(小程序最多 6 项)
+-->
+<template>
+  <view v-if="visible" class="picker-mask" @click="onCancel">
+    <view class="picker-panel" @click.stop>
+      <view class="picker-header">
+        <text class="picker-title">{{ title }}</text>
+        <text class="picker-cancel" @click="onCancel">取消</text>
+      </view>
+      <scroll-view scroll-y class="picker-list">
+        <view
+          v-for="item in options"
+          :key="item.code"
+          class="picker-item"
+          @click="onSelect(item)"
+        >
+          <text>{{ item.name }}</text>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    visible: { type: Boolean, default: false },
+    title: { type: String, default: '' },
+    // 统一为 { code, name },由父组件 normalizeOptions 处理
+    options: { type: Array, default: () => [] }
+  },
+  methods: {
+    /** 选中一项后通知父组件 */
+    onSelect(item) {
+      this.$emit('select', item);
+    },
+    /** 点击遮罩或取消 */
+    onCancel() {
+      this.$emit('cancel');
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.picker-mask {
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 999;
+  background: rgba(0, 0, 0, 0.45);
+}
+.picker-panel {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #fff;
+  border-radius: 24upx 24upx 0 0;
+  padding-bottom: env(safe-area-inset-bottom);
+}
+.picker-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28upx 32upx;
+  border-bottom: 1upx solid #eee;
+}
+.picker-title {
+  font-size: 32upx;
+  font-weight: 600;
+  color: #222;
+}
+.picker-cancel {
+  font-size: 35upx;
+  color: #999;
+}
+.picker-list {
+  max-height: 65vh;
+  height: 65vh;
+}
+.picker-item {
+  padding: 28upx 32upx;
+  font-size: 35upx;
+  color: #333;
+  border-bottom: 1upx solid #f5f5f5;
+}
+</style>

+ 3 - 0
hdApp/src/admin/lakala/lakala-components/SectionTitle.vue

@@ -0,0 +1,3 @@
+<template><view class="section-title">{{ title }}</view></template>
+<script>export default { props: { title: { type: String, default: '' } } };</script>
+<style scoped>.section-title { margin: 28rpx 28rpx 16rpx; font-size: 37.5rpx; font-weight: 600; border-left: 8rpx solid #09c567; padding-left: 16rpx; }</style>

+ 197 - 0
hdApp/src/admin/lakala/lakala.vue

@@ -0,0 +1,197 @@
+<!--
+  拉卡拉进件列表页
+  花掌柜 App 展示进件申请与已认证收款账户,入口在 admin/lakala/lakala
+-->
+<template>
+  <view class="page">
+    <view v-if="pendingApplications.length === 0 && accounts.length === 0" class="empty">暂无进件或收款账户</view>
+
+    <view v-for="item in pendingApplications" :key="item.rowKey" class="card">
+      <view class="card-head">
+        <text class="title">{{ item.displayTitle }}</text>
+        <text class="status" :class="item.statusCls">{{ item.displayStatus }}</text>
+      </view>
+      <view class="line">申请编号:{{ item.id }}</view>
+      <view class="line">更新时间:{{ item.updateTime }}</view>
+      <view v-if="item.rejectReason" class="reject">驳回原因:{{ item.rejectReason }}</view>
+      <view class="actions">
+        <button v-if="item.canContinueDraft" class="outline" @click="resumeApplication(item.id)">继续填写</button>
+        <button v-if="item.canDeleteDraft" class="danger" @click="removeDraft(item.id)">删除</button>
+        <button v-if="item.canSign" class="outline" @click="resumeApplication(item.id)">继续签约</button>
+        <button v-if="item.canResubmit" class="primary" @click="resumeApplication(item.id)">修改</button>
+        <button v-if="item.canRefresh" class="outline" @click="refresh(item.id)">刷新状态</button>
+      </view>
+    </view>
+
+    <view v-for="account in accounts" :key="account.rowKey" class="card account-card">
+      <view class="card-head">
+        <text class="title">{{ account.licenseName || account.accountName }}</text>
+        <text class="status passed">{{ account.displayStatus }}</text>
+      </view>
+      <view class="line">收款人:{{ account.accountName }}</view>
+      <view class="line">收款银行:{{ account.bankName }}</view>
+      <view class="line">收款账号:{{ account.maskedAccountNo }}</view>
+      <view class="line">商户编号:{{ account.merchantNo }}</view>
+      <view class="line">扫码终端:{{ account.scanTermNo || '开通中' }}</view>
+      <view class="line">B2B终端:{{ account.b2bTermNo || '开通中' }}</view>
+      <view class="actions">
+        <button class="outline" @click="changeBank(account.id)">收款银行卡更换</button>
+        <button v-if="account.canSetCurrent" class="primary" @click="setCurrent(account.id)">设置为收款账户</button>
+      </view>
+      <view v-if="account.canAuthorize" class="auth-box">
+        <view class="auth-tip">微信/支付宝认证完成后,收款账户即可正常用于扫码收款。</view>
+        <view class="line">微信认证状态:{{ account.wxAuthorizeText }}</view>
+        <view class="line">支付宝认证状态:{{ account.zfbAuthorizeText }}</view>
+        <button class="auth-btn" @click="openAuthorize(account.id)">去微信/支付宝认证</button>
+      </view>
+    </view>
+
+    <view class="bottom-space"></view>
+    <view class="bottom">
+      <button class="add" @click="create">+ 新增账户</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { accountList, createApplication, deleteDraft, refreshApplication, setCurrentAccount } from "@/api/lakala-account";
+
+const STATUS_TEXT = { DRAFT: '草稿', CONTRACT_PENDING: '待签约', PLATFORM_AUDIT: '后台审核', WAIT_AUDIT: '待审核', REJECT: '已驳回', APPROVED: '认证通过' };
+const STATUS_CLASS = { REJECT: 'rejected', APPROVED: 'passed', WAIT_AUDIT: 'pending', CONTRACT_PENDING: 'pending', PLATFORM_AUDIT: 'pending' };
+
+export default {
+  data() {
+    return { applications: [], accounts: [] };
+  },
+  computed: {
+    /** 未通过的进件申请,已通过的在收款账户区展示,避免 v-for 与 v-if 同用 */
+    pendingApplications() {
+      return this.applications.filter(item => item.status !== 'APPROVED');
+    }
+  },
+  onShow() { this.load(); },
+  onPullDownRefresh() { this.load().finally(() => uni.stopPullDownRefresh()); },
+  methods: {
+    init() {
+    },
+    async load() {
+      const res = await accountList();
+      this.applications = (res.data.applications || []).map(item => this.decorateApplication(item));
+      this.accounts = (res.data.accounts || []).map(account => this.decorateAccount(account));
+    },
+    /** 进件列表项补充展示字段,避免模板里写复杂表达式(小程序编译限制) */
+    decorateApplication(item) {
+      const data = item.formData || {};
+      const title = (data.merchant && data.merchant.businessName) || (data.license && data.license.licenseName) || '商户进件';
+      const status = item.status;
+      return {
+        ...item,
+        rowKey: 'app' + item.id,
+        displayTitle: item.type === 'settlement_change' ? '收款银行卡更换' : title,
+        displayStatus: STATUS_TEXT[status] || status,
+        statusCls: STATUS_CLASS[status] || '',
+        canContinueDraft: status === 'DRAFT',
+        canDeleteDraft: status === 'DRAFT',
+        canSign: status === 'CONTRACT_PENDING',
+        canResubmit: status === 'REJECT',
+        canRefresh: status === 'WAIT_AUDIT' || status === 'APPROVED' //status === 'REJECT'
+      };
+    },
+    /** 收款账户列表项补充展示字段 */
+    decorateAccount(account) {
+      const no = account.accountNo || '';
+      let maskedAccountNo = no;
+      if (no.length >= 8) {
+        maskedAccountNo = no.slice(0, 4) + ' **** **** ' + no.slice(-4);
+      }
+      return {
+        ...account,
+        rowKey: 'acc' + account.id,
+        maskedAccountNo,
+        displayStatus: account.isCurrent == 1 ? '收款账户' : '认证通过',
+        canSetCurrent: account.isCurrent != 1,
+        canAuthorize: !!account.scanTermNo && !!account.b2bTermNo,
+        wxAuthorizeText: this.authorizeText(account.wxAuthorizeState),
+        zfbAuthorizeText: this.authorizeText(account.zfbAuthorizeState)
+      };
+    },
+    authorizeText(state) {
+      const map = { SUCCESS: '已认证', AUTHORIZED: '已认证', FINISH: '已认证', PASS: '已认证', PROCESSING: '认证中', APPLYING: '认证中', WAIT: '待认证', REJECT: '已驳回', FAILED: '认证失败', 0: '待认证'};
+      return map[state] || state || '待认证';
+    },
+    findApplication(id) {
+      return this.applications.find(item => item.id === id);
+    },
+    findAccount(id) {
+      return this.accounts.find(account => account.id === id);
+    },
+    async create() {
+      const res = await createApplication({ type: 'onboarding' });
+      this.open(res.data.id, 'create');
+    },
+    async changeBank(accountId) {
+      const res = await createApplication({ type: 'settlement_change', accountId });
+      this.open(res.data.id, 'settlement-change');
+    },
+    resumeApplication(id) {
+      const item = this.findApplication(id);
+      if (!item) return;
+      const mode = item.type === 'settlement_change' ? 'settlement-change' : (item.status === 'REJECT' ? 'resubmit' : 'create');
+      this.open(item.id, mode);
+    },
+    open(id, mode) {
+      uni.navigateTo({ url: `/admin/lakala/lakalaOnboarding?id=${id}&mode=${mode}` });
+    },
+    openAuthorize(accountId) {
+      uni.navigateTo({ url: `/admin/lakala/lakalaAuthorize?accountId=${accountId}` });
+    },
+    removeDraft(id) {
+      uni.showModal({
+        title: '删除草稿',
+        content: '删除后无法恢复,确认删除?',
+        success: async res => {
+          if (!res.confirm) return;
+          await deleteDraft({ id });
+          this.load();
+        }
+      });
+    },
+    async refresh(id) {
+      await refreshApplication({ id });
+      uni.showToast({ title: '状态已刷新', icon: 'none' });
+      this.load();
+    },
+    async setCurrent(accountId) {
+      await setCurrentAccount({ accountId });
+      this.load();
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page { min-height: 100vh; background: #f2f7f4; padding: 24upx; box-sizing: border-box; }
+.empty { padding: 160upx 0; text-align: center; color: #999; }
+.card { background: #fff; border-radius: 18upx; padding: 28upx; margin-bottom: 24upx; box-shadow: 0 4upx 20upx rgba(0,0,0,.05); }
+.account-card { border-left: 6upx solid #09c567; }
+.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20upx; }
+.title { font-size: 32upx; font-weight: 600; color: #222; }
+.status { padding: 8upx 18upx; border-radius: 10upx; color: #777; background: #f3f3f3; }
+.pending { color: #139a55; background: #ecfaf2; }
+.passed { color: #159957; background: #ecfaf2; }
+.rejected { color: #f04a4a; background: #fff0f0; }
+.line { color: #444; font-size: 28upx; line-height: 48upx; }
+.reject { color: #f04a4a; background: #fff3f3; padding: 16upx; margin-top: 16upx; border-radius: 10upx; }
+.actions { display: flex; margin-top: 24upx; }
+.actions button { flex: 1; font-size: 28upx; line-height: 72upx; padding: 0 12upx; margin-left: 18upx; }
+.actions button:first-child { margin-left: 0; }
+.primary, .add { color: #fff; background: #09c567; }
+.outline { color: #09c567; background: #fff; border: 1upx solid #09c567; }
+.danger { color: #f04a4a; background: #fff; border: 1upx solid #f04a4a; }
+.auth-box { margin-top: 22upx; padding-top: 22upx; border-top: 1upx dashed #e2e8f0; }
+.auth-tip { color: #666; font-size: 26upx; line-height: 40upx; margin-bottom: 8upx; }
+.auth-btn { margin-top: 18upx; color: #fff; background: #09c567; font-size: 28upx; line-height: 72upx; }
+.bottom-space { height: 130upx; }
+.bottom { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; padding: 20upx 28upx calc(20upx + env(safe-area-inset-bottom)); }
+.add { border-radius: 12upx; }
+</style>

+ 109 - 0
hdApp/src/admin/lakala/lakalaAuthorize.vue

@@ -0,0 +1,109 @@
+<template>
+  <view class="page">
+    <view class="notice-title">认证须知</view>
+    <view class="notice">
+      开户资料提交审核通过了,已经绑定到店铺收款。需要你按下面这个视频进行认证开通收款账户,认证完了系统就可以用了。
+      <br />
+      微信如果点进去没有可选的,跟支付宝一样,需要注册新商家。
+    </view>
+
+    <view class="video-card">
+      <view class="card-title">操作视频教程</view>
+      <view class="video-block">
+        <video
+          id="lakalaAuthVideo"
+          :src="videoUrl"
+          show-mute-btn="true"
+          object-fit="contain"
+          class="video-content">
+        </video>
+      </view>
+    </view>
+
+    <view class="grid">
+      <view v-for="item in authItems" :key="item.key" class="auth-card">
+        <view class="card-title">{{ item.title }}</view>
+        <image class="auth-img" :src="item.url" mode="widthFix" @click="preview(item.url)"></image>
+        <button class="save-btn" @click="saveImage(item.url)">下载保存</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+const LAKALA_AUTH_ASSET_HOST = 'https://api.shop.hzghd.com';
+
+export default {
+  data() {
+    return {
+      // 仅存文件名,完整 URL 在 computed 里用 $constant 拼接(data 阶段 constant 尚未注入)
+      authFileItems: [
+        { key: 'lkl-wx', title: '拉卡拉微信授权', file: 'lkl_wx_auth.png' },
+        { key: 'system-wx', title: '系统微信授权', file: 'system_wx_auth.png' },
+        { key: 'lkl-zfb', title: '拉卡拉支付宝授权', file: 'lkl_zfb_auth.png' }
+      ]
+    };
+  },
+  computed: {
+    /** 认证素材复用销花宝现网地址,避免两端维护不同版本的认证教程。 */
+    imgBase() {
+      return `${LAKALA_AUTH_ASSET_HOST}/image/lakalaJingjian`;
+    },
+    videoUrl() {
+      return `${LAKALA_AUTH_ASSET_HOST}/jc/upgrade.mp4`;
+    },
+    authItems() {
+      return this.authFileItems.map(item => ({
+        ...item,
+        url: `${this.imgBase}/${item.file}`
+      }));
+    }
+  },
+  methods: {
+    init() {},
+    preview(url) {
+      uni.previewImage({ urls: [url] });
+    },
+    saveImage(url) {
+      uni.downloadFile({
+        url,
+        success: res => {
+          if (res.statusCode !== 200) {
+            uni.showToast({ title: '下载失败', icon: 'none' });
+            return;
+          }
+          uni.saveImageToPhotosAlbum({
+            filePath: res.tempFilePath,
+            success: () => uni.showToast({ title: '保存成功', icon: 'none' }),
+            fail: () => uni.showToast({ title: '保存失败', icon: 'none' })
+          });
+        },
+        fail: () => uni.showToast({ title: '下载失败', icon: 'none' })
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page { min-height: 100vh; background: #fff; padding: 24rpx 20rpx 48rpx; box-sizing: border-box; color: #222; }
+.notice-title { font-size: 34rpx; font-weight: 700; line-height: 56rpx; margin-bottom: 12rpx; }
+.notice { font-size: 31rpx; line-height: 52rpx; margin-bottom: 44rpx; }
+.video-card { margin-bottom: 28rpx; }
+.card-title { text-align: center; font-size: 24rpx; font-weight: 600; color: #333; margin-bottom: 18rpx; }
+// 视频播放区:与 book/detail 一致,黑底居中固定比例
+.video-block {
+  margin: 20upx 0;
+  background: #000;
+  .video-content {
+    width: 700upx;
+    height: 1100upx;
+    margin: 0 auto;
+    display: block;
+  }
+}
+.grid { display: flex; flex-wrap: wrap; margin: 0 -12rpx; }
+.auth-card { width: 50%; padding: 0 12rpx 28rpx; box-sizing: border-box; }
+.auth-img { width: 100%; border-radius: 8rpx; background: #f5f5f5; display: block; }
+.save-btn { width: 180rpx; margin-top: 20rpx; color: #fff; background: #09c567; border-radius: 8rpx; font-size: 24rpx; line-height: 58rpx; }
+</style>

+ 560 - 0
hdApp/src/admin/lakala/lakalaOnboarding.vue

@@ -0,0 +1,560 @@
+<template>
+  <view class="page" v-if="loaded">
+    <view v-if="mode !== 'settlement-change'" class="steps">
+      <view v-for="(name, index) in stepNames" :key="name" class="step" :class="{ active: currentStep >= index + 1 }" @click="goStep(index + 1)">
+        <view class="circle">{{ index + 1 }}</view>
+        <text>{{ name }}</text>
+      </view>
+    </view>
+
+    <view v-if="currentStep === 1 && mode !== 'settlement-change'">
+      <SectionTitle title="商家基本情况" />
+      <view class="box choice-row">
+        <text class="label required" style="width: 280upx;">商户类别</text>
+        <radio-group @change="setValue('license.merchantType', $event.detail.value)">
+          <label><radio value="TP_MERCHANT" :checked="form.license.merchantType === 'TP_MERCHANT'" />企业</label>
+          <label><radio value="TP_PERSONAL" :checked="form.license.merchantType === 'TP_PERSONAL'" />小微</label>
+        </radio-group>
+      </view>
+      <AttachmentCard v-if="form.license.merchantType === 'TP_MERCHANT'" title="营业执照" imgType="BUSINESS_LICENCE" :items="form.license.attachments" @upload="uploadAttachment('license', 'BUSINESS_LICENCE', true)" />
+      <view class="box">
+        <FormInput v-if="form.license.merchantType === 'TP_MERCHANT'" label="营业执照名称" v-model="form.license.licenseName" required />
+        <FormInput v-if="form.license.merchantType === 'TP_MERCHANT'" label="证件代码" v-model="form.license.licenseNo" required />
+        <DateInput v-if="form.license.merchantType === 'TP_MERCHANT'" label="执照起始日期" v-model="form.license.licenseStart" />
+        <DateInput v-if="form.license.merchantType === 'TP_MERCHANT'" label="执照有效期" v-model="form.license.licenseEnd" />
+        <FormInput v-if="form.license.merchantType === 'TP_MERCHANT'" label="注册地址" v-model="form.license.registeredAddress" />
+      </view>
+      <AttachmentCard title="身份证人像面" imgType="ID_CARD_FRONT" :items="form.license.attachments" @upload="uploadAttachment('license', 'ID_CARD_FRONT', true)" />
+      <AttachmentCard title="身份证国徽面" imgType="ID_CARD_BEHIND" :items="form.license.attachments" @upload="uploadAttachment('license', 'ID_CARD_BEHIND', true)" />
+      <view class="box">
+        <!-- <FormInput label="经营范围" v-model="form.license.businessContent" required /> -->
+        <view v-if="form.license.merchantType === 'TP_PERSONAL'">
+            <FormInput label="姓名" v-model="form.license.legalName" required />
+        </view>
+        <view v-if="form.license.merchantType === 'TP_MERCHANT'">
+            <FormInput label="法人姓名" v-model="form.license.legalName" required />
+        </view>
+        <FormInput label="身份证号" v-model="form.license.legalIdNo" required />
+        <DateInput label="身份证起始日期" v-model="form.license.legalIdStart" />
+        <DateInput label="身份证有效期" v-model="form.license.legalIdEnd" />
+      </view>
+    </view>
+
+    <view v-if="currentStep === 2 && mode !== 'settlement-change'">
+      <SectionTitle title="商户信息" />
+      <view class="box">
+        <FormInput label="商户经营名称" v-model="form.merchant.businessName" required />
+        <view class="form-row">
+          <text class="form-label required">商户所在地区</text>
+          <button class="mini-btn" @click="chooseRegion('merchant', 'region')">{{ merchantRegionText }}</button>
+        </view>
+        <FormInput label="详细地址" v-model="form.merchant.address" required />
+      </view>
+      <view class="tip">商户类型固定为花店(5992);联系人、手机号和邮箱按系统默认值提交。</view>
+    </view>
+
+    <view v-if="currentStep === 3">
+      <SectionTitle title="结算信息" />
+      <view class="box choice-row">
+        <text class="label required" style="width: 290upx;">账户类型</text>
+        <radio-group @change="setValue('settlement.accountType', $event.detail.value)">
+          <label><radio value="58" :checked="form.settlement.accountType === '58'" />对私</label>
+          <label><radio value="57" :checked="form.settlement.accountType === '57'" />对公</label>
+        </radio-group>
+      </view>
+      <view v-if="form.settlement.accountType === '58'" class="box switch-row">
+        <text>是否法人进件</text>
+        <switch :checked="form.settlement.isLegalPerson" @change="setValue('settlement.isLegalPerson', $event.detail.value)" color="#09c567" />
+      </view>
+      <AttachmentCard v-if="form.settlement.accountType === '58'" title="银行卡照片" imgType="BANK_CARD" :items="form.settlement.attachments" @upload="uploadAttachment('settlement', 'BANK_CARD', true)" />
+      <template v-if="form.settlement.accountType === '58' && !form.settlement.isLegalPerson">
+        <AttachmentCard title="结算人身份证人像面" imgType="ID_CARD_FRONT" :items="form.settlement.attachments" @upload="uploadAttachment('settlement', 'ID_CARD_FRONT', true)" />
+        <AttachmentCard title="结算人身份证国徽面" imgType="ID_CARD_BEHIND" :items="form.settlement.attachments" @upload="uploadAttachment('settlement', 'ID_CARD_BEHIND', true)" />
+        <AttachmentCard title="法人授权函" imgType="LETTER_OF_AUTHORIZATION" :items="form.settlement.attachments" @upload="uploadAttachment('settlement', 'LETTER_OF_AUTHORIZATION', false)" />
+      </template>
+      <AttachmentCard v-if="form.settlement.accountType === '57'" title="开户许可证" imgType="OPENING_PERMIT" :items="form.settlement.attachments" @upload="uploadAttachment('settlement', 'OPENING_PERMIT', false)" />
+      <view class="box">
+        <FormInput label="开户名" v-model="form.settlement.accountName" required />
+        <FormInput label="银行卡号" v-model="form.settlement.accountNo" required />
+        <FormInput label="开户银行" v-model="form.settlement.bankName" required />
+        <view class="form-row">
+          <text class="form-label required">开户地区</text>
+          <button class="mini-btn" @click="chooseRegion('settlement', 'bank-region')">{{ settlementRegionText }}</button>
+        </view>
+        <view class="form-row">
+          <text class="form-label required">开户支行</text>
+          <button class="mini-btn" @click="chooseBank">{{ settlementBranchText }}</button>
+        </view>
+        <FormInput v-if="!form.settlement.isLegalPerson" label="结算人身份证号" v-model="form.settlement.accountIdNo" />
+        <DateInput v-if="!form.settlement.isLegalPerson" label="结算人证件起始日" v-model="form.settlement.accountIdStart" />
+        <DateInput v-if="!form.settlement.isLegalPerson" label="结算人证件有效期" v-model="form.settlement.accountIdEnd" />
+      </view>
+    </view>
+
+    <view v-if="currentStep === 4 && mode !== 'settlement-change'">
+      <SectionTitle title="门店信息" />
+      <AttachmentCard title="门头照" imgType="SHOP_OUTSIDE_IMG" :items="form.shop.attachments" @upload="uploadAttachment('shop', 'SHOP_OUTSIDE_IMG', false)" />
+      <AttachmentCard title="内设照" imgType="SHOP_INSIDE_IMG" :items="form.shop.attachments" @upload="uploadAttachment('shop', 'SHOP_INSIDE_IMG', false)" />
+      <AttachmentCard title="收银台照" imgType="CHECKSTAND_IMG" :items="form.shop.attachments" @upload="uploadAttachment('shop', 'CHECKSTAND_IMG', false)" />
+      <view class="box">
+        <FormInput label="门店名称" v-model="form.shop.name" required />
+        <FormInput label="详细地址" v-model="form.shop.address" required />
+        <FormInput label="门店联系人" v-model="form.shop.contactName" required />
+        <FormInput label="联系手机号" v-model="form.shop.contactMobile" required />
+        <!-- <FormInput label="经度" v-model="form.shop.longitude" required />
+        <FormInput label="纬度" v-model="form.shop.latitude" required /> -->
+      </view>
+    </view>
+
+    <view v-if="currentStep === 5 && mode !== 'settlement-change'">
+      <SectionTitle title="业务信息" />
+      <view class="box summary">
+        <view><text>结算类型</text><text>自动结算</text></view>
+        <view><text>结算周期</text><text>D1</text></view>
+        <view><text>商户类型</text><text>花店(5992)</text></view>
+        <view><text>批发费率</text><text>{{ form.business.rate }}%</text></view>
+      </view>
+      <view class="box">
+        <FormInput label="电子合同签约手机号" v-model="form.business.signMobile" required />
+      </view>
+      <view class="contract">
+        <button class="outline" @click="createContract">生成并签署电子合同</button>
+        <button class="outline" @click="checkContract">刷新签约状态</button>
+        <text :class="{ signed: contractStatus === 'COMPLETED' }">签约状态:{{ contractStatusText }}</text>
+      </view>
+    </view>
+
+    <view class="bottom-space"></view>
+    <view class="bottom">
+      <button v-if="currentStep > 1 && mode !== 'settlement-change'" class="back" @click="previous">上一步</button>
+      <button v-if="currentStep < 5 && mode !== 'settlement-change'" class="next" @click="next">下一步</button>
+      <button v-else class="next" @click="submit">{{ mode === 'settlement-change' ? '提交申请' : '提交审核' }}</button>
+    </view>
+
+    <!-- 省市区/支行等超长列表选择,小程序 showActionSheet 最多 6 项不够用 -->
+    <OptionPicker
+      :visible="optionPicker.visible"
+      :title="optionPicker.title"
+      :options="optionPicker.options"
+      @select="onOptionPickerSelect"
+      @cancel="onOptionPickerCancel"
+    />
+  </view>
+</template>
+
+<script>
+import {
+  applicationDetail, saveDraft, uploadFile, ocrResult, aliyunOcr, applyContract,
+  contractStatus, submitApplication, getOption
+} from "@/api/lakala-account";
+import SectionTitle from "./lakala-components/SectionTitle.vue";
+import FormInput from "./lakala-components/FormInput.vue";
+import DateInput from "./lakala-components/DateInput.vue";
+import AttachmentCard from "./lakala-components/AttachmentCard.vue";
+import OptionPicker from "./lakala-components/OptionPicker.vue";
+
+export default {
+  components: { SectionTitle, FormInput, DateInput, AttachmentCard, OptionPicker },
+  data() {
+    return {
+      id: 0, mode: 'create', loaded: false, currentStep: 1, form: null,
+      stepNames: ['执照信息', '商户信息', '结算信息', '门店信息', '业务信息'],
+      contractStatus: '', saveTimer: null, saving: false,
+      // 通用选项弹层状态,resolve 用于 chooseOption / chooseBank 的 Promise
+      optionPicker: { visible: false, title: '', options: [], resolve: null }
+    };
+  },
+  computed: {
+    contractStatusText() {
+      return { COMPLETED: '已完成', UNDONE: '待签署' }[this.contractStatus] || '未生成';
+    },
+    merchantRegionText() {
+      const m = this.form && this.form.merchant;
+      return m && m.countyCode ? `${m.provinceName || ''}${m.cityName || ''}${m.countyName || ''}` : '请选择地区';
+    },
+    settlementRegionText() {
+      const s = this.form && this.form.settlement;
+      return s && s.cityCode ? `${s.provinceName || ''}${s.cityName || ''}` : '请选择开户地区';
+    },
+    /** 已选支行展示名称,未选时提示查询 */
+    settlementBranchText() {
+      const s = this.form && this.form.settlement;
+      if (!s || !s.bankNo) return '查询并选择支行';
+      return s.bankName || s.bankNo;
+    }
+  },
+  watch: {
+    form: {
+      deep: true,
+      handler() {
+        if (!this.loaded || this.saving) return;
+        clearTimeout(this.saveTimer);
+        this.saveTimer = setTimeout(() => this.persist(), 800);
+      }
+    }
+  },
+  onLoad(option) {
+    this.id = Number(option.id || 0);
+    this.mode = option.mode || 'create';
+    this.load();
+  },
+  onShow() {
+    if (this.loaded && this.contractStatus === 'UNDONE') this.checkContract(false);
+  },
+  onUnload() {
+    clearTimeout(this.saveTimer);
+    this.persist();
+  },
+  methods: {
+    init() {
+    },
+    async load() {
+      const res = await applicationDetail({ id: this.id });
+      this.form = res.data.formData;
+      this.applyDefaultMerchantType();
+      this.normalizeFormAttachments();
+      this.applyDefaultExpiryDates();
+      this.currentStep = this.mode === 'settlement-change' ? 3 : Number(res.data.currentStep || 1);
+      this.contractStatus = res.data.contractStatus || '';
+      this.loaded = true;
+    },
+    /** 新建进件默认按企业商户处理;已有草稿或详情值不覆盖 */
+    applyDefaultMerchantType() {
+      if (!this.form.license.merchantType) {
+        this.$set(this.form.license, 'merchantType', 'TP_MERCHANT');
+      }
+    },
+    /** 返回当天日期 yyyy-MM-dd,供 date picker 默认值使用 */
+    getTodayDateStr() {
+      const d = new Date();
+      const pad = n => (n < 10 ? '0' : '') + n;
+      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+    },
+    /** 执照/身份证有效期未填时默认当天,减少用户重复选择 */
+    applyDefaultExpiryDates() {
+      const today = this.getTodayDateStr();
+      if (!this.form.license.legalIdEnd || this.form.license.legalIdEnd == '9999-12-31') {
+        this.$set(this.form.license, 'legalIdEnd', today);
+      }
+      if (!this.form.license.licenseEnd || this.form.license.licenseEnd == '9999-12-31') {
+        //this.$set(this.form.license, 'licenseEnd', today);
+        this.$set(this.form.license, 'licenseEnd', '2100-01-01');
+      }
+    },
+    async persist() {
+      if (!this.form || this.saving) return;
+      this.saving = true;
+      try {
+        await saveDraft({ id: this.id, currentStep: this.currentStep, formData: JSON.stringify(this.form) });
+      } catch (e) {
+        // Submitted applications are intentionally no longer editable.
+      } finally {
+        this.saving = false;
+      }
+    },
+    setValue(path, value) {
+      const parts = path.split('.');
+      this.$set(this.form[parts[0]], parts[1], value);
+    },
+    async chooseRegion(section, type) {
+      const province = await this.chooseOption(type, '1', '选择省份');
+      if (!province) return;
+      const city = await this.chooseOption(type, province.code, '选择城市');
+      if (!city) return;
+      this.$set(this.form[section], 'provinceCode', province.code);
+      this.$set(this.form[section], 'provinceName', province.name);
+      this.$set(this.form[section], 'cityCode', city.code);
+      this.$set(this.form[section], 'cityName', city.name);
+      if (section === 'merchant') {
+        const county = await this.chooseOption(type, city.code, '选择区县');
+        if (county) {
+          this.$set(this.form.merchant, 'countyCode', county.code);
+          this.$set(this.form.merchant, 'countyName', county.name);
+        }
+      }
+    },
+    async chooseOption(type, parentCode, title) {
+      const res = await getOption({ type, parentCode });
+      const list = this.normalizeOptions(res.data);
+      if (!list.length) {
+        uni.showToast({ title: '没有可选数据', icon: 'none' });
+        return null;
+      }
+      return this.openOptionPicker(title, list);
+    },
+    /** 打开可滚动选项弹层,返回选中项或 null */
+    openOptionPicker(title, options) {
+      return new Promise(resolve => {
+        this.optionPicker = { visible: true, title, options, resolve };
+      });
+    },
+    onOptionPickerSelect(item) {
+      const { resolve } = this.optionPicker;
+      this.optionPicker.visible = false;
+      this.optionPicker.resolve = null;
+      if (resolve) resolve(item);
+    },
+    onOptionPickerCancel() {
+      const { resolve } = this.optionPicker;
+      this.optionPicker.visible = false;
+      this.optionPicker.resolve = null;
+      if (resolve) resolve(null);
+    },
+    /** OCR/接口日期统一为 yyyy-MM-dd,供 date picker 使用 */
+    normalizeDateStr(value) {
+      if (!value) return value;
+      const str = String(value).trim();
+      if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str;
+      const cn = str.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/);
+      if (cn) {
+        const pad = n => (n < 10 ? '0' : '') + Number(n);
+        return `${cn[1]}-${pad(cn[2])}-${pad(cn[3])}`;
+      }
+      const dot = str.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/);
+      if (dot) {
+        const pad = n => (n < 10 ? '0' : '') + Number(n);
+        return `${dot[1]}-${pad(dot[2])}-${pad(dot[3])}`;
+      }
+      return str;
+    },
+    /** 统一省市区、支行等选项结构;兼容 camelCase 与 snake_case 字段 */
+    normalizeOptions(data) {
+      const source = Array.isArray(data) ? data : ((data && data.data) || []);
+      return source.map(v => ({
+        code: String(
+          v.code
+          || v.branch_bank_no
+          || v.area_code || v.bank_no || v.id || ''
+        ),
+        name: v.name || v.area_name
+          || v.branch_bank_name
+          || v.bank_name || '',
+        raw: v
+      })).filter(v => v.code && v.name);
+    },
+    async chooseBank() {
+      if (!this.form.settlement.cityCode) { //|| !this.form.settlement.bankName
+        uni.showToast({ title: '请先选择开户地区', icon: 'none' }); //并填写银行名称
+        return;
+      }
+      const res = await getOption({ type: 'bank', parentCode: this.form.settlement.cityCode, keyword: this.form.settlement.bankName });
+      const list = this.normalizeOptions(res.data);
+      if (!list.length) {
+        uni.showToast({ title: '没有查询到支行', icon: 'none' });
+        return;
+      }
+      const selected = await this.openOptionPicker('选择开户支行', list);
+      if (!selected) return;
+      const row = selected.raw || selected;
+      this.$set(this.form.settlement, 'bankName', row.branch_bank_name);
+      // bankNo 取支行联行号,不能用 bank_no(仅为银行类别码,如 102)
+      this.$set(this.form.settlement, 'bankNo', String(row.branch_bank_no || ''));
+      this.$set(this.form.settlement, 'clearingBankNo', String(row.clear_no || ''));
+    },
+    goStep(step) {
+      if (step < this.currentStep) {
+        this.currentStep = step;
+        this.persist();
+      }
+    },
+    previous() { this.currentStep--; this.persist(); },
+    next() {
+      if (!this.validateStep(this.currentStep)) return;
+      if (this.currentStep === 3) {
+        this.form.shop.name = this.form.merchant.businessName;
+        this.form.shop.address = this.form.merchant.address;
+        this.form.shop.provinceCode = this.form.merchant.provinceCode;
+        this.form.shop.cityCode = this.form.merchant.cityCode;
+        this.form.shop.countyCode = this.form.merchant.countyCode;
+        this.form.shop.provinceName = this.form.merchant.provinceName;
+        this.form.shop.cityName = this.form.merchant.cityName;
+        this.form.shop.countyName = this.form.merchant.countyName;
+      }
+      this.currentStep++;
+      this.persist();
+    },
+    validateStep(step) {
+      const required = {
+        1: ['license.legalName', 'license.legalIdNo', 'license.legalIdStart', 'license.legalIdEnd'],
+        2: ['merchant.businessName', 'merchant.provinceCode', 'merchant.cityCode', 'merchant.countyCode', 'merchant.address'],
+        3: ['settlement.accountName', 'settlement.accountNo', 'settlement.bankName', 'settlement.bankNo', 'settlement.clearingBankNo', 'settlement.provinceCode', 'settlement.cityCode'],
+        4: ['shop.name', 'shop.address', 'shop.contactName', 'shop.contactMobile', 'shop.longitude', 'shop.latitude'],
+        5: ['business.signMobile']
+      }[step] || [];
+      // 经营范围仅企业商户必填(小微无营业执照,无此字段)
+      if (step === 1 && this.form.license.merchantType === 'TP_MERCHANT') {
+        required.push('license.businessContent');
+      }
+      const missing = required.some(path => {
+        const [section, field] = path.split('.');
+        return !this.form[section][field];
+      });
+      if (missing) uni.showToast({ title: '请填写完整必填信息', icon: 'none' });
+      return !missing;
+    },
+    async uploadAttachment(section, imgType, isOcr) {
+      const path = await this.chooseImage();
+      if (!path) return;
+      uni.showLoading({ title: '上传识别中', mask: true });
+      try {
+        const content = await this.toBase64(path);
+        const useLakalaOcr = isOcr && imgType === 'BUSINESS_LICENCE';
+        const res = await uploadFile({ id: this.id, content, imgType, isOcr: useLakalaOcr ? 1 : 0 });
+        const list = this.form[section].attachments || [];
+        const attachment = this.normalizeAttachment(res.data);
+        const oldIndex = list.findIndex(v => v.imgType === imgType);
+        if (oldIndex >= 0) list.splice(oldIndex, 1, attachment); else list.push(attachment);
+        this.$set(this.form[section], 'attachments', list);
+        if (isOcr && (attachment.batchNo || attachment.ossPath || attachment.lakalaUrl || attachment.url)) {
+          await this.pollOcr(section, attachment, 0);
+        }
+        await this.persist();
+      } finally {
+        uni.hideLoading();
+      }
+    },
+    normalizeAttachment(attachment) {
+      return Object.assign({}, attachment, {
+        lakalaUrl: attachment.lakalaUrl || attachment.url || '',
+        batchNo: attachment.batchNo || '',
+        imgType: attachment.imgType || '',
+        ocrStatus: attachment.ocrStatus || attachment.status || '',
+        ocrResult: attachment.ocrResult || attachment.ocr || null
+      });
+    },
+    normalizeFormAttachments() {
+      ['license', 'settlement', 'shop'].forEach(section => {
+        const list = (this.form[section] && this.form[section].attachments) || [];
+        this.$set(this.form[section], 'attachments', list.map(item => this.normalizeAttachment(item)));
+      });
+    },
+    chooseImage() {
+      return new Promise(resolve => {
+        uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => resolve(res.tempFilePaths[0]), fail: () => resolve('') });
+      });
+    },
+    toBase64(path) {
+      // #ifdef MP-WEIXIN
+      return new Promise((resolve, reject) => uni.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: res => resolve('data:image/png;base64,' + res.data), fail: reject }));
+      // #endif
+      // #ifdef H5
+      return fetch(path).then(r => r.blob()).then(blob => new Promise(resolve => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }));
+      // #endif
+      // #ifdef APP-PLUS
+      return new Promise((resolve, reject) => { plus.io.resolveLocalFileSystemURL(path, entry => { entry.file(file => { const reader = new plus.io.FileReader(); reader.onloadend = e => resolve(e.target.result); reader.onerror = reject; reader.readAsDataURL(file); }); }, reject); });
+      // #endif
+    },
+    async pollOcr(section, attachment, count) {
+      if (count > 8) return;
+      const type = attachment.imgType;
+      if (type === 'BUSINESS_LICENCE') {
+        const res = await ocrResult({ id: this.id, batchNo: attachment.batchNo, imgType: attachment.imgType });
+        this.updateAttachment(section, attachment.imgType, {
+          ocrStatus: res.data.status || '',
+          ocrResult: res.data.result || null
+        });
+        if (res.data.status === '01') {
+          await new Promise(resolve => setTimeout(resolve, 1200));
+          return this.pollOcr(section, attachment, count + 1);
+        }
+        if (res.data.status === '00' && res.data.result) this.applyOcr(section, attachment.imgType, res.data.result);
+      } else {
+        const imageUrl = attachment.ossPath || attachment.lakalaUrl || attachment.url || '';
+        if (!imageUrl) return;
+        const res = await aliyunOcr({ id: this.id, imgType: attachment.imgType, imageUrl });
+        this.updateAttachment(section, attachment.imgType, {
+          ocrStatus: res.data.status || '',
+          ocrResult: res.data.result || null
+        });
+        if (res.data.status === '00' && res.data.result) this.applyOcr(section, attachment.imgType, res.data.result);
+      }
+    },
+    updateAttachment(section, imgType, patch) {
+      const list = this.form[section].attachments || [];
+      const index = list.findIndex(v => v.imgType === imgType);
+      if (index < 0) return;
+      this.$set(list, index, Object.assign({}, list[index], patch));
+      this.$set(this.form[section], 'attachments', list);
+    },
+    applyOcr(section, type, result) {
+      if (type === 'BUSINESS_LICENCE') {
+        Object.assign(this.form.license, {
+          licenseName: result.biz_license_address[1] || '',
+          licenseNo: result.biz_license_credit_code || result.biz_license_registration_code || this.form.license.licenseNo,
+          registeredAddress: result.biz_license_address[0] || '',
+          businessContent: result.biz_license_scope || this.form.license.businessContent,
+          legalName: result.biz_license_owner_name || this.form.license.legalName,
+          licenseStart: this.normalizeDateStr(result.biz_license_start_time) || this.form.license.licenseStart
+        });
+        if (!this.form.merchant.address) this.$set(this.form.merchant, 'address', result.biz_license_address || '');
+      } else if (type === 'ID_CARD_FRONT') {
+        const target = section === 'license' ? this.form.license : this.form.settlement;
+        this.$set(target, section === 'license' ? 'legalName' : 'accountName', result.name || target.accountName || '');
+        this.$set(target, section === 'license' ? 'legalIdNo' : 'accountIdNo', result.id_number || '');
+      } else if (type === 'ID_CARD_BEHIND') {
+        const validity = String(result.validity || '').split('-');
+        const target = section === 'license' ? this.form.license : this.form.settlement;
+        if (validity.length >= 2) {
+          this.$set(target, section === 'license' ? 'legalIdStart' : 'accountIdStart', this.normalizeDateStr(validity[0]));
+          this.$set(target, section === 'license' ? 'legalIdEnd' : 'accountIdEnd', this.normalizeDateStr(validity.slice(1).join('-')));
+        }
+      } else if (type === 'BANK_CARD') {
+        this.$set(this.form.settlement, 'accountNo', result.card_number || result.bank_card_number || this.form.settlement.accountNo);
+        this.$set(this.form.settlement, 'bankName', result.bank_name || this.form.settlement.bankName);
+      }
+    },
+    async createContract() {
+      if (!this.validateStep(5)) return;
+      await this.persist();
+      const res = await applyContract({ id: this.id });
+      this.contractStatus = res.data.status;
+      uni.navigateTo({ url: `/common/webview?url=${encodeURIComponent(res.data.resultUrl)}` });
+    },
+    async checkContract(show = true) {
+      if (!this.contractStatus) {
+        if (show) uni.showToast({ title: '请先生成电子合同', icon: 'none' });
+        return;
+      }
+      const res = await contractStatus({ id: this.id });
+      this.contractStatus = res.data.status;
+      if (show) uni.showToast({ title: this.contractStatusText, icon: 'none' });
+    },
+    async submit() {
+      if (!this.validateStep(this.currentStep)) return;
+      await this.persist();
+      const res = await submitApplication({ id: this.id, formData: JSON.stringify(this.form) });
+      uni.showToast({ title: this.mode === 'settlement-change' ? '申请已提交' : '已提交后台审核', icon: 'none' });
+      setTimeout(() => uni.navigateBack(), 1000);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page { min-height: 100vh; background: #f2f7f4; padding-bottom: 30upx; font-size: 28upx; }
+.steps { display: flex; background: #fff; padding: 30upx 12upx; border-radius: 0 0 28upx 28upx; }
+.step { flex: 1; text-align: center; color: #999; font-size: 27.5upx; position: relative; }
+.step:not(:last-child):before { content: ''; position: absolute; height: 4upx; background: #d8ebe2; top: 25upx; left: 60%; right: -40%; }
+.step.active:not(:last-child):before { background: #09c567; }
+.circle { width: 52upx; height: 52upx; line-height: 52upx; border-radius: 50%; background: #d8ebe2; color: #fff; margin: 0 auto 12upx; position: relative; z-index: 1; }
+.active { color: #222; }.active .circle { background: #09c567; }
+.box { margin: 0 24upx 24upx; padding: 24upx; background: #fff; border-radius: 18upx; }
+.choice-row, .switch-row { display: flex; justify-content: space-between; align-items: center; }
+.choice-row label { margin-left: 26upx; font-size: 28upx; }
+.label, .form-label { color: #34445c; }
+.required:before { content: '*'; color: #f33; margin-right: 6upx; }
+.form-row { min-height: 88upx; display: flex; align-items: center; border-bottom: 1upx solid #eee; }
+.form-label { width: 270upx; flex-shrink: 0; }
+.mini-btn { flex: 1; margin: 0; padding: 0 12upx; background: #fff; color: #09c567; font-size: 32.5upx; line-height: 64upx; text-align: right; }
+.tip { margin: 20upx 30upx; color: #999; font-size: 35upx; line-height: 52.5upx; }
+.summary view { display: flex; justify-content: space-between; font-size: 35upx; line-height: 87.5upx; border-bottom: 1upx solid #eee; }
+.contract { margin: 24upx; display: flex; flex-direction: column; text-align: center; color: #999; font-size: 35upx; }
+.contract > view, .contract > button { margin-top: 18upx; }
+.contract > view:first-child, .contract > button:first-child { margin-top: 0; }
+.signed { color: #09c567; }
+.bottom-space { height: 150upx; }
+.bottom { position: fixed; left: 0; right: 0; bottom: 0; display: flex; padding: 20upx 28upx calc(20upx + env(safe-area-inset-bottom)); background: #fff; }
+.bottom button { flex: 1; border-radius: 50upx; font-size: 35upx; }
+.bottom button + button { margin-left: 18upx; }
+.next { color: #fff; background: #09c567; }.back, .outline { color: #09c567; background: #fff; border: 1upx solid #09c567; }
+</style>

+ 16 - 0
hdApp/src/api/lakala-account/index.js

@@ -0,0 +1,16 @@
+import https from "@/plugins/luch-request_0.0.7/request";
+
+export const accountList = data => https.get("/lakala-account/list", data);
+export const applicationDetail = data => https.get("/lakala-account/detail", data);
+export const createApplication = data => https.post("/lakala-account/create", data);
+export const saveDraft = data => https.post("/lakala-account/save-draft", data, {}, { hideError: true });
+export const deleteDraft = data => https.post("/lakala-account/delete-draft", data);
+export const uploadFile = data => https.post("/lakala-account/upload-file", data);
+export const ocrResult = data => https.get("/lakala-account/ocr-result", data);
+export const aliyunOcr = data => https.post("/lakala-account/aliyun-ocr", data);
+export const getOption = data => https.get("/lakala-account/option", data);
+export const applyContract = data => https.post("/lakala-account/apply-contract", data);
+export const contractStatus = data => https.get("/lakala-account/contract-status", data);
+export const submitApplication = data => https.post("/lakala-account/submit", data);
+export const refreshApplication = data => https.post("/lakala-account/refresh", data);
+export const setCurrentAccount = data => https.post("/lakala-account/set-current", data);

+ 8 - 0
hdApp/src/pages.json

@@ -592,6 +592,14 @@
 				{"path": "video","style": {"navigationBarTitleText": "播放视频"}}
 			]
 		},
+		{
+			"root": "admin/lakala",
+			"pages": [
+				{"path": "lakala","style": {"navigationBarTitleText": "收款账户管理", "enablePullDownRefresh": true}},
+				{"path": "lakalaOnboarding","style": {"navigationBarTitleText": "商户入网"}},
+				{"path": "lakalaAuthorize","style": {"navigationBarTitleText": "商户认证"}}
+			]
+		},
 		{
 			"root": "admin/clear",
 			"pages": [