feat(guide): add withdrawal settlement flow
| | |
| | | <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> |
| | |
| | | backgroundColor: { |
| | | type: String, |
| | | default: '#4d8fe8' |
| | | }, |
| | | actionText: { |
| | | type: String, |
| | | default: '' |
| | | }, |
| | | theme: { |
| | | type: String, |
| | | default: 'brand' |
| | | } |
| | | }, |
| | | data() { |
| | |
| | | } |
| | | }, |
| | | 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) |
| | | }, |
| | |
| | | return { |
| | | paddingTop: `${this.statusBarHeight}px`, |
| | | height: `${this.navHeightPx}px`, |
| | | background: this.backgroundColor |
| | | background: this.barBackground |
| | | } |
| | | } |
| | | }, |
| | | methods: { |
| | | handleBack() { |
| | | this.$emit('back') |
| | | }, |
| | | handleAction() { |
| | | this.$emit('action') |
| | | } |
| | | } |
| | | } |
| | |
| | | width: 88rpx; |
| | | height: 64rpx; |
| | | padding: 0; |
| | | border: 0; |
| | | background: transparent; |
| | | transform: translateY(-50%); |
| | | } |
| | | |
| | | .guide-nav-bar__back::after { |
| | | border: 0; |
| | | } |
| | | |
| | | .guide-nav-bar__icon { |
| | |
| | | } |
| | | |
| | | .guide-nav-bar__title { |
| | | max-width: 460rpx; |
| | | max-width: 360rpx; |
| | | color: #ffffff; |
| | | font-size: 32rpx; |
| | | font-weight: 800; |
| | |
| | | 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> |
| New file |
| | |
| | | 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'] |
| | | ] |
| | |
| | | { 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' |
| | | } |
| | | ] |
| | |
| | | - `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`:更换手机号第一步,验证旧手机号。 |
| | |
| | | | 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 | 提现复用页顶栏改为浅色(灰底深色标题),与原商户端一致;业绩明细等导游主流程页仍用蓝底导航。 | |
| | |
| | | } |
| | | }, |
| | | { |
| | | "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", |
| | |
| | | "navigationBarBackgroundColor": "#F6F8FB", |
| | | "backgroundColor": "#F6F8FB" |
| | | }, |
| | | "uniIdRouter": {} |
| | | "uniIdRouter": {}, |
| | | "condition" : { //模式配置,仅开发期间生效 |
| | | "current": 0, //当前激活的模式(list 的索引项) |
| | | "list": [ |
| | | { |
| | | "name": "", //模式名称 |
| | | "path": "", //启动页面,必选 |
| | | "query": "" //启动参数,在页面的onLoad函数里面得到 |
| | | } |
| | | ] |
| | | } |
| | | } |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| | |
| | | <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"> |
| | |
| | | <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: { |
| | |
| | | backOrHome() |
| | | }, 280) |
| | | }, |
| | | goWithdraw() { |
| | | routeTo('/pages/withdrawal/index') |
| | | }, |
| | | loadData() { |
| | | if (this.loading) { |
| | | return |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | 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: '' |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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) |
| | | } |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |