liujie
2026-08-14 d4470f1be3134cc219b6a529bc22567540d01255
feat(guide): add withdrawal settlement flow
14个文件已添加
5个文件已修改
2982 ■■■■■ 已修改文件
mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/constants/area-codes.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/constants/mock.js 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/docs/开发说明.md 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages.json 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/bank-card-add/index.vue 426 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/bank-card-select/index.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/earnings/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/onboarding/index.vue 687 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/pages/withdrawal/index.vue 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/services/settlement.js 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/services/withdrawal.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/static/icons/chevron-left.svg 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/static/icons/clock-3.svg 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/static/icons/search.svg 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue
@@ -1,10 +1,13 @@
<template>
  <view class="guide-nav-bar" :style="navStyle">
  <view class="guide-nav-bar" :class="'is-' + theme" :style="navStyle">
    <view class="guide-nav-bar__row">
      <button v-if="showBack" class="guide-nav-bar__back" @tap="handleBack">
        <image class="guide-nav-bar__icon" src="/static/icons/chevron-left-white.svg" mode="aspectFit" />
      </button>
      <view v-if="showBack" class="guide-nav-bar__back" @tap="handleBack">
        <image class="guide-nav-bar__icon" :src="backIcon" mode="aspectFit" />
      </view>
      <text class="guide-nav-bar__title">{{ title }}</text>
      <view v-if="actionText" class="guide-nav-bar__action" @tap="handleAction">
        <text class="guide-nav-bar__action-text">{{ actionText }}</text>
      </view>
    </view>
  </view>
</template>
@@ -26,6 +29,14 @@
    backgroundColor: {
      type: String,
      default: '#4d8fe8'
    },
    actionText: {
      type: String,
      default: ''
    },
    theme: {
      type: String,
      default: 'brand'
    }
  },
  data() {
@@ -34,6 +45,18 @@
    }
  },
  computed: {
    isLight() {
      return this.theme === 'light'
    },
    backIcon() {
      return this.isLight ? '/static/icons/chevron-left.svg' : '/static/icons/chevron-left-white.svg'
    },
    barBackground() {
      if (this.isLight && this.backgroundColor === '#4d8fe8') {
        return '#f4f6f9'
      }
      return this.backgroundColor
    },
    navHeightPx() {
      return this.statusBarHeight + upxToPx(98)
    },
@@ -41,13 +64,16 @@
      return {
        paddingTop: `${this.statusBarHeight}px`,
        height: `${this.navHeightPx}px`,
        background: this.backgroundColor
        background: this.barBackground
      }
    }
  },
  methods: {
    handleBack() {
      this.$emit('back')
    },
    handleAction() {
      this.$emit('action')
    }
  }
}
@@ -82,13 +108,7 @@
  width: 88rpx;
  height: 64rpx;
  padding: 0;
  border: 0;
  background: transparent;
  transform: translateY(-50%);
}
.guide-nav-bar__back::after {
  border: 0;
}
.guide-nav-bar__icon {
@@ -97,7 +117,7 @@
}
.guide-nav-bar__title {
  max-width: 460rpx;
  max-width: 360rpx;
  color: #ffffff;
  font-size: 32rpx;
  font-weight: 800;
@@ -105,4 +125,37 @@
  text-overflow: ellipsis;
  white-space: nowrap;
}
.guide-nav-bar__action {
  position: absolute;
  right: 24rpx;
  top: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 104rpx;
  height: 52rpx;
  padding: 0 22rpx;
  border-radius: 10rpx;
  background: #ffffff;
  transform: translateY(-50%);
}
.guide-nav-bar__action-text {
  color: #4d8fe8;
  font-size: 26rpx;
  font-weight: 700;
  line-height: 1;
}
.guide-nav-bar.is-light .guide-nav-bar__title {
  color: #202b3e;
  font-size: 34rpx;
  font-weight: 700;
}
.guide-nav-bar.is-light .guide-nav-bar__icon {
  width: 38rpx;
  height: 38rpx;
}
</style>
mingdaojingjiang_guide_app/constants/area-codes.js
New file
@@ -0,0 +1,16 @@
export const areaProvinceNames = ['北京', '上海', '重庆', '四川省', '广东省']
export const areaProvinceCodes = ['110000', '310000', '500000', '510000', '440000']
export const areaCityNames = [
  ['北京市'],
  ['上海市'],
  ['重庆市'],
  ['成都市', '自贡市', '攀枝花市', '泸州市', '德阳市', '绵阳市', '广元市', '遂宁市', '内江市', '乐山市', '南充市', '眉山市', '宜宾市', '广安市', '达州市', '雅安市', '巴中市', '资阳市'],
  ['广州市', '深圳市', '珠海市', '佛山市', '东莞市']
]
export const areaCityCodes = [
  ['110100'],
  ['310100'],
  ['500100'],
  ['510100', '510300', '510400', '510500', '510600', '510700', '510800', '510900', '511000', '511100', '511300', '511400', '511500', '511600', '511700', '511800', '511900', '512000'],
  ['440100', '440300', '440400', '440600', '441900']
]
mingdaojingjiang_guide_app/constants/mock.js
@@ -142,3 +142,93 @@
  { id: 'E20260616008', title: '拼团 · 精讲', orderNo: 'MJ20260616008', amountCent: 26880, status: '已入账', time: '昨日 16:36' },
  { id: 'E20260615003', title: '包团 · 快讲', orderNo: 'MJ20260615003', amountCent: 32200, status: '已入账', time: '06-15 11:12' }
]
export const GUIDE_WITHDRAWALS_KEY = 'mingdao_guide_withdrawals'
export const GUIDE_ONBOARDING_KEY = 'mingdao_guide_onboarding'
export const GUIDE_BANK_CARDS_KEY = 'mingdao_guide_bank_cards'
export const GUIDE_SELECTED_BANK_KEY = 'mingdao_guide_selected_bank_id'
export const GUIDE_SELECTED_BRANCH_KEY = 'mingdao_guide_selected_branch'
export const withdrawalSummary = {
  availableAmount: 590.80
}
export const initialOnboarding = {
  status: 'unsubmitted',
  reason: '',
  submittedAt: ''
}
export const initialBankCards = [
  {
    id: 'card_001',
    bankName: '招商银行',
    branchName: '招商银行成都高新支行',
    cardMasked: '****5678',
    status: 'approved',
    reason: ''
  },
  {
    id: 'card_002',
    bankName: '中国银行',
    branchName: '中国银行都江堰支行',
    cardMasked: '****1024',
    status: 'pending',
    reason: ''
  },
  {
    id: 'card_003',
    bankName: '工商银行',
    branchName: '工商银行青城山支行',
    cardMasked: '****8899',
    status: 'rejected',
    reason: '开户名与进件主体不一致'
  }
]
export const bankBranches = [
  { id: 'br_001', bankName: '招商银行', branchName: '招商银行成都高新支行', branchCode: 'CMB51010001' },
  { id: 'br_002', bankName: '招商银行', branchName: '招商银行都江堰支行', branchCode: 'CMB51010002' },
  { id: 'br_003', bankName: '中国银行', branchName: '中国银行成都天府支行', branchCode: 'BOC51010001' },
  { id: 'br_004', bankName: '中国银行', branchName: '中国银行都江堰支行', branchCode: 'BOC51010002' },
  { id: 'br_005', bankName: '工商银行', branchName: '工商银行成都春熙路支行', branchCode: 'ICBC51010001' },
  { id: 'br_006', bankName: '工商银行', branchName: '工商银行青城山支行', branchCode: 'ICBC51010002' },
  { id: 'br_007', bankName: '建设银行', branchName: '建设银行成都高新支行', branchCode: 'CCB51010001' },
  { id: 'br_008', bankName: '农业银行', branchName: '农业银行都江堰支行', branchCode: 'ABC51010001' }
]
export const initialWithdrawals = [
  {
    id: 'WD20260808001',
    withdrawalNo: 'WD202608081643001',
    amount: 200.00,
    status: 'APPROVED',
    reason: '',
    bankName: '招商银行',
    cardMasked: '****5678',
    appliedAt: '2026-08-08 16:43:42',
    processedAt: '2026-08-09 09:12:18'
  },
  {
    id: 'WD20260805002',
    withdrawalNo: 'WD202608051020002',
    amount: 86.50,
    status: 'PENDING',
    reason: '',
    bankName: '招商银行',
    cardMasked: '****5678',
    appliedAt: '2026-08-05 10:20:11',
    processedAt: ''
  },
  {
    id: 'WD20260728003',
    withdrawalNo: 'WD202607281530003',
    amount: 120.00,
    status: 'REJECTED',
    reason: '收款账户信息与实名信息不一致',
    bankName: '招商银行',
    cardMasked: '****5678',
    appliedAt: '2026-07-28 15:30:06',
    processedAt: '2026-07-28 18:02:41'
  }
]
mingdaojingjiang_guide_app/docs/开发说明.md
@@ -11,7 +11,15 @@
- `pages/receiving/index.vue`:选择景区、汇合点并开启 / 停止接单。
- `pages/order-detail/index.vue`:订单详情,支持接单、确认客户到场、开始讲解、结束讲解。
- `pages/orders/index.vue`:历史订单页,按全部讲解、接单中、讲解中、已完成筛选。
- `pages/earnings/index.vue`:业绩明细页,展示今日收益、本月收益、筛选栏和收益明细列表。
- `pages/earnings/index.vue`:业绩明细页,展示今日收益、本月收益、筛选栏和收益明细列表;右上角「提现」进入导游提现。
- `pages/withdrawal/index.vue`:导游提现列表,展示当前可提、提现按钮和提现记录;按进件状态跳转进件、审核中或提现详情。
- `pages/onboarding/index.vue`:导游进件表单,支持企业/个人主体资料提交审核。
- `pages/onboarding-pending/index.vue`:进件审核中 / 审核未通过状态页;静态 mock 可通过「刷新审核状态」变为已通过。
- `pages/withdrawal-detail/index.vue`:填写提现金额,并选择 / 更换审核通过的银行卡后提交。
- `pages/bank-card-select/index.vue`:银行卡列表,仅已通过的卡可选用,支持添加银行卡。
- `pages/bank-card-add/index.vue`:填写卡号、开户省份/城市和开户行。
- `pages/bank-branch-select/index.vue`:搜索并选择开户行。
- `pages/withdrawal-success/index.vue`:提现提交成功页。
- `pages/rating/index.vue`:我的评分页,展示综合评分、日期筛选和评价列表。
- `pages/profile/index.vue`:个人中心信息页,展示头像、手机号,并支持更换头像和进入更换手机号流程。
- `pages/change-phone-old/index.vue`:更换手机号第一步,验证旧手机号。
@@ -138,3 +146,6 @@
| 2026-06-17 | 优化历史订单页列表设计:订单状态改为彩色标签,接单中/讲解中/已完成用不同颜色区分,并增强订单信息层级。 |
| 2026-07-28 | 汇合点接入地图展示:为景区汇合点补充经纬度与地址,接单选择页改为真地图预览,首页/订单详情/接单页点击“查看汇合点”后直接拉起系统地图导航。 |
| 2026-07-29 | 导游意见反馈接入 `POST /api/guide/feedback`,提交人身份从 GUIDE Token 获取,反馈统一进入后台意见反馈管理。 |
| 2026-08-14 | 业绩明细右上角增加「提现」入口,复用商户端提现静态流程:列表、填写金额、提交成功;当前为 App 本地 mock。 |
| 2026-08-14 | 补齐提现完整静态流程:导游进件、审核状态、选择/填写银行卡、选择开户行;提现详情支持选择或更换已通过银行卡。 |
| 2026-08-14 | 提现复用页顶栏改为浅色(灰底深色标题),与原商户端一致;业绩明细等导游主流程页仍用蓝底导航。 |
mingdaojingjiang_guide_app/pages.json
@@ -69,6 +69,78 @@
      }
    },
    {
      "path": "pages/withdrawal/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "导游提现",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/withdrawal-detail/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "导游提现",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/withdrawal-success/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "导游提现",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/onboarding/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "导游进件",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/onboarding-pending/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "导游进件",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/bank-card-select/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "选择银行卡",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/bank-card-add/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "填写银行卡",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/bank-branch-select/index",
      "style": {
        "navigationStyle": "custom",
        "navigationBarTitleText": "选择开户行",
        "navigationBarBackgroundColor": "#4d8fe8",
        "navigationBarTextStyle": "white"
      }
    },
    {
      "path": "pages/rating/index",
      "style": {
        "navigationStyle": "custom",
@@ -162,5 +234,15 @@
    "navigationBarBackgroundColor": "#F6F8FB",
    "backgroundColor": "#F6F8FB"
  },
  "uniIdRouter": {}
  "uniIdRouter": {},
    "condition" : { //模式配置,仅开发期间生效
        "current": 0, //当前激活的模式(list 的索引项)
        "list": [
            {
                "name": "", //模式名称
                "path": "", //启动页面,必选
                "query": "" //启动参数,在页面的onLoad函数里面得到
            }
        ]
    }
}
mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue
New file
@@ -0,0 +1,257 @@
<template>
  <view class="page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="选择开户行" @back="goBack" />
    <view class="search-section">
      <view class="search-box">
        <input
          class="search-input"
          v-model="searchKeyword"
          maxlength="30"
          placeholder="搜索银行或支行名称"
          placeholder-class="search-placeholder"
          @input="onSearchInput"
        />
        <image class="search-icon" src="/static/icons/search.svg" mode="aspectFit" />
      </view>
      <text class="city-tip">当前开户城市:{{ cityName }}</text>
    </view>
    <scroll-view class="branch-scroll" scroll-y>
      <view class="branch-card">
        <view v-for="(branch, index) in branches" :key="branch.id" class="branch-item" @tap="selectBranch(branch)">
          <text class="branch-name">{{ branch.branchName }}</text>
          <image class="branch-arrow" src="/static/icons/chevron-right.svg" mode="aspectFit" />
          <view v-if="index < branches.length - 1" class="branch-divider"></view>
        </view>
      </view>
      <view v-if="loading" class="empty-state">
        <text class="empty-title">正在加载开户行</text>
      </view>
      <view v-else-if="loadError" class="empty-state" @tap="loadBranches">
        <text class="empty-title">开户行加载失败</text>
        <text class="empty-tip">点击重新加载</text>
      </view>
      <view v-else-if="!branches.length" class="empty-state">
        <text class="empty-title">未找到匹配的开户行</text>
        <text class="empty-tip">请尝试输入银行简称或支行名称</text>
      </view>
      <view class="safe-space"></view>
    </scroll-view>
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { saveSelectedBranch, searchBankBranches } from '../../services/settlement'
import { backOrHome } from '../../utils/router'
export default {
  components: {
    GuideNavBar
  },
  data() {
    return {
      cityName: '',
      cityCode: '',
      searchKeyword: '',
      branches: [],
      loading: false,
      loadError: false,
      searchTimer: null,
      isLeaving: false
    }
  },
  onLoad(options) {
    if (options && options.city) {
      this.cityName = decodeURIComponent(options.city)
    }
    if (options && options.cityCode) {
      this.cityCode = decodeURIComponent(options.cityCode)
    }
    this.loadBranches()
  },
  onUnload() {
    if (this.searchTimer) {
      clearTimeout(this.searchTimer)
    }
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/bank-card-add/index')
      }, 280)
    },
    onSearchInput() {
      if (this.searchTimer) {
        clearTimeout(this.searchTimer)
      }
      this.searchTimer = setTimeout(() => {
        this.loadBranches()
      }, 250)
    },
    loadBranches() {
      this.loading = true
      this.loadError = false
      searchBankBranches(this.searchKeyword.trim()).then((items) => {
        this.branches = items || []
        this.loading = false
      }).catch(() => {
        this.loading = false
        this.loadError = true
      })
    },
    selectBranch(branch) {
      saveSelectedBranch({
        branchName: branch.branchName,
        bankName: branch.bankName,
        branchCode: branch.branchCode
      })
      uni.navigateBack()
    }
  }
}
</script>
<style scoped>
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.search-section {
  padding: 28rpx 28rpx 24rpx;
  flex: none;
  box-sizing: border-box;
}
.search-box {
  display: flex;
  height: 88rpx;
  padding: 0 24rpx;
  flex-direction: row;
  align-items: center;
  border: 1rpx solid #cfd5dd;
  border-radius: 12rpx;
  background: #ffffff;
  box-sizing: border-box;
}
.search-input {
  flex: 1;
  height: 86rpx;
  color: #1d2939;
  font-size: 27rpx;
}
.search-placeholder {
  color: #a8b1be;
  font-size: 27rpx;
}
.search-icon {
  width: 38rpx;
  height: 38rpx;
  margin-left: 18rpx;
}
.city-tip {
  display: block;
  margin: 18rpx 8rpx 0;
  color: #667085;
  font-size: 23rpx;
}
.branch-scroll {
  flex: 1;
  min-height: 0;
  padding: 0 28rpx;
  box-sizing: border-box;
}
.branch-card {
  overflow: hidden;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
}
.branch-item {
  display: flex;
  min-height: 104rpx;
  padding: 0 26rpx;
  flex-direction: row;
  align-items: center;
  position: relative;
  box-sizing: border-box;
}
.branch-name {
  flex: 1;
  padding: 26rpx 0;
  color: #1d2939;
  font-size: 27rpx;
}
.branch-arrow {
  width: 30rpx;
  height: 30rpx;
  margin-left: 18rpx;
  opacity: 0.58;
}
.branch-divider {
  height: 1rpx;
  position: absolute;
  left: 26rpx;
  right: 26rpx;
  bottom: 0;
  background: #edf0f4;
}
.empty-state {
  display: flex;
  padding: 110rpx 30rpx;
  flex-direction: column;
  align-items: center;
}
.empty-title {
  color: #4b5667;
  font-size: 28rpx;
  font-weight: 700;
}
.empty-tip {
  margin-top: 12rpx;
  color: #9aa4b2;
  font-size: 24rpx;
}
.safe-space {
  height: calc(60rpx + env(safe-area-inset-bottom));
}
</style>
mingdaojingjiang_guide_app/pages/bank-card-add/index.vue
New file
@@ -0,0 +1,426 @@
<template>
  <view class="page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="填写银行卡" @back="goBack" />
    <scroll-view class="form-scroll" scroll-y>
      <view class="form-card">
        <view class="form-row" @tap="showCardTypeTip">
          <text class="field-label">卡类型</text>
          <view class="field-control readonly-control">
            <text class="field-value">本人</text>
            <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
          </view>
        </view>
        <view class="row-divider"></view>
        <view class="field-block">
          <view class="form-row">
            <text class="field-label">卡号</text>
            <view class="field-control input-control" :class="{ 'field-invalid': cardNumberError }">
              <input
                class="card-input"
                type="number"
                maxlength="19"
                v-model="cardNumber"
                placeholder="请输入银行卡号"
                placeholder-class="input-placeholder"
                @input="cardNumberError = ''"
              />
            </view>
          </view>
          <text v-if="cardNumberError" class="field-error">{{ cardNumberError }}</text>
        </view>
        <view class="row-divider"></view>
        <view class="field-block">
          <view class="form-row">
            <text class="field-label">开户省份</text>
            <picker class="region-picker" mode="selector" :range="areaProvinceNames" @change="onProvinceChange">
              <view class="field-control" :class="{ 'field-invalid': cityError && !selectedProvince }">
                <text class="field-value" :class="{ 'field-placeholder': !selectedProvince }">{{ selectedProvince || '选择开户省份' }}</text>
                <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
              </view>
            </picker>
          </view>
        </view>
        <view class="row-divider"></view>
        <view class="field-block">
          <view class="form-row" @tap="handleCityRowTap">
            <text class="field-label">开户城市</text>
            <picker class="region-picker" mode="selector" :range="currentCityNames" :disabled="!selectedProvince" @change="onCityChange">
              <view class="field-control" :class="{ 'field-invalid': cityError && selectedProvince }">
                <text class="field-value" :class="{ 'field-placeholder': !selectedCity }">{{ selectedCity || '选择开户城市' }}</text>
                <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
              </view>
            </picker>
          </view>
          <text v-if="cityError" class="field-error">{{ cityError }}</text>
        </view>
        <view class="row-divider"></view>
        <view class="field-block">
          <view class="form-row" @tap="chooseBranch">
            <text class="field-label">开户行</text>
            <view class="field-control" :class="{ 'field-invalid': branchError }">
              <text class="field-value branch-value" :class="{ 'field-placeholder': !selectedBranch }">{{ selectedBranch || '选择开户行' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </view>
          <text v-if="branchError" class="field-error">{{ branchError }}</text>
        </view>
      </view>
      <view class="security-tip">
        <text>银行卡仅用于导游提现,请填写本人名下银行卡</text>
      </view>
      <view class="submit-button" :class="{ disabled: submitting }" @tap="submitBankCard">
        <text>{{ submitting ? '提交中' : '添加银行卡' }}</text>
      </view>
      <view class="safe-space"></view>
    </scroll-view>
    <guide-dialog
      :visible="confirmVisible"
      title="确认添加银行卡"
      :content="confirmContent"
      confirm-text="确认添加"
      @cancel="confirmVisible = false"
      @confirm="confirmAdd"
    />
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import GuideDialog from '../../components/guide-dialog/guide-dialog.vue'
import { areaCityCodes, areaCityNames, areaProvinceCodes, areaProvinceNames } from '../../constants/area-codes'
import { addBankCard, consumeSelectedBranch } from '../../services/settlement'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar,
    GuideDialog
  },
  data() {
    return {
      areaProvinceNames,
      submitting: false,
      cardNumber: '',
      selectedProvince: '',
      selectedProvinceCode: '',
      selectedCity: '',
      selectedCityCode: '',
      currentCityNames: [],
      currentCityCodes: [],
      selectedBranch: '',
      selectedBankName: '',
      selectedBranchCode: '',
      cardNumberError: '',
      cityError: '',
      branchError: '',
      confirmVisible: false,
      isLeaving: false
    }
  },
  computed: {
    confirmContent() {
      const numberText = this.cardNumber.trim()
      return this.selectedBranch + '\n尾号 ' + numberText.slice(-4)
    }
  },
  onShow() {
    const branch = consumeSelectedBranch()
    if (branch) {
      this.selectedBranch = branch.branchName || ''
      this.selectedBankName = branch.bankName || ''
      this.selectedBranchCode = branch.branchCode || ''
      this.branchError = ''
    }
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/bank-card-select/index')
      }, 280)
    },
    showCardTypeTip() {
      uni.showToast({ title: '当前仅支持本人银行卡', icon: 'none' })
    },
    onProvinceChange(event) {
      const provinceIndex = parseInt(event.detail.value, 10)
      this.selectedProvince = areaProvinceNames[provinceIndex]
      this.selectedProvinceCode = areaProvinceCodes[provinceIndex]
      this.currentCityNames = areaCityNames[provinceIndex] || []
      this.currentCityCodes = areaCityCodes[provinceIndex] || []
      this.selectedCity = ''
      this.selectedCityCode = ''
      this.selectedBranch = ''
      this.selectedBranchCode = ''
      this.cityError = ''
      this.branchError = ''
    },
    onCityChange(event) {
      const cityIndex = parseInt(event.detail.value, 10)
      this.selectedCity = this.currentCityNames[cityIndex]
      this.selectedCityCode = this.currentCityCodes[cityIndex]
      this.selectedBranch = ''
      this.selectedBranchCode = ''
      this.cityError = ''
      this.branchError = ''
    },
    handleCityRowTap() {
      if (!this.selectedProvince) {
        uni.showToast({ title: '请先选择开户省份', icon: 'none' })
      }
    },
    chooseBranch() {
      this.branchError = ''
      if (!this.selectedCity) {
        this.cityError = '请先选择开户城市'
        uni.showToast({ title: '请先选择开户城市', icon: 'none' })
        return
      }
      routeTo(
        '/pages/bank-branch-select/index?city=' +
          encodeURIComponent(this.selectedProvince + ' ' + this.selectedCity) +
          '&cityCode=' +
          encodeURIComponent(this.selectedCityCode)
      )
    },
    submitBankCard() {
      if (this.submitting) {
        return
      }
      this.cardNumberError = ''
      this.cityError = ''
      this.branchError = ''
      let hasError = false
      const numberText = this.cardNumber.trim()
      if (!numberText) {
        this.cardNumberError = '请输入银行卡号'
        hasError = true
      } else if (!(/^\d{16,19}$/).test(numberText)) {
        this.cardNumberError = '请输入 16 至 19 位银行卡号'
        hasError = true
      }
      if (!this.selectedCity) {
        this.cityError = '请选择开户城市'
        hasError = true
      }
      if (!this.selectedProvinceCode || !this.selectedCityCode) {
        this.cityError = '请选择有效的省份和城市'
        hasError = true
      }
      if (!this.selectedBranch || !this.selectedBranchCode) {
        this.branchError = '请选择开户行'
        hasError = true
      }
      if (hasError) {
        return
      }
      this.confirmVisible = true
    },
    confirmAdd() {
      if (this.submitting) {
        return
      }
      this.submitting = true
      addBankCard({
        cardNumber: this.cardNumber.trim(),
        cityCode: this.selectedCityCode,
        bankName: this.selectedBankName,
        branchName: this.selectedBranch,
        branchCode: this.selectedBranchCode
      }).then(() => {
        this.submitting = false
        uni.showToast({ title: '已提交审核', icon: 'success' })
        setTimeout(() => {
          uni.navigateBack()
        }, 500)
      }).catch((error) => {
        this.submitting = false
        uni.showToast({ title: (error && error.message) || '提交失败', icon: 'none' })
      })
    }
  }
}
</script>
<style scoped>
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.form-scroll {
  flex: 1;
  min-height: 0;
  padding: 28rpx 28rpx 0;
  box-sizing: border-box;
}
.form-card {
  padding: 8rpx 30rpx;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
  box-sizing: border-box;
}
.form-row {
  display: flex;
  min-height: 112rpx;
  flex-direction: row;
  align-items: center;
}
.field-block {
  display: flex;
  flex-direction: column;
}
.field-label {
  width: 150rpx;
  flex: none;
  color: #1f3b5b;
  font-size: 28rpx;
  font-weight: 700;
}
.field-control {
  display: flex;
  min-height: 76rpx;
  padding: 0 22rpx;
  flex: 1;
  flex-direction: row;
  align-items: center;
  border: 1rpx solid #cfd5dd;
  border-radius: 12rpx;
  background: #ffffff;
  box-sizing: border-box;
}
.region-picker {
  flex: 1;
}
.readonly-control {
  background: #f0f2f5;
  border-color: #f0f2f5;
}
.input-control {
  padding-right: 14rpx;
}
.field-value {
  flex: 1;
  color: #1d2939;
  font-size: 27rpx;
}
.branch-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.field-placeholder {
  color: #a8b1be;
}
.card-input {
  flex: 1;
  height: 74rpx;
  color: #1d2939;
  font-size: 27rpx;
}
.input-placeholder {
  color: #a8b1be;
  font-size: 27rpx;
}
.down-icon {
  width: 30rpx;
  height: 30rpx;
  margin-left: 12rpx;
  flex: none;
}
.row-divider {
  height: 1rpx;
  margin-left: 150rpx;
  background: #eef1f5;
}
.field-invalid {
  border-color: #f0746d;
  background: #fff9f8;
}
.field-error {
  margin: -8rpx 0 14rpx 150rpx;
  color: #e95b55;
  font-size: 23rpx;
}
.security-tip {
  display: flex;
  margin: 24rpx 16rpx 0;
  color: #8e98a7;
  font-size: 23rpx;
  line-height: 34rpx;
}
.submit-button {
  display: flex;
  width: 100%;
  height: 88rpx;
  margin-top: 58rpx;
  justify-content: center;
  align-items: center;
  border-radius: 12rpx;
  color: #ffffff;
  font-size: 29rpx;
  font-weight: 700;
  background: #4d8fe8;
}
.submit-button.disabled {
  opacity: 0.58;
}
.safe-space {
  height: calc(70rpx + env(safe-area-inset-bottom));
}
</style>
mingdaojingjiang_guide_app/pages/bank-card-select/index.vue
New file
@@ -0,0 +1,186 @@
<template>
  <view class="page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="选择银行卡" @back="goBack" />
    <view class="add" @tap="addCard">添加银行卡</view>
    <scroll-view class="scroll" scroll-y>
      <view v-if="loading" class="state">正在加载银行卡</view>
      <view v-else-if="!cards.length" class="state">暂无银行卡</view>
      <view v-for="card in cards" :key="card.id" class="card" @tap="selectCard(card)">
        <view class="row">
          <text class="name">{{ card.bankName }}</text>
          <text :class="'status ' + card.status">{{ statusText(card.status) }}</text>
        </view>
        <text class="number">{{ card.cardMasked }}</text>
        <text class="branch">{{ card.branchName }}</text>
        <text v-if="card.reason" class="reason">{{ card.reason }}</text>
      </view>
    </scroll-view>
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { getBankCards, saveSelectedBankId } from '../../services/settlement'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar
  },
  data() {
    return {
      loading: false,
      cards: [],
      isLeaving: false
    }
  },
  onShow() {
    this.load()
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/withdrawal-detail/index')
      }, 280)
    },
    addCard() {
      routeTo('/pages/bank-card-add/index')
    },
    statusText(value) {
      if (value === 'approved') return '已通过'
      if (value === 'rejected') return '未通过'
      return '审核中'
    },
    selectCard(card) {
      if (card.status !== 'approved') {
        uni.showToast({
          title: card.reason || '该银行卡尚未审核通过',
          icon: 'none'
        })
        return
      }
      saveSelectedBankId(card.id)
      uni.navigateBack()
    },
    load() {
      this.loading = true
      getBankCards().then((data) => {
        this.cards = data || []
      }).catch((error) => {
        uni.showToast({ title: (error && error.message) || '银行卡加载失败', icon: 'none' })
      }).finally(() => {
        this.loading = false
      })
    }
  }
}
</script>
<style scoped>
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.add {
  height: 88rpx;
  margin: 28rpx 28rpx 24rpx;
  border-radius: 12rpx;
  background: #4d8fe8;
  color: #ffffff;
  text-align: center;
  line-height: 88rpx;
  font-weight: 700;
}
.scroll {
  flex: 1;
  min-height: 0;
  padding: 0 28rpx;
  box-sizing: border-box;
}
.card {
  padding: 28rpx;
  margin-bottom: 16rpx;
  border: 1rpx solid #edf0f4;
  background: #ffffff;
  border-radius: 12rpx;
}
.row {
  display: flex;
  justify-content: space-between;
}
.name {
  font-size: 30rpx;
  font-weight: 700;
  color: #1f3b5b;
}
.number {
  display: block;
  margin-top: 20rpx;
  font-size: 28rpx;
  color: #344054;
}
.branch,
.reason {
  display: block;
  margin-top: 10rpx;
  color: #667085;
  font-size: 23rpx;
}
.reason {
  color: #e95b5b;
}
.status {
  font-size: 23rpx;
}
.approved {
  color: #249765;
}
.pending {
  color: #e58b28;
}
.rejected {
  color: #e95b5b;
}
.state {
  padding: 150rpx 0;
  text-align: center;
  color: #8993a1;
}
</style>
mingdaojingjiang_guide_app/pages/earnings/index.vue
@@ -1,6 +1,6 @@
<template>
  <view class="performance-page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="业绩明细" @back="goBack" />
    <guide-nav-bar title="业绩明细" action-text="提现" @back="goBack" @action="goWithdraw" />
    <view class="summary-area">
      <view class="income-card">
@@ -50,7 +50,7 @@
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { getEarnings } from '../../services/order'
import { backOrHome } from '../../utils/router'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
@@ -104,6 +104,9 @@
        backOrHome()
      }, 280)
    },
    goWithdraw() {
      routeTo('/pages/withdrawal/index')
    },
    loadData() {
      if (this.loading) {
        return
mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue
New file
@@ -0,0 +1,223 @@
<template>
  <view class="page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="导游进件" @back="goBack" />
    <view class="result-content">
      <view class="status-icon-wrap" :class="{ rejected: isRejected, approved: status === 'approved' }">
        <image class="status-icon" :src="statusIcon" mode="aspectFit" />
      </view>
      <text class="result-title">{{ resultTitle }}</text>
      <text v-if="isRejected && reason" class="result-reason">{{ reason }}</text>
      <text v-else class="result-tip">{{ resultTip }}</text>
      <view v-if="isRejected" class="return-button" @tap="resubmit">
        <text>重新提交资料</text>
      </view>
      <view v-else-if="status === 'approved'" class="return-button" @tap="goWithdraw">
        <text>去提现</text>
      </view>
      <view v-else class="return-button ghost" :class="{ disabled: refreshing }" @tap="refreshStatus">
        <text>{{ refreshing ? '刷新中' : '刷新审核状态' }}</text>
      </view>
      <view class="return-button" :class="{ secondary: isRejected || status === 'approved' }" @tap="goBack">
        <text>返回提现</text>
      </view>
    </view>
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { getOnboardingStatus, refreshOnboardingStatus } from '../../services/settlement'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar
  },
  data() {
    return {
      status: 'pending',
      reason: '',
      refreshing: false,
      isLeaving: false
    }
  },
  computed: {
    isRejected() {
      return this.status === 'rejected'
    },
    resultTitle() {
      if (this.isRejected) return '进件审核未通过'
      if (this.status === 'approved') return '进件审核已通过'
      return '进件信息审核中'
    },
    resultTip() {
      if (this.status === 'approved') return '审核已通过,可以继续添加银行卡和提现'
      return '审核通过后才能继续添加银行卡和提现'
    },
    statusIcon() {
      if (this.isRejected) return '/static/icons/message-warning.svg'
      if (this.status === 'approved') return '/static/icons/shield-check.svg'
      return '/static/icons/clock-3.svg'
    }
  },
  onShow() {
    this.loadStatus()
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/withdrawal/index')
      }, 280)
    },
    loadStatus() {
      getOnboardingStatus().then((item) => {
        this.status = (item && item.status) || 'pending'
        this.reason = (item && item.reason) || ''
      })
    },
    refreshStatus() {
      if (this.refreshing || this.isRejected) {
        return
      }
      this.refreshing = true
      refreshOnboardingStatus().then((item) => {
        this.refreshing = false
        this.status = (item && item.status) || this.status
        this.reason = (item && item.reason) || ''
        if (this.status === 'approved') {
          uni.showToast({ title: '审核已通过', icon: 'success' })
        } else {
          uni.showToast({ title: '仍在审核中', icon: 'none' })
        }
      }).catch(() => {
        this.refreshing = false
        uni.showToast({ title: '刷新失败,请重试', icon: 'none' })
      })
    },
    resubmit() {
      routeTo('/pages/onboarding/index')
    },
    goWithdraw() {
      routeTo('/pages/withdrawal-detail/index')
    }
  }
}
</script>
<style scoped>
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.result-content {
  display: flex;
  padding: 76rpx 48rpx 0;
  flex: 1;
  flex-direction: column;
  align-items: center;
  box-sizing: border-box;
}
.status-icon-wrap {
  display: flex;
  width: 176rpx;
  height: 176rpx;
  justify-content: center;
  align-items: center;
  border-radius: 88rpx;
  background: #eef6ff;
}
.status-icon-wrap.rejected {
  background: #fff1f2;
}
.status-icon-wrap.approved {
  background: #eafaf0;
}
.status-icon {
  width: 82rpx;
  height: 82rpx;
}
.result-title {
  margin-top: 40rpx;
  color: #1f3b5b;
  font-size: 34rpx;
  font-weight: 700;
}
.result-tip,
.result-reason {
  margin-top: 16rpx;
  color: #667085;
  font-size: 25rpx;
  text-align: center;
  line-height: 1.5;
}
.result-reason {
  color: #e95b5b;
}
.return-button {
  display: flex;
  width: 100%;
  height: 88rpx;
  margin-top: 28rpx;
  justify-content: center;
  align-items: center;
  border-radius: 12rpx;
  color: #ffffff;
  font-size: 29rpx;
  font-weight: 700;
  background: #4d8fe8;
}
.return-button.ghost {
  margin-top: 70rpx;
  color: #4d8fe8;
  background: #ffffff;
  box-shadow: none;
  border: 1rpx solid #4d8fe8;
}
.return-button.secondary {
  margin-top: 24rpx;
  color: #4d8fe8;
  background: #ffffff;
  box-shadow: none;
  border: 1rpx solid #4d8fe8;
}
.return-button.disabled {
  opacity: 0.58;
}
</style>
mingdaojingjiang_guide_app/pages/onboarding/index.vue
New file
@@ -0,0 +1,687 @@
<template>
  <view class="page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="导游进件" @back="goBack" />
    <scroll-view class="form-scroll" scroll-y>
      <view class="form-card subject-card">
        <view class="merchant-type-row" @tap="chooseMerchantType">
          <view class="type-copy">
            <text class="field-label inline-label">进件类型</text>
          </view>
          <view class="type-control">
            <text>{{ merchantType === 'enterprise' ? '企业' : '个人' }}</text>
            <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
          </view>
        </view>
      </view>
      <view v-if="merchantType === 'enterprise'" class="form-card">
        <text class="section-title">企业资料</text>
        <view class="field-block">
          <text class="field-label">统一社会信用代码</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'creditCode' }">
            <input
              class="text-input"
              v-model="creditCode"
              maxlength="18"
              placeholder="请输入18位统一社会信用代码"
              placeholder-class="input-placeholder"
              @input="normalizeCreditCode"
            />
          </view>
          <text v-if="invalidField === 'creditCode'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">营业执照有效期</text>
          <view class="select-shell" :class="{ 'field-invalid': invalidField === 'businessValidity' }" @tap="chooseBusinessValidity">
            <text>{{ businessValidity }}</text>
            <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
          </view>
          <text v-if="invalidField === 'businessValidity'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">证照有效期起始日期</text>
          <picker class="date-picker" mode="date" :value="businessStartDate" start="1990-01-01" end="2099-12-31" @change="onBusinessStartChange">
            <view class="select-shell" :class="{ 'field-invalid': invalidField === 'businessStartDate' }">
              <text :class="{ 'select-placeholder': !businessStartDate }">{{ businessStartDate || '请选择' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </picker>
          <text v-if="invalidField === 'businessStartDate'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view v-if="businessValidity === '固定期限'" class="field-block">
          <text class="field-label">证照有效期结束日期</text>
          <picker class="date-picker" mode="date" :value="businessEndDate" start="1990-01-01" end="2099-12-31" @change="onBusinessEndChange">
            <view class="select-shell" :class="{ 'field-invalid': invalidField === 'businessEndDate' }">
              <text :class="{ 'select-placeholder': !businessEndDate }">{{ businessEndDate || '请选择' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </picker>
          <text v-if="invalidField === 'businessEndDate'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">注册地址(省、市、区)</text>
          <picker class="region-picker" mode="region" @change="onRegisteredRegionChange">
            <view class="select-shell" :class="{ 'field-invalid': invalidField === 'registeredRegion' }">
              <text class="ellipsis-text" :class="{ 'select-placeholder': !registeredRegion }">{{ registeredRegion || '请选择' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </picker>
          <text v-if="invalidField === 'registeredRegion'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block last-field">
          <text class="field-label">详细注册地址</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'registeredAddress' }">
            <input
              class="text-input"
              v-model="registeredAddress"
              maxlength="80"
              placeholder="请输入详细注册地址"
              placeholder-class="input-placeholder"
              @input="clearFieldError('registeredAddress')"
            />
          </view>
          <text v-if="invalidField === 'registeredAddress'" class="field-error">{{ errorMessage }}</text>
        </view>
      </view>
      <view class="form-card">
        <text class="section-title">{{ merchantType === 'enterprise' ? '法人信息' : '个人信息' }}</text>
        <view class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人姓名' : '个人姓名' }}</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'subjectName' }">
            <input
              class="text-input"
              v-model="subjectName"
              maxlength="30"
              placeholder="请输入姓名"
              placeholder-class="input-placeholder"
              @input="clearFieldError('subjectName')"
            />
          </view>
          <text v-if="invalidField === 'subjectName'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人证件类型' : '个人证件类型' }}</text>
          <view class="select-shell readonly-shell">
            <text>身份证</text>
            <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
          </view>
        </view>
        <view class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人证件号码' : '个人证件号码' }}</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'subjectIdNumber' }">
            <input
              class="text-input"
              type="idcard"
              v-model="subjectIdNumber"
              maxlength="18"
              placeholder="请输入身份证号码"
              placeholder-class="input-placeholder"
              @input="clearFieldError('subjectIdNumber')"
            />
          </view>
          <text v-if="invalidField === 'subjectIdNumber'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人证件有效期类型' : '个人证件有效期类型' }}</text>
          <view class="select-shell" :class="{ 'field-invalid': invalidField === 'subjectValidity' }" @tap="chooseSubjectValidity">
            <text>{{ subjectValidity }}</text>
            <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
          </view>
          <text v-if="invalidField === 'subjectValidity'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人证件有效期开始日期' : '个人证件有效期开始日期' }}</text>
          <picker class="date-picker" mode="date" :value="subjectStartDate" start="1990-01-01" end="2099-12-31" @change="onSubjectStartChange">
            <view class="select-shell" :class="{ 'field-invalid': invalidField === 'subjectStartDate' }">
              <text :class="{ 'select-placeholder': !subjectStartDate }">{{ subjectStartDate || '请选择' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </picker>
          <text v-if="invalidField === 'subjectStartDate'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view v-if="subjectValidity === '固定期限'" class="field-block">
          <text class="field-label">{{ merchantType === 'enterprise' ? '法人证件有效期截止日期' : '个人证件有效期截止日期' }}</text>
          <picker class="date-picker" mode="date" :value="subjectEndDate" start="1990-01-01" end="2099-12-31" @change="onSubjectEndChange">
            <view class="select-shell" :class="{ 'field-invalid': invalidField === 'subjectEndDate' }">
              <text :class="{ 'select-placeholder': !subjectEndDate }">{{ subjectEndDate || '请选择' }}</text>
              <image class="down-icon" src="/static/icons/chevron-down.svg" mode="aspectFit" />
            </view>
          </picker>
          <text v-if="invalidField === 'subjectEndDate'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view v-if="merchantType === 'individual'" class="field-block last-field">
          <text class="field-label">手机号</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'personalPhone' }">
            <input
              class="text-input"
              type="number"
              v-model="personalPhone"
              maxlength="11"
              placeholder="请输入手机号"
              placeholder-class="input-placeholder"
              @input="clearFieldError('personalPhone')"
            />
          </view>
          <text v-if="invalidField === 'personalPhone'" class="field-error">{{ errorMessage }}</text>
        </view>
      </view>
      <view v-if="merchantType === 'enterprise'" class="form-card">
        <text class="section-title">管理员信息</text>
        <view class="field-block">
          <text class="field-label">管理员姓名</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'administratorName' }">
            <input
              class="text-input"
              v-model="administratorName"
              maxlength="30"
              placeholder="请输入管理员姓名"
              placeholder-class="input-placeholder"
              @input="clearFieldError('administratorName')"
            />
          </view>
          <text v-if="invalidField === 'administratorName'" class="field-error">{{ errorMessage }}</text>
        </view>
        <view class="field-block last-field">
          <text class="field-label">管理员手机号</text>
          <view class="input-shell" :class="{ 'field-invalid': invalidField === 'administratorPhone' }">
            <input
              class="text-input"
              type="number"
              v-model="administratorPhone"
              maxlength="11"
              placeholder="请输入管理员手机号"
              placeholder-class="input-placeholder"
              @input="clearFieldError('administratorPhone')"
            />
          </view>
          <text v-if="invalidField === 'administratorPhone'" class="field-error">{{ errorMessage }}</text>
        </view>
      </view>
      <view class="submit-button" :class="{ disabled: submitting }" @tap="submitOnboarding">
        <text>{{ submitting ? '提交中' : '提交审核' }}</text>
      </view>
      <view class="safe-space"></view>
    </scroll-view>
    <guide-dialog
      :visible="confirmVisible"
      title="确认提交审核"
      content="提交后资料将进入审核,审核期间暂不能发起提现。"
      confirm-text="确认提交"
      @cancel="confirmVisible = false"
      @confirm="confirmSubmit"
    />
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import GuideDialog from '../../components/guide-dialog/guide-dialog.vue'
import { submitOnboarding } from '../../services/settlement'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar,
    GuideDialog
  },
  data() {
    return {
      submitting: false,
      merchantType: 'enterprise',
      creditCode: '',
      businessValidity: '固定期限',
      businessStartDate: '',
      businessEndDate: '',
      registeredRegion: '',
      registeredProvinceCode: '',
      registeredCityCode: '',
      registeredDistrictCode: '',
      registeredAddress: '',
      subjectName: '',
      subjectIdNumber: '',
      subjectValidity: '固定期限',
      subjectStartDate: '',
      subjectEndDate: '',
      administratorName: '',
      administratorPhone: '',
      personalPhone: '',
      invalidField: '',
      errorMessage: '',
      confirmVisible: false,
      isLeaving: false,
      merchantTypeOptions: ['企业', '个人'],
      validityOptions: ['固定期限', '长期']
    }
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/withdrawal/index')
      }, 280)
    },
    setFieldError(field, message) {
      this.invalidField = field
      this.errorMessage = message
    },
    clearFieldError(field) {
      if (this.invalidField === field) {
        this.invalidField = ''
        this.errorMessage = ''
      }
    },
    normalizeCreditCode(event) {
      const value = (event && event.detail && event.detail.value) || this.creditCode
      this.creditCode = String(value || '').toUpperCase().replace(/[^0-9A-Z]/g, '')
      this.clearFieldError('creditCode')
    },
    chooseMerchantType() {
      uni.showActionSheet({
        itemList: this.merchantTypeOptions,
        success: (result) => {
          this.merchantType = result.tapIndex === 0 ? 'enterprise' : 'individual'
          this.invalidField = ''
          this.errorMessage = ''
        }
      })
    },
    chooseBusinessValidity() {
      uni.showActionSheet({
        itemList: this.validityOptions,
        success: (result) => {
          this.businessValidity = this.validityOptions[result.tapIndex]
          if (this.businessValidity === '长期') {
            this.businessEndDate = ''
          }
          this.clearFieldError('businessValidity')
        }
      })
    },
    chooseSubjectValidity() {
      uni.showActionSheet({
        itemList: this.validityOptions,
        success: (result) => {
          this.subjectValidity = this.validityOptions[result.tapIndex]
          if (this.subjectValidity === '长期') {
            this.subjectEndDate = ''
          }
          this.clearFieldError('subjectValidity')
        }
      })
    },
    onRegisteredRegionChange(event) {
      const values = (event.detail && event.detail.value) || []
      const codes = (event.detail && event.detail.code) || []
      if (values.length !== 3) {
        return
      }
      this.registeredRegion = values[0] + ' ' + values[1] + ' ' + values[2]
      this.registeredProvinceCode = codes[0] || ''
      this.registeredCityCode = codes[1] || ''
      this.registeredDistrictCode = codes[2] || ''
      this.clearFieldError('registeredRegion')
    },
    onBusinessStartChange(event) {
      this.businessStartDate = event.detail.value
      this.clearFieldError('businessStartDate')
    },
    onBusinessEndChange(event) {
      this.businessEndDate = event.detail.value
      this.clearFieldError('businessEndDate')
    },
    onSubjectStartChange(event) {
      this.subjectStartDate = event.detail.value
      this.clearFieldError('subjectStartDate')
    },
    onSubjectEndChange(event) {
      this.subjectEndDate = event.detail.value
      this.clearFieldError('subjectEndDate')
    },
    isPhoneValid(phone) {
      return /^1[3-9]\d{9}$/.test(phone)
    },
    validateCommonIdentity() {
      if (!this.subjectName.trim()) {
        this.setFieldError('subjectName', this.merchantType === 'enterprise' ? '请输入法人姓名' : '请输入个人姓名')
        return false
      }
      if (!(/^\d{17}[\dXx]$/).test(this.subjectIdNumber.trim())) {
        this.setFieldError('subjectIdNumber', '请输入正确的18位身份证号码')
        return false
      }
      if (!this.subjectStartDate) {
        this.setFieldError('subjectStartDate', '请选择证件有效期开始日期')
        return false
      }
      if (this.subjectValidity === '固定期限') {
        if (!this.subjectEndDate) {
          this.setFieldError('subjectEndDate', '请选择证件有效期截止日期')
          return false
        }
        if (this.subjectStartDate > this.subjectEndDate) {
          this.setFieldError('subjectEndDate', '证件截止日期不能早于开始日期')
          return false
        }
      }
      return true
    },
    validateEnterprise() {
      if (!(/^[0-9A-Z]{18}$/).test(this.creditCode.trim())) {
        this.setFieldError('creditCode', '请输入正确的18位统一社会信用代码')
        return false
      }
      if (!this.businessStartDate) {
        this.setFieldError('businessStartDate', '请选择营业执照有效期开始日期')
        return false
      }
      if (this.businessValidity === '固定期限') {
        if (!this.businessEndDate) {
          this.setFieldError('businessEndDate', '请选择营业执照有效期结束日期')
          return false
        }
        if (this.businessStartDate > this.businessEndDate) {
          this.setFieldError('businessEndDate', '证照结束日期不能早于开始日期')
          return false
        }
      }
      if (!this.registeredRegion) {
        this.setFieldError('registeredRegion', '请选择注册地址')
        return false
      }
      if (this.registeredAddress.trim().length < 5) {
        this.setFieldError('registeredAddress', '请输入完整的详细注册地址')
        return false
      }
      if (!this.validateCommonIdentity()) {
        return false
      }
      if (!this.administratorName.trim()) {
        this.setFieldError('administratorName', '请输入管理员姓名')
        return false
      }
      if (!this.isPhoneValid(this.administratorPhone.trim())) {
        this.setFieldError('administratorPhone', '请输入正确的管理员手机号')
        return false
      }
      return true
    },
    validateIndividual() {
      if (!this.validateCommonIdentity()) {
        return false
      }
      if (!this.isPhoneValid(this.personalPhone.trim())) {
        this.setFieldError('personalPhone', '请输入正确的手机号')
        return false
      }
      return true
    },
    submitOnboarding() {
      if (this.submitting) {
        return
      }
      this.invalidField = ''
      this.errorMessage = ''
      const valid = this.merchantType === 'enterprise' ? this.validateEnterprise() : this.validateIndividual()
      if (!valid) {
        uni.showToast({ title: this.errorMessage, icon: 'none' })
        return
      }
      this.confirmVisible = true
    },
    confirmSubmit() {
      if (this.submitting) {
        return
      }
      this.submitting = true
      const contactName = this.merchantType === 'enterprise' ? this.administratorName.trim() : this.subjectName.trim()
      const contactMobile = this.merchantType === 'enterprise' ? this.administratorPhone.trim() : this.personalPhone.trim()
      submitOnboarding({
        subjectType: this.merchantType,
        subjectName: this.subjectName.trim(),
        creditCode: this.creditCode.trim(),
        subjectIdNumber: this.subjectIdNumber.trim(),
        subjectCertificateValidityType: this.subjectValidity === '长期' ? 'long_term' : 'fixed',
        subjectCertificateBeginDate: this.subjectStartDate,
        subjectCertificateEndDate: this.subjectEndDate,
        licenseValidityType: this.businessValidity === '长期' ? 'long_term' : 'fixed',
        licenseBeginDate: this.businessStartDate,
        licenseEndDate: this.businessEndDate,
        registeredRegion: this.registeredRegion.trim(),
        registeredProvinceCode: this.registeredProvinceCode,
        registeredCityCode: this.registeredCityCode,
        registeredDistrictCode: this.registeredDistrictCode,
        registeredAddress: this.registeredAddress.trim(),
        contactName,
        contactMobile
      }).then(() => {
        this.submitting = false
        routeTo('/pages/onboarding-pending/index', 'redirectTo')
      }).catch((error) => {
        this.submitting = false
        uni.showToast({ title: (error && error.message) || '提交失败,请重试', icon: 'none' })
      })
    }
  }
}
</script>
<style scoped>
.page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  min-height: 0;
  background: #f6f8fb;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  overflow: hidden;
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.form-scroll {
  flex: 1;
  height: 0;
  min-height: 0;
  padding: 0 28rpx;
  box-sizing: border-box;
}
.form-card {
  margin-top: 26rpx;
  padding: 30rpx;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
  box-sizing: border-box;
}
.subject-card {
  margin-top: 24rpx;
  padding-top: 20rpx;
  padding-bottom: 20rpx;
}
.merchant-type-row {
  display: flex;
  min-height: 92rpx;
  flex-direction: row;
  align-items: center;
}
.type-copy {
  display: flex;
  flex: 1;
  flex-direction: column;
}
.inline-label {
  margin-bottom: 0;
}
.type-control {
  display: flex;
  width: 190rpx;
  height: 72rpx;
  padding: 0 18rpx 0 22rpx;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  border: 1rpx solid #e1e6ed;
  border-radius: 12rpx;
  color: #1d2939;
  font-size: 27rpx;
  background: #f7f9fc;
  box-sizing: border-box;
}
.section-title {
  display: block;
  margin-bottom: 28rpx;
  color: #1f3b5b;
  font-size: 31rpx;
  font-weight: 700;
}
.field-block {
  display: flex;
  margin-bottom: 26rpx;
  flex-direction: column;
}
.last-field {
  margin-bottom: 0;
}
.field-label {
  display: block;
  margin-bottom: 13rpx;
  color: #344054;
  font-size: 27rpx;
  font-weight: 600;
}
.input-shell,
.select-shell {
  display: flex;
  width: 100%;
  height: 82rpx;
  padding: 0 22rpx;
  flex-direction: row;
  align-items: center;
  border: 1rpx solid #cfd5dd;
  border-radius: 12rpx;
  background: #ffffff;
  box-sizing: border-box;
}
.text-input {
  flex: 1;
  height: 80rpx;
  color: #1d2939;
  font-size: 27rpx;
}
.input-placeholder,
.select-placeholder {
  color: #a8b1be;
  font-size: 26rpx;
}
.select-shell {
  justify-content: space-between;
  color: #1d2939;
  font-size: 27rpx;
}
.readonly-shell {
  color: #5a6472;
  background: #f0f2f5;
  border-color: #f0f2f5;
}
.date-picker,
.region-picker {
  width: 100%;
}
.down-icon {
  width: 30rpx;
  height: 30rpx;
  margin-left: 14rpx;
  flex: none;
}
.ellipsis-text {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.field-invalid {
  border-color: #f0746d;
  background: #fff9f8;
}
.field-error {
  margin-top: 9rpx;
  color: #e95b55;
  font-size: 23rpx;
}
.submit-button {
  display: flex;
  height: 88rpx;
  margin-top: 34rpx;
  justify-content: center;
  align-items: center;
  border-radius: 12rpx;
  color: #ffffff;
  font-size: 29rpx;
  font-weight: 700;
  background: #4d8fe8;
}
.submit-button.disabled {
  opacity: 0.58;
}
.safe-space {
  height: calc(70rpx + env(safe-area-inset-bottom));
}
</style>
mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue
New file
@@ -0,0 +1,342 @@
<template>
  <view class="detail-page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="导游提现" @back="goBack" />
    <scroll-view class="form-scroll" scroll-y>
      <view class="form-card">
        <view class="available-row">
          <text class="available-label">当前可提</text>
          <text class="available-value">¥{{ availableText }}</text>
        </view>
        <view class="field-block">
          <view class="field-row">
            <text class="field-label">提现金额</text>
            <view class="amount-shell" :class="{ invalid: error }">
              <text class="amount-prefix">¥</text>
              <input
                class="amount-input"
                v-model="amount"
                type="digit"
                maxlength="10"
                placeholder="请输入"
                placeholder-class="input-placeholder"
                @input="clearError"
              />
            </view>
          </view>
          <text v-if="error" class="field-error">{{ error }}</text>
        </view>
        <view class="field-divider"></view>
        <view class="field-row bank-row">
          <text class="field-label">提现银行卡</text>
          <view class="bank-value">
            <view v-if="selected" class="selected-bank">
              <text class="bank-name">{{ selected.bankName }}</text>
              <text class="card-number">{{ selected.cardMasked }}</text>
            </view>
            <text v-else class="bank-placeholder">请选择银行卡</text>
          </view>
          <view class="select-bank-button" @tap="chooseBank">
            <text>{{ selected ? '更换' : '选择' }}</text>
          </view>
        </view>
      </view>
      <view class="submit-button" :class="{ disabled: submitting }" @tap="submit">
        <text>{{ submitting ? '提交中' : '发起提现' }}</text>
      </view>
    </scroll-view>
    <guide-dialog
      :visible="confirmVisible"
      title="确认提现"
      :content="confirmContent"
      confirm-text="确认提现"
      @cancel="confirmVisible = false"
      @confirm="confirmSubmit"
    />
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import GuideDialog from '../../components/guide-dialog/guide-dialog.vue'
import { consumeSelectedBankId, getBankCards } from '../../services/settlement'
import { createWithdrawal, getWithdrawalSummary } from '../../services/withdrawal'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar,
    GuideDialog
  },
  data() {
    return {
      amount: '',
      availableAmount: 0,
      cards: [],
      selectedId: '',
      error: '',
      submitting: false,
      confirmVisible: false,
      isLeaving: false
    }
  },
  computed: {
    availableText() {
      return Number(this.availableAmount || 0).toFixed(2)
    },
    selected() {
      return this.cards.find((item) => item.id === this.selectedId) || null
    },
    confirmContent() {
      const bankName = (this.selected && this.selected.bankName) || '已绑定银行卡'
      return '确认提现 ¥' + Number(this.amount || 0).toFixed(2) + ' 至' + bankName + '吗?'
    }
  },
  onShow() {
    this.loadSummary()
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/withdrawal/index')
      }, 280)
    },
    clearError() {
      this.error = ''
    },
    chooseBank() {
      routeTo('/pages/bank-card-select/index')
    },
    loadSummary() {
      getWithdrawalSummary().then((summary) => {
        this.availableAmount = Number(summary && summary.availableAmount || 0)
      }).catch((error) => {
        this.error = error.message || '可提金额加载失败,请稍后重试'
      })
      getBankCards().then((items) => {
        this.cards = items || []
      }).catch((error) => {
        this.error = error.message || '银行卡加载失败,请稍后重试'
      })
      const selectedId = consumeSelectedBankId()
      if (selectedId) {
        this.selectedId = selectedId
      }
    },
    submit() {
      if (this.submitting) {
        return
      }
      const value = Number(this.amount)
      const cents = Math.round(value * 100)
      const availableCents = Math.round(Number(this.availableAmount || 0) * 100)
      if (!Number.isFinite(value) || cents <= 0 || cents > availableCents) {
        this.error = '请输入不超过当前可提金额的提现金额'
        return
      }
      if (!this.selected) {
        this.error = '请选择审核通过的银行卡'
        return
      }
      this.error = ''
      this.confirmVisible = true
    },
    confirmSubmit() {
      if (this.submitting) {
        return
      }
      this.submitting = true
      createWithdrawal(Number(Number(this.amount).toFixed(2)), this.selected).then((created) => {
        this.submitting = false
        uni.redirectTo({
          url: '/pages/withdrawal-success/index?withdrawalNo=' + encodeURIComponent((created && created.withdrawalNo) || '')
        })
      }).catch((error) => {
        this.submitting = false
        this.error = error.message || '提现提交失败,请重试'
      })
    }
  }
}
</script>
<style scoped>
.detail-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.detail-page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.form-scroll {
  flex: 1;
  min-height: 0;
  padding: 28rpx 28rpx 40rpx;
  box-sizing: border-box;
}
.form-card {
  padding: 10rpx 28rpx 14rpx;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
}
.available-row {
  display: flex;
  align-items: center;
  height: 72rpx;
}
.available-label {
  color: #667085;
  font-size: 25rpx;
}
.available-value {
  margin-left: 12rpx;
  color: #1f3b5b;
  font-size: 27rpx;
  font-weight: 700;
}
.field-row {
  display: flex;
  min-height: 104rpx;
  align-items: center;
}
.field-label {
  width: 178rpx;
  flex: none;
  color: #344054;
  font-size: 27rpx;
}
.amount-shell {
  display: flex;
  height: 70rpx;
  padding: 0 18rpx;
  flex: 1;
  align-items: center;
  border: 1rpx solid #cfd5dd;
  border-radius: 12rpx;
  box-sizing: border-box;
}
.amount-shell.invalid {
  border-color: #f0746d;
  background: #fff9f8;
}
.amount-prefix {
  color: #8b94a2;
  font-size: 27rpx;
}
.amount-input {
  height: 68rpx;
  margin-left: 8rpx;
  flex: 1;
  color: #1d2939;
  font-size: 27rpx;
}
.input-placeholder {
  color: #b5bdc9;
  font-size: 25rpx;
}
.field-error {
  margin: 0 0 14rpx 178rpx;
  color: #e95b55;
  font-size: 22rpx;
}
.field-divider {
  height: 1rpx;
  margin-left: 178rpx;
  background: #edf0f4;
}
.bank-value {
  min-width: 0;
  flex: 1;
}
.selected-bank {
  display: flex;
  flex-direction: column;
}
.bank-name {
  color: #1d2939;
  font-size: 25rpx;
}
.card-number {
  margin-top: 4rpx;
  color: #8a93a1;
  font-size: 22rpx;
}
.bank-placeholder {
  color: #b5bdc9;
  font-size: 25rpx;
}
.select-bank-button {
  display: flex;
  width: 84rpx;
  height: 52rpx;
  margin-left: 14rpx;
  flex: none;
  align-items: center;
  justify-content: center;
  border: 1rpx solid #58a5f8;
  border-radius: 12rpx;
  color: #318cf0;
  background: #ffffff;
  font-size: 23rpx;
}
.submit-button {
  display: flex;
  width: 100%;
  height: 88rpx;
  margin-top: 34rpx;
  align-items: center;
  justify-content: center;
  border-radius: 12rpx;
  color: #ffffff;
  background: #4d8fe8;
  font-size: 30rpx;
  font-weight: 700;
}
.submit-button.disabled {
  opacity: 0.58;
}
</style>
mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue
New file
@@ -0,0 +1,114 @@
<template>
  <view class="success-page">
    <guide-nav-bar title="导游提现" @back="returnToWithdrawal" />
    <view class="result-content">
      <view class="success-icon-shell">
        <image class="success-icon" src="/static/icons/shield-check.svg" mode="aspectFit" />
      </view>
      <text class="result-title">提交成功,等待审核</text>
      <text v-if="withdrawalNo" class="withdrawal-no">提现单号:{{ withdrawalNo }}</text>
      <text class="result-tip">审核通过后将发起到账处理,请留意提现记录状态。</text>
      <view class="return-button" @tap="returnToWithdrawal">
        <text>返回</text>
      </view>
    </view>
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { backOrHome } from '../../utils/router'
export default {
  components: {
    GuideNavBar
  },
  data() {
    return {
      withdrawalNo: ''
    }
  },
  onLoad(options) {
    this.withdrawalNo = options && options.withdrawalNo ? String(options.withdrawalNo) : ''
  },
  methods: {
    returnToWithdrawal() {
      uni.navigateBack({
        fail() {
          backOrHome('/pages/withdrawal/index')
        }
      })
    }
  }
}
</script>
<style scoped>
.success-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding-bottom: env(safe-area-inset-bottom);
}
.result-content {
  display: flex;
  flex: 1;
  padding: 96rpx 48rpx 0;
  flex-direction: column;
  align-items: center;
  box-sizing: border-box;
}
.success-icon-shell {
  display: flex;
  width: 140rpx;
  height: 140rpx;
  align-items: center;
  justify-content: center;
  border-radius: 70rpx;
  background: #eef6ff;
}
.success-icon {
  width: 72rpx;
  height: 72rpx;
}
.result-title {
  margin-top: 42rpx;
  color: #1f3b5b;
  font-size: 30rpx;
  font-weight: 700;
}
.withdrawal-no {
  margin-top: 22rpx;
  color: #667085;
  font-size: 25rpx;
}
.result-tip {
  margin-top: 14rpx;
  color: #667085;
  font-size: 23rpx;
  text-align: center;
  line-height: 34rpx;
}
.return-button {
  display: flex;
  width: 100%;
  height: 88rpx;
  margin-top: 86rpx;
  align-items: center;
  justify-content: center;
  border-radius: 12rpx;
  color: #ffffff;
  background: #4d8fe8;
  font-size: 29rpx;
  font-weight: 700;
}
</style>
mingdaojingjiang_guide_app/pages/withdrawal/index.vue
New file
@@ -0,0 +1,267 @@
<template>
  <view class="withdrawal-page" :class="{ leaving: isLeaving }">
    <guide-nav-bar title="导游提现" @back="goBack" />
    <view class="balance-card">
      <text class="balance-label">当前可提</text>
      <text class="balance-amount">¥{{ availableText }}</text>
      <view class="withdraw-button" @tap="startWithdrawal">提现</view>
    </view>
    <view class="heading">
      <text>提现记录</text>
      <text>共 {{ records.length }} 条</text>
    </view>
    <scroll-view class="record-list" scroll-y>
      <view v-if="loading" class="state-text">正在加载</view>
      <view v-else-if="!records.length" class="state-text">暂无提现记录</view>
      <view v-for="item in records" :key="item.id" class="record">
        <view class="record-top">
          <text>提现</text>
          <text class="record-money">¥{{ formatMoney(item.amount) }}</text>
        </view>
        <view class="record-bottom">
          <text>{{ displayTime(item) }} {{ item.cardMasked || '' }}</text>
          <text :class="'status ' + statusClass(item.status)">{{ statusText(item.status) }}</text>
        </view>
        <text v-if="item.reason" class="reason">{{ item.reason }}</text>
      </view>
    </scroll-view>
  </view>
</template>
<script>
import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
import { getOnboardingStatus } from '../../services/settlement'
import { getWithdrawals, getWithdrawalSummary } from '../../services/withdrawal'
import { backOrHome, routeTo } from '../../utils/router'
export default {
  components: {
    GuideNavBar
  },
  data() {
    return {
      loading: false,
      availableAmount: 0,
      records: [],
      isLeaving: false
    }
  },
  computed: {
    availableText() {
      return this.formatMoney(this.availableAmount)
    }
  },
  onShow() {
    this.loadData()
  },
  methods: {
    goBack() {
      if (this.isLeaving) {
        return
      }
      this.isLeaving = true
      setTimeout(() => {
        backOrHome('/pages/earnings/index')
      }, 280)
    },
    startWithdrawal() {
      getOnboardingStatus().then((item) => {
        const status = (item && item.status) || 'unsubmitted'
        if (status === 'approved') {
          routeTo('/pages/withdrawal-detail/index')
          return
        }
        if (status === 'pending' || status === 'rejected') {
          routeTo('/pages/onboarding-pending/index')
          return
        }
        routeTo('/pages/onboarding/index')
      }).catch(() => {
        routeTo('/pages/onboarding/index')
      })
    },
    loadData() {
      this.loading = true
      Promise.all([
        getWithdrawalSummary(),
        getWithdrawals()
      ]).then(([summary, page]) => {
        this.availableAmount = Number(summary && summary.availableAmount || 0)
        this.records = (page && page.records) || []
      }).catch((error) => {
        uni.showToast({ title: error.message || '提现记录加载失败', icon: 'none' })
      }).finally(() => {
        this.loading = false
      })
    },
    formatMoney(amount = 0) {
      return Number(amount || 0).toFixed(2)
    },
    formatTime(value) {
      return value ? String(value).replace('T', ' ').slice(0, 19) : ''
    },
    displayTime(item) {
      if (item.status === 'APPROVED' && item.processedAt) {
        return '到账 ' + this.formatTime(item.processedAt)
      }
      return this.formatTime(item.appliedAt)
    },
    statusText(status) {
      if (status === 'APPROVED') return '已到账'
      if (status === 'REJECTED') return '已驳回'
      return '待审核'
    },
    statusClass(status) {
      if (status === 'APPROVED') return 'paid'
      if (status === 'REJECTED') return 'rejected'
      return 'pending'
    }
  }
}
</script>
<style scoped>
.withdrawal-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f6f8fb;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pageSlideIn 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.withdrawal-page.leaving {
  animation: pageSlideOut 280ms cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes pageSlideIn {
  from { opacity: 0.35; transform: translateX(72rpx); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes pageSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0.2; transform: translateX(260rpx); }
}
.balance-card {
  margin: 28rpx 28rpx 0;
  padding: 36rpx 32rpx 32rpx;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
  text-align: center;
}
.balance-label {
  color: #667085;
  font-size: 26rpx;
}
.balance-amount {
  display: block;
  margin: 16rpx 0 28rpx;
  color: #1f3b5b;
  font-size: 56rpx;
  font-weight: 800;
  line-height: 1;
}
.withdraw-button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 88rpx;
  border-radius: 12rpx;
  background: #4d8fe8;
  color: #ffffff;
  font-size: 30rpx;
  font-weight: 700;
}
.heading {
  display: flex;
  justify-content: space-between;
  padding: 36rpx 36rpx 18rpx;
  color: #1f3b5b;
  font-size: 30rpx;
  font-weight: 700;
}
.heading text:last-child {
  color: #a0a9b7;
  font-size: 24rpx;
  font-weight: 400;
}
.record-list {
  flex: 1;
  min-height: 0;
  padding: 0 28rpx 24rpx;
  box-sizing: border-box;
}
.record {
  padding: 26rpx 28rpx;
  margin-bottom: 16rpx;
  border: 1rpx solid #edf0f4;
  border-radius: 12rpx;
  background: #ffffff;
}
.record-top,
.record-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.record-top {
  color: #1f2937;
  font-size: 28rpx;
  font-weight: 700;
}
.record-money {
  font-weight: 800;
}
.record-bottom {
  margin-top: 16rpx;
  color: #667085;
  font-size: 23rpx;
}
.status {
  font-weight: 700;
}
.pending {
  color: #e58b28;
}
.paid {
  color: #249765;
}
.rejected,
.reason {
  color: #e95b5b;
}
.reason {
  display: block;
  margin-top: 8rpx;
  font-size: 22rpx;
}
.state-text {
  padding-top: 120rpx;
  color: #8993a1;
  text-align: center;
  font-size: 28rpx;
}
</style>
mingdaojingjiang_guide_app/services/settlement.js
New file
@@ -0,0 +1,141 @@
import {
  GUIDE_BANK_CARDS_KEY,
  GUIDE_ONBOARDING_KEY,
  GUIDE_SELECTED_BANK_KEY,
  GUIDE_SELECTED_BRANCH_KEY,
  bankBranches,
  initialBankCards,
  initialOnboarding
} from '../constants/mock'
function formatNow() {
  const now = new Date()
  const pad = (value) => String(value).padStart(2, '0')
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
function maskCard(cardNumber) {
  const digits = String(cardNumber || '').replace(/\D/g, '')
  return `****${digits.slice(-4)}`
}
function readOnboarding() {
  const stored = uni.getStorageSync(GUIDE_ONBOARDING_KEY)
  if (stored && stored.status) {
    return stored
  }
  uni.setStorageSync(GUIDE_ONBOARDING_KEY, { ...initialOnboarding })
  return { ...initialOnboarding }
}
function writeOnboarding(data) {
  uni.setStorageSync(GUIDE_ONBOARDING_KEY, data)
}
function readBankCards() {
  const stored = uni.getStorageSync(GUIDE_BANK_CARDS_KEY)
  if (Array.isArray(stored) && stored.length) {
    return stored
  }
  uni.setStorageSync(GUIDE_BANK_CARDS_KEY, initialBankCards)
  return initialBankCards.slice()
}
function writeBankCards(cards) {
  uni.setStorageSync(GUIDE_BANK_CARDS_KEY, cards)
}
export function getOnboardingStatus() {
  return Promise.resolve(readOnboarding())
}
export function submitOnboarding(payload) {
  const next = {
    status: 'pending',
    reason: '',
    submittedAt: formatNow(),
    payload: payload || {}
  }
  writeOnboarding(next)
  return Promise.resolve(next)
}
export function refreshOnboardingStatus() {
  const current = readOnboarding()
  if (current.status === 'pending') {
    const next = {
      ...current,
      status: 'approved',
      reason: '',
      reviewedAt: formatNow()
    }
    writeOnboarding(next)
    return Promise.resolve(next)
  }
  return Promise.resolve(current)
}
export function getBankCards() {
  return Promise.resolve(readBankCards())
}
export function addBankCard(payload) {
  const cards = readBankCards()
  const created = {
    id: `card_${Date.now()}`,
    bankName: payload.bankName,
    branchName: payload.branchName,
    cardMasked: maskCard(payload.cardNumber),
    status: 'pending',
    reason: ''
  }
  writeBankCards([created, ...cards])
  return Promise.resolve(created)
}
export function searchBankBranches(keyword = '') {
  const query = String(keyword || '').trim()
  const items = bankBranches.filter((item) => {
    if (!query) {
      return true
    }
    return item.bankName.indexOf(query) >= 0 || item.branchName.indexOf(query) >= 0
  })
  return Promise.resolve(items)
}
export function saveSelectedBankId(id) {
  uni.setStorageSync(GUIDE_SELECTED_BANK_KEY, id)
}
export function consumeSelectedBankId() {
  const value = uni.getStorageSync(GUIDE_SELECTED_BANK_KEY)
  if (value) {
    uni.removeStorageSync(GUIDE_SELECTED_BANK_KEY)
  }
  return value || ''
}
export function saveSelectedBranch(branch) {
  uni.setStorageSync(GUIDE_SELECTED_BRANCH_KEY, JSON.stringify(branch))
}
export function consumeSelectedBranch() {
  const stored = uni.getStorageSync(GUIDE_SELECTED_BRANCH_KEY)
  if (!stored) {
    return null
  }
  uni.removeStorageSync(GUIDE_SELECTED_BRANCH_KEY)
  if (typeof stored === 'object') {
    return stored
  }
  try {
    return JSON.parse(stored)
  } catch (error) {
    return {
      branchName: stored,
      bankName: stored,
      branchCode: ''
    }
  }
}
mingdaojingjiang_guide_app/services/withdrawal.js
New file
@@ -0,0 +1,49 @@
import { GUIDE_WITHDRAWALS_KEY, initialWithdrawals, withdrawalSummary } from '../constants/mock'
function readWithdrawals() {
  const records = uni.getStorageSync(GUIDE_WITHDRAWALS_KEY)
  if (Array.isArray(records) && records.length) {
    return records
  }
  uni.setStorageSync(GUIDE_WITHDRAWALS_KEY, initialWithdrawals)
  return initialWithdrawals.slice()
}
function writeWithdrawals(records) {
  uni.setStorageSync(GUIDE_WITHDRAWALS_KEY, records)
}
function formatNow() {
  const now = new Date()
  const pad = (value) => String(value).padStart(2, '0')
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
export function getWithdrawalSummary() {
  return Promise.resolve({ ...withdrawalSummary })
}
export function getWithdrawals() {
  const records = readWithdrawals()
  return Promise.resolve({
    records,
    total: records.length
  })
}
export function createWithdrawal(amount, bank) {
  const records = readWithdrawals()
  const created = {
    id: `WD${Date.now()}`,
    withdrawalNo: `WD${Date.now()}`,
    amount: Number(Number(amount).toFixed(2)),
    status: 'PENDING',
    reason: '',
    bankName: (bank && bank.bankName) || '',
    cardMasked: (bank && bank.cardMasked) || '',
    appliedAt: formatNow(),
    processedAt: ''
  }
  writeWithdrawals([created, ...records])
  return Promise.resolve(created)
}
mingdaojingjiang_guide_app/static/icons/chevron-left.svg
New file
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#222222" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5L8 12L15 19"/></svg>
mingdaojingjiang_guide_app/static/icons/clock-3.svg
New file
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4d8fe8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6h4"/></svg>
mingdaojingjiang_guide_app/static/icons/search.svg
New file
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#596579" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>