From d4470f1be3134cc219b6a529bc22567540d01255 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期五, 14 八月 2026 14:00:39 +0800
Subject: [PATCH] feat(guide): add withdrawal settlement flow
---
mingdaojingjiang_guide_app/constants/mock.js | 90 +
mingdaojingjiang_guide_app/pages/bank-card-select/index.vue | 186 +++
mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue | 114 ++
mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue | 257 +++++
mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue | 342 ++++++
mingdaojingjiang_guide_app/pages/withdrawal/index.vue | 267 +++++
mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue | 77 +
mingdaojingjiang_guide_app/static/icons/clock-3.svg | 1
mingdaojingjiang_guide_app/services/withdrawal.js | 49
mingdaojingjiang_guide_app/static/icons/search.svg | 1
mingdaojingjiang_guide_app/services/settlement.js | 141 ++
mingdaojingjiang_guide_app/pages.json | 84 +
mingdaojingjiang_guide_app/docs/开发说明.md | 13
mingdaojingjiang_guide_app/pages/earnings/index.vue | 7
mingdaojingjiang_guide_app/pages/onboarding/index.vue | 687 +++++++++++++
mingdaojingjiang_guide_app/constants/area-codes.js | 16
mingdaojingjiang_guide_app/pages/bank-card-add/index.vue | 426 ++++++++
mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue | 223 ++++
mingdaojingjiang_guide_app/static/icons/chevron-left.svg | 1
19 files changed, 2,966 insertions(+), 16 deletions(-)
diff --git a/mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue b/mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue
index 079f102..c80cee1 100644
--- a/mingdaojingjiang_guide_app/components/guide-nav-bar/guide-nav-bar.vue
+++ b/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>
diff --git a/mingdaojingjiang_guide_app/constants/area-codes.js b/mingdaojingjiang_guide_app/constants/area-codes.js
new file mode 100644
index 0000000..5b9f9b0
--- /dev/null
+++ b/mingdaojingjiang_guide_app/constants/area-codes.js
@@ -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']
+]
diff --git a/mingdaojingjiang_guide_app/constants/mock.js b/mingdaojingjiang_guide_app/constants/mock.js
index e70f126..a058a34 100644
--- a/mingdaojingjiang_guide_app/constants/mock.js
+++ b/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'
+ }
+]
diff --git "a/mingdaojingjiang_guide_app/docs/\345\274\200\345\217\221\350\257\264\346\230\216.md" "b/mingdaojingjiang_guide_app/docs/\345\274\200\345\217\221\350\257\264\346\230\216.md"
index 53d8a58..c50c830 100644
--- "a/mingdaojingjiang_guide_app/docs/\345\274\200\345\217\221\350\257\264\346\230\216.md"
+++ "b/mingdaojingjiang_guide_app/docs/\345\274\200\345\217\221\350\257\264\346\230\216.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 | 提现复用页顶栏改为浅色(灰底深色标题),与原商户端一致;业绩明细等导游主流程页仍用蓝底导航。 |
diff --git a/mingdaojingjiang_guide_app/pages.json b/mingdaojingjiang_guide_app/pages.json
index 8635d18..fb76f90 100644
--- a/mingdaojingjiang_guide_app/pages.json
+++ b/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函数里面得到
+ }
+ ]
+ }
}
diff --git a/mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue b/mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue
new file mode 100644
index 0000000..057d5a6
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/bank-branch-select/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/bank-card-add/index.vue b/mingdaojingjiang_guide_app/pages/bank-card-add/index.vue
new file mode 100644
index 0000000..8131572
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/bank-card-add/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/bank-card-select/index.vue b/mingdaojingjiang_guide_app/pages/bank-card-select/index.vue
new file mode 100644
index 0000000..e22611a
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/bank-card-select/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/earnings/index.vue b/mingdaojingjiang_guide_app/pages/earnings/index.vue
index 4154364..4706b5e 100644
--- a/mingdaojingjiang_guide_app/pages/earnings/index.vue
+++ b/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
diff --git a/mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue b/mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue
new file mode 100644
index 0000000..6139bb8
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/onboarding-pending/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/onboarding/index.vue b/mingdaojingjiang_guide_app/pages/onboarding/index.vue
new file mode 100644
index 0000000..0b71d6b
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/onboarding/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue b/mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue
new file mode 100644
index 0000000..8667abe
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/withdrawal-detail/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue b/mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue
new file mode 100644
index 0000000..4d27eae
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/withdrawal-success/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/pages/withdrawal/index.vue b/mingdaojingjiang_guide_app/pages/withdrawal/index.vue
new file mode 100644
index 0000000..39c405f
--- /dev/null
+++ b/mingdaojingjiang_guide_app/pages/withdrawal/index.vue
@@ -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>
diff --git a/mingdaojingjiang_guide_app/services/settlement.js b/mingdaojingjiang_guide_app/services/settlement.js
new file mode 100644
index 0000000..c07d6e0
--- /dev/null
+++ b/mingdaojingjiang_guide_app/services/settlement.js
@@ -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: ''
+ }
+ }
+}
diff --git a/mingdaojingjiang_guide_app/services/withdrawal.js b/mingdaojingjiang_guide_app/services/withdrawal.js
new file mode 100644
index 0000000..3907234
--- /dev/null
+++ b/mingdaojingjiang_guide_app/services/withdrawal.js
@@ -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)
+}
diff --git a/mingdaojingjiang_guide_app/static/icons/chevron-left.svg b/mingdaojingjiang_guide_app/static/icons/chevron-left.svg
new file mode 100644
index 0000000..c7d9116
--- /dev/null
+++ b/mingdaojingjiang_guide_app/static/icons/chevron-left.svg
@@ -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>
diff --git a/mingdaojingjiang_guide_app/static/icons/clock-3.svg b/mingdaojingjiang_guide_app/static/icons/clock-3.svg
new file mode 100644
index 0000000..b17569a
--- /dev/null
+++ b/mingdaojingjiang_guide_app/static/icons/clock-3.svg
@@ -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>
diff --git a/mingdaojingjiang_guide_app/static/icons/search.svg b/mingdaojingjiang_guide_app/static/icons/search.svg
new file mode 100644
index 0000000..c5d940b
--- /dev/null
+++ b/mingdaojingjiang_guide_app/static/icons/search.svg
@@ -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>
--
Gitblit v1.7.1