瀏覽代碼

工作台。

付晓文。 5 天之前
父節點
當前提交
0a6389b1c0

+ 145 - 4
src/pages.json

@@ -9,7 +9,7 @@
 		{
 			"path": "pages/index/index",
 			"style": {
-				"navigationBarTitleText": "广誉源",
+				"navigationBarTitleText": "工作台",
 				"app-plus": {
 					"softinputNavBar": "none",
 					"titleNView": false
@@ -726,12 +726,153 @@
 					}
 				}
 			]
+		},
+		{
+			"root": "workbench",
+			"pages": [
+				{
+					"path": "password/index",
+					"style": {
+						"navigationBarTitleText": "修改密码",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "rating/index",
+					"style": {
+						"navigationBarTitleText": "我的评分",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "bank/index",
+					"style": {
+						"navigationBarTitleText": "银行卡",
+						"navigationBarBackgroundColor": "#2cb8d4",
+						"navigationBarTextStyle": "white"
+					}
+				},
+				{
+					"path": "bank/add",
+					"style": {
+						"navigationBarTitleText": "添加银行卡",
+						"navigationBarBackgroundColor": "#2cb8d4",
+						"navigationBarTextStyle": "white"
+					}
+				},
+				{
+					"path": "bank/detail",
+					"style": {
+						"navigationBarTitleText": "银行卡详情",
+						"navigationBarBackgroundColor": "#2cb8d4",
+						"navigationBarTextStyle": "white"
+					}
+				},
+				{
+					"path": "withdraw/apply",
+					"style": {
+						"navigationBarTitleText": "申请提现",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "withdraw/record",
+					"style": {
+						"navigationBarTitleText": "提现记录",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "withdraw/detail",
+					"style": {
+						"navigationBarTitleText": "提现详情",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "income/index",
+					"style": {
+						"navigationBarTitleText": "我的收入",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "skill/index",
+					"style": {
+						"navigationBarTitleText": "技能管理",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "skill/add",
+					"style": {
+						"navigationBarTitleText": "开通新服务",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "skill/edit",
+					"style": {
+						"navigationBarTitleText": "编辑项目",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "fare/index",
+					"style": {
+						"navigationBarTitleText": "免车费",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "contract/index",
+					"style": {
+						"navigationBarTitleText": "我的合同",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "contract/detail",
+					"style": {
+						"navigationBarTitleText": "合同查看",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "city/index",
+					"style": {
+						"navigationBarTitleText": "城市管理",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "city/apply",
+					"style": {
+						"navigationBarTitleText": "城市管理",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black"
+					}
+				}
+			]
 		}
 	],
 	"preloadRule": {
 		"pages/index/index": {
 			"network": "all",
-			"packages": ["points"]
+			"packages": ["points", "workbench"]
 		}
 	},
 	"globalStyle": {
@@ -744,8 +885,8 @@
 		"color": "#ADB4B8",
 		"selectedColor": "#333333",
 		"backgroundColor": "#FFFFFF",
-		"list": [{
-				"text": "首页",
+		"list": [			{
+				"text": "工作台",
 				"pagePath": "pages/index/index",
 				"iconPath": "static/identify/index.png",
 				"selectedIconPath": "static/identify/index-cur.png"

+ 1123 - 999
src/pages/index/index.vue

@@ -1,1029 +1,1153 @@
 <template>
-  <view class="page-container">
-    <view class="swiper">
-      <u-swiper :list="list"></u-swiper>
-    </view>
-    <view class="tag">
-      <view class="item" v-for="(item,index) in tagList" :key="index">
-        <image src="/static/index/Frame.png" alt="" />
-        {{ item.value }}
-      </view>
-    </view>
-    <view class="recommend">
-      <view class="tuijian_img">
-        <image src="/static/index/tuijian.png" alt="" />
-      </view>
-
-      <view
-        class="recommend_js"
-        v-for="(item, index) in rankList"
-        :key="index"
-        @click="goPDetails(item)"
-      >
-        <view class="js_img">
-          <!-- <image src="/static/index/ranking.png" mode=""></image> -->
-          <view class="border">
-            <image
-              :src="$globalData.publicUrl + item.cPortrait"
-              mode=""
-            ></image>
-          </view>
-        </view>
-        {{ item.cNickName }}
-      </view>
-    </view>
-    <view class="content">
-      <view class="selcet_box">
-        <view
-          class="options"
-          :class="{ hover: current == index }"
-          v-for="(item, index) in selectList"
-          :key="index"
-          @click="onSelect(item, index)"
-        >
-          {{ item.cdescribe }}
-        </view>
-      </view>
-      <view class="menu">
-        <view
-          class="project"
-          v-for="(item, index) in pojectList"
-          :key="index"
-          @click="goGDetails(item.cId)"
-        >
-          <view class="img">
-            <image :src="$globalData.publicUrl + item.cCover"></image>
-            <!-- <view class="time">
-							{{item.nMinute}}分钟
-						</view> -->
-          </view>
-          <view class="describe">
-            <view class="name">
-              {{ item.cTitle }}
-            </view>
-            <view class="num">
-              <view class="time">
-                <image src="/static/index/time.png" alt="" />
-                {{ item.nMinute }}分钟
-              </view>
-              <view class="time">
-                <image src="/static/index/people.png" alt="" />
-                {{ item.nSaleNumber }}人选择
-              </view>
-            </view>
-            <view class="num">
-              {{ item.cLdList }}
-            </view>
-            <!-- <view class="num">
-							超{{item.nSaleNumber}}人选择
-						</view> -->
-            <view class="price_box">
-              <view class="price">
-                ¥<text>{{ item.dPrice }}</text>
-              </view>
-              <view class="make" @click.stop="onSubmit(item.cId)"> 预约 </view>
-            </view>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="modal-container" v-if="showCoupon">
-      <view class="coupon-content show">
-        <view class="coupon-box">
-          <image class="imgs" src="/static/index/popup_bgi.png" mode=""></image>
-          <view class="border">
-            <image class="imgs" src="/static/index/border.png" mode=""></image>
-          </view>
-          <view class="close" @click="onClose('showCoupon')">
-            <image src="/static/other/cut.png" mode=""></image>
-          </view>
-          <view class="btn" @click="receiveAllCoupon">
-            <image
-              class="imgs"
-              src="/static/index/collection.png"
-              mode=""
-            ></image>
-          </view>
-          <view class="coupon">
-            <view class="item" v-for="(item, index) in couponList" :key="index">
-              <view class="left">
-                <text v-if="item.discountType == 3"
-                  >¥{{ item.discountValue }}<br
-                /></text>
-                <text v-else>{{ item.rebValue }}折券<br /></text>
-                <text>有效期{{ item.termDays }}天</text>
-              </view>
-              <view class="right" v-if="item.discountType == 3">
-                满{{ item.thresholdAmount }}减{{ item.discountValue }}
-              </view>
-              <view class="right" v-else>
-                满{{ item.thresholdAmount }}可用
-              </view>
-              <view class="one_btn" @click.stop="receiveCoupon(item)">
-                <image src="/static/index/one.png" mode=""></image>
-              </view>
-            </view>
-          </view>
-        </view>
-
-        <!-- <view class="coupon-header">恭喜您获得优惠券</view>
-				<view class="coupon-amount">{{ couponAmount }}</view>
-				<view class="coupon-desc">{{ couponDesc }}</view>
-				<button class="coupon-btn" @click="receiveCoupon">立即领取</button> -->
-      </view>
-      <view class="mask show"></view>
-    </view>
-	<view>
-		<!-- 优惠券弹窗 -->
-		<uni-popup class="popup-box" ref="popup" background-color="#fff" :mask-click="false">
-			<view class="popup-content">
-				<view class="coupon-centent">
-					<view class="coupon-title">
-						<image src="../../static/other/leftBg.png"></image>
-						<view class="words">
-							恭喜你获得<view class="special">1张</view>优惠券
-						</view>
-						<image src="../../static/other/rightBg.png"></image>
+	<view class="workbench">
+		<!-- 头部:头像、昵称、经营状态 -->
+		<view class="section header-section">
+			<view class="header-main" @click="goProfile">
+				<view class="avatar-wrap">
+					<image
+						class="avatar"
+						:src="merchantInfo.cPortrait ? $globalData.publicUrl + merchantInfo.cPortrait : '/static/common/avatar.png'"
+						mode="aspectFill"
+					/>
+				</view>
+				<view class="header-info">
+					<view class="nickname">
+						{{ merchantInfo.cNickName || merchantInfo.cName || '技师昵称' }}
+						<text class="arrow">&gt;</text>
 					</view>
-					<view class="coupon-message">
-						<view class="left">
-							<view class="left-item">{{ myCouponObj.name }}</view>
-							<view class="left-item" v-if="myCouponObj.validityType == 2">领券{{item.validDays }}天后失效</view>
-							<view class="left-item" v-else-if="myCouponObj.validityType == 3">长期有效</view>
-							<view class="left-item" v-else>有效期至{{ myCouponObj.validEndTime }}</view>
-							<view class="left-item">{{ myCouponObj.type==='1'?'兑换券':myCouponObj.type==='2'?'折扣券':myCouponObj.type==='3'?'满减券':'' }}*1</view>
-						</view>
-						<view class="right" v-if="myCouponObj.type==='1'">
-							<view>兑换</view>
-						</view>
-						<view class="right" v-if="myCouponObj.type==='2'">
-							<view class="special">{{ myCouponObj.ruleDiscountRate?myCouponObj.ruleDiscountRate*100:'' }}</view>折
+				</view>
+			</view>
+			<view class="status-bar">
+				<view class="status-tag rest" v-if="!isWorking">休息时间</view>
+				<view class="status-select" @click="openStatusPopup">
+					<text>{{ isWorking ? '在线接单' : '下线休息' }}</text>
+					<text class="arrow-down">▼</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 统计 -->
+		<view class="section stats-section">
+			<view class="stat-item" @click="goTodayOrders">
+				<view class="stat-value">{{ todayOrderCount }}<text class="unit">笔</text></view>
+				<view class="stat-label">今日订单</view>
+			</view>
+			<view class="stat-item" @click="goBankList">
+				<view class="stat-value">{{ bankCardCount }}<text class="unit">张</text></view>
+				<view class="stat-label">银行卡</view>
+			</view>
+			<view class="stat-item" @click="goReviewList">
+				<view class="stat-value">{{ ratingScore }}</view>
+				<view class="stat-label">评分</view>
+			</view>
+			<view class="stat-item">
+				<view class="stat-value">{{ onlineHours }}<text class="unit">小时</text></view>
+				<view class="stat-label">在线时长</view>
+			</view>
+		</view>
+
+		<!-- 钱包 -->
+		<view class="section wallet-section">
+			<view class="balance-row">
+				<view class="balance-main" @click="goWithdraw">
+					<text class="balance-amount">{{ formatMoney(balance) }}元</text>
+					<text class="balance-label">余额</text>
+				</view>
+				<view class="withdraw-btn" @click.stop="goWithdraw">提现</view>
+			</view>
+			<view class="wallet-grid">
+				<view class="wallet-item" @click="goIncome">
+					<view class="wallet-value">{{ formatMoney(myIncome.tAmount) }}<text class="unit">元</text></view>
+					<view class="wallet-label">收入</view>
+				</view>
+				<view class="wallet-item" @click="goIncome">
+					<view class="wallet-value">{{ formatMoney(myIncome.wAmount) }}<text class="unit">元</text></view>
+					<view class="wallet-label">待结算</view>
+				</view>
+				<view class="wallet-item" @click="goIncome">
+					<view class="wallet-value">{{ formatMoney(myIncome.yAmount) }}<text class="unit">元</text></view>
+					<view class="wallet-label">已结算</view>
+				</view>
+				<view class="wallet-item" @click="goWithdrawRecord">
+					<view class="wallet-value">{{ formatMoney(myIncome.gAmount) }}<text class="unit">元</text></view>
+					<view class="wallet-label">已提现</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 新订单 -->
+		<view class="section order-section" v-if="pendingOrder">
+			<view class="section-title">
+				<text class="bell">🔔</text>
+				新订单
+			</view>
+			<view class="order-card">
+				<view class="order-title">
+					{{ orderTitle }}
+					<text class="tag-new" v-if="isNewCustomer">#新客户#</text>
+				</view>
+				<view class="order-time">{{ orderServiceTime }}</view>
+				<view class="order-address">
+					<text class="loc-icon">📍</text>
+					{{ orderAddress }}
+				</view>
+				<view class="order-actions">
+					<view class="reject-btn" @click="onRejectOrder">拒绝</view>
+					<view class="slide-wrap" id="slideWrap">
+						<view class="slide-track">
+							<text class="slide-hint">滑动接单</text>
+							<view
+								class="slide-thumb"
+								:style="{ transform: `translateX(${slideX}px)` }"
+								@touchstart="onSlideStart"
+								@touchmove.stop.prevent="onSlideMove"
+								@touchend="onSlideEnd"
+							>滑动接单</view>
 						</view>
-						<view class="right" v-if="myCouponObj.type==='3'">
-							<view class="special">¥</view>{{ myCouponObj.ruleReductionAmount?myCouponObj.ruleReductionAmount:'' }}
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 我的技能 -->
+		<view class="section skills-section">
+			<view class="section-header">
+				<text class="section-heading">我的技能 ({{ skillList.length }}项)</text>
+				<text class="section-link" @click="goSkillManage">管理 &gt;</text>
+			</view>
+			<scroll-view scroll-x class="skill-scroll" v-if="skillList.length">
+				<view class="skill-list">
+					<view class="skill-card" v-for="(item, index) in skillList" :key="index">
+						<view class="skill-img">
+							<image
+								v-if="item.cCover"
+								:src="$globalData.publicUrl + item.cCover"
+								mode="aspectFill"
+							/>
 						</view>
+						<view class="skill-name one-line-text">{{ item.cTitle }}</view>
+						<view class="skill-price">¥ {{ item.dPrice }}{{ skillPriceUnit(item) }}</view>
 					</view>
-					<view class="coupon-btn-box">
-						<button class="coupon-btns" :loading="receiveLoading" @click="handleReceiveCoupon">立即领取</button>
+				</view>
+			</scroll-view>
+			<view class="empty-tip" v-else>暂未添加技能</view>
+		</view>
+
+		<!-- 我的地址 -->
+		<view class="section address-section">
+			<view class="section-heading">我的地址</view>
+			<view class="address-text">{{ currentAddress || '暂无地址' }}</view>
+			<view class="address-actions">
+				<view class="addr-btn" @click="goSwitchAddress">切换地址</view>
+				<view class="addr-btn primary" @click="goUpdateAddress">更新地址</view>
+			</view>
+		</view>
+
+		<!-- 更多功能 -->
+		<view class="section more-section">
+			<view class="section-heading">更多功能</view>
+			<view class="more-grid">
+				<view
+					class="more-item"
+					v-for="(item, index) in moreFeatures"
+					:key="index"
+					@click="onMoreFeature(item)"
+				>
+					<view class="more-icon">{{ item.icon }}</view>
+					<text class="more-label">{{ item.label }}</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 拒绝接单弹窗 -->
+		<view class="popup-mask" v-if="showRejectPopup" @click="closeRejectPopup">
+			<view class="popup-box" @click.stop>
+				<view class="popup-title">请输入拒绝接单原因</view>
+				<input
+					class="popup-input"
+					v-model="rejectReason"
+					maxlength="10"
+					placeholder="请输入拒绝原因,最多10个字"
+				/>
+				<view class="popup-btns">
+					<view class="popup-btn" @click="closeRejectPopup">取消</view>
+					<view class="popup-btn confirm" @click="submitReject">提交</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 接单状态底部弹窗 -->
+		<view class="popup-mask bottom" v-if="showStatusPopup" @click="showStatusPopup = false">
+			<view class="status-panel" @click.stop>
+				<view class="panel-title">接单状态</view>
+				<view
+					class="status-option"
+					:class="{ active: statusDraft === 'rest' }"
+					@click="statusDraft = 'rest'"
+				>
+					<view class="option-body">
+						<text class="option-name">{{ !isWorking ? '休息中' : '下线休息' }}</text>
+						<text class="option-desc">{{ getStatusDesc('rest') }}</text>
+					</view>
+					<text class="option-check" v-if="statusDraft === 'rest'">✓</text>
+				</view>
+				<view
+					class="status-option"
+					:class="{ active: statusDraft === 'working' }"
+					@click="statusDraft = 'working'"
+				>
+					<view class="option-body">
+						<text class="option-name">在线接单</text>
+						<text class="option-desc">{{ getStatusDesc('working') }}</text>
 					</view>
+					<text class="option-check" v-if="statusDraft === 'working'">✓</text>
 				</view>
-				<view class="coupon-close" @click="handleCloseCoupon">
-					<image src="../../static/other/closeBg.png"></image>
+				<view class="panel-btns">
+					<view class="panel-btn" @click="showStatusPopup = false">取消</view>
+					<view class="panel-btn confirm" @click="confirmStatusChange">确定</view>
 				</view>
 			</view>
-		</uni-popup>
+		</view>
 	</view>
-  </view>
 </template>
 
 <script>
 import {
-  getCateList,
-  getBannerList,
-  getCouponList,
-  getProjectList,
-  getAccessToken,
-  getRecommendList,
-  netCouponReceive,
-  getCityCode,
-  getShareVolutionDetail,
-  receiveCoupon
+	getMerchantData,
+	getInfo,
+	myIncome,
+	getStaffWorkData,
+	getWaitOrder,
+	myBank,
+	getProjectByJsId,
+	netStaffWork,
 } from '@/api/index'
-import { h5Url } from '@/common/config.js';
-export default {
-  data() {
-    return {
-      showCoupon: false,
-      list: [],
-      rankList: [],
-      selectList: [],
-      pojectList: [],
-      couponList: [],
-      current: 0,
-      selectCurrent: '',
-      tagList: [
-        {
-          value: '实名认证',
-        },
-        {
-          value: '爽约包赔',
-        },
-        {
-          value: '超时秒退',
-        },
-        {
-          value: '资质证书',
-        },
-      ],
-	  myCouponObj: {},
-	  query: [],
-	  receiveLoading: false
-    }
-  },
-  onShow() {
-	// this.$refs.popup.open('center')
-    // 获取坐标
-    this.getLocaltion()
-    // 获取城市
-    //this.getCity()
-    // 获取banner
-    this.getBanner()
-    // 获取商品
-    this.getProject()
-    // 获取商品分类
-    this.getCatetory()
-    // 获取优惠劵
-    this.getCoupon()
-    // 获取推荐数据
-    this.getRanking()
-  },
-  onLoad(query) {
-    // 静默登录
-	this.query = query
-	console.log('参数', query)
-    // 1. 获取URL中的code和state参数
-    const urlParams = new URLSearchParams(window.location.search)
-    const code = urlParams.get('code')
-    const state = urlParams.get('state')
-
-    // 2. 如果有code参数,就跳转到目标Hash路径并携带参数
-    if (code && state) {
-      const targetUrl = `${h5Url}/#/?code=${code}&state=${
-        state || 'STATE'
-      }`
-      // 替换当前页面,避免浏览器返回按钮出现重复授权
-      window.location.replace(targetUrl)
-    }
-    if (query.code) this.onLogin(query.code)
-	// 获取优惠券
-	if(query.shareType === 'COUPON' && query.shareContentId) {
-		this.showCoupons()
-	}
+import { orderDeatails, takeOrder, refuseOrder } from '@/api/order.js'
 
-    // uni.setStorageSync('wx_copenid', 'o-HEJ6VP2YNi0HPeLaIHCQsQD69s')
-    // uni.setStorageSync('access-token', 'eyJhbGciOiJIUzUxMiJ9.eyJ0ZjoiOiJlNGNiNjY0ZS05Y2Q5LTRlOWItODU1Yy0zN2RlNjRiOGI3ZDIifQ.xj0ZBULLc7wYOaUgDQd_zbeynbJ8M3M-UQM6JPUKcV9rIWVlax8dJnU247om39nialcKnklKFZ8m8oerc-NY0A')
-  },
-  methods: {
-	// 优惠券弹框
-	showCoupons() {
-		getShareVolutionDetail({
-			couponId: this.query.shareContentId
-		}).then(res => {
-			if(res.data.code == 200) {
-				this.myCouponObj = res.data.data
-			}
-		})
-		this.$nextTick(() => {
-			this.$refs.popup.open()
-		})
+export default {
+	data() {
+		return {
+			merchantInfo: {},
+			cJsId: '',
+			isWorking: false,
+			todayOrderCount: 0,
+			bankCardCount: 0,
+			ratingScore: '0.0',
+			onlineHours: '0',
+			workStartTime: null,
+			restStartTime: null,
+			balance: 0,
+			myIncome: {
+				tAmount: 0,
+				wAmount: 0,
+				yAmount: 0,
+				gAmount: 0,
+				kAmount: 0,
+				deductAmount: 0,
+			},
+			pendingOrder: true,
+			skillList: [],
+			currentAddress: '',
+			showRejectPopup: false,
+			rejectReason: '',
+			showStatusPopup: false,
+			statusDraft: 'working',
+			slideX: 0,
+			slideStartX: 0,
+			slideMax: 0,
+			isSliding: false,
+			moreFeatures: [
+				{ label: '免车费', icon: '免', path: '/workbench/fare/index' },
+				{ label: '开通新技能', icon: '技', path: '/workbench/skill/add' },
+				{ label: '我的合同', icon: '合', path: '/workbench/contract/index' },
+				{ label: '我的资料', icon: '资', path: '/pages/my/indent?title=编辑资料' },
+				{ label: '城市管理', icon: '城', path: '/workbench/city/index' },
+			],
+		}
 	},
-	// 关闭优惠券弹窗
-	handleCloseCoupon() {
-		this.$nextTick(() => {
-			this.$refs.popup.close()
-		})
+	computed: {
+		orderTitle() {
+			if (!this.pendingOrder) return ''
+			const goods = this.pendingOrder.cGoods || []
+			return goods[0]?.cTitle || '服务项目'
+		},
+		orderServiceTime() {
+			if (!this.pendingOrder) return ''
+			return this.formatOrderTime(
+				this.pendingOrder.reachTime || this.pendingOrder.dtCreateTime || ''
+			)
+		},
+		deductionAmount() {
+			return (
+				this.myIncome.kAmount ??
+				this.myIncome.deductAmount ??
+				this.myIncome.dAmount ??
+				0
+			)
+		},
+		orderAddress() {
+			if (!this.pendingOrder) return ''
+			return this.pendingOrder.address || this.pendingOrder.atlasAdd || ''
+		},
+		isNewCustomer() {
+			return this.pendingOrder?.nB2 == 1
+		},
 	},
-	// 领取优惠券
-	handleReceiveCoupon() {
-		this.receiveLoading = true
-		const token = uni.getStorageSync('access-token') || '';
-		const isLogin = !!token;
-		if(!isLogin) {
-			receiveCoupon({
-				...this.myCouponObj
+	onShow() {
+		this.loadPageData()
+	},
+	methods: {
+		loadPageData() {
+			this.fetchMerchantInfo()
+			this.fetchWalletInfo()
+			this.fetchIncome()
+			this.fetchWorkData()
+			this.fetchBankCards()
+		},
+		fetchMerchantInfo() {
+			const params = { cOpenId: uni.getStorageSync('wx_copenid') }
+			getMerchantData(params).then(res => {
+				if (res.data.code != 200 || !res.data.data) return
+				const data = res.data.data
+				this.merchantInfo = data
+				this.cJsId = data.id
+				this.isWorking = data.nStatus2 == '0'
+				this.ratingScore = data.nStar != null ? Number(data.nStar).toFixed(1) : '0.0'
+				this.workStartTime = data.dtWorkStart || data.dtOnlineStart || null
+				this.restStartTime = data.dtRestStart || data.restStartTime || null
+				this.currentAddress = data.address || data.name || ''
+				this.fetchTodayOrders()
+				this.fetchPendingOrder()
+				this.fetchSkills()
+			})
+		},
+		fetchWalletInfo() {
+			getInfo().then(res => {
+				if (res.data.code == 200 && res.data.data) {
+					this.balance = res.data.data.getAmount ?? res.data.data.dBalance ?? 0
+				}
+			})
+		},
+		fetchIncome() {
+			myIncome().then(res => {
+				if (res.data.code == 200 && res.data.data) {
+					this.myIncome = res.data.data
+				}
+			})
+		},
+		fetchWorkData() {
+			getStaffWorkData({ openId: uni.getStorageSync('wx_copenid') }).then(res => {
+				if (res.data.code == 200 && res.data.data) {
+					const minutes = res.data.data.onLine || 0
+					this.onlineHours = (minutes / 60).toFixed(1).replace(/\.0$/, '')
+				}
+			})
+		},
+		fetchBankCards() {
+			myBank().then(res => {
+				if (res.data.code == 200 && Array.isArray(res.data.data)) {
+					this.bankCardCount = res.data.data.length
+				}
+			})
+		},
+		fetchTodayOrders() {
+			if (!this.cJsId) return
+			getWaitOrder({ cJsId: this.cJsId }).then(res => {
+				if (res.data.code == 200) {
+					this.todayOrderCount = res.data.data || 0
+				}
+			})
+		},
+		fetchSkills() {
+			const openId = uni.getStorageSync('wx_copenid')
+			if (!openId) return
+			getProjectByJsId({ openId }).then(res => {
+				if (res.data.code == 200 && Array.isArray(res.data.data)) {
+					this.skillList = res.data.data
+				}
+			})
+		},
+		fetchPendingOrder() {
+			if (!this.cJsId) return
+			orderDeatails({
+				cJsId: this.cJsId,
+				nStatus: 0,
+				current: 1,
+				size: 1,
 			}).then(res => {
-				this.receiveLoading = false
-				if(res.data.code == 200) {
-					this.$nextTick(() => {
-						this.$refs.popup.close()
-					})
+				if (res.data.code == 200 && res.data.data?.records?.length) {
+					this.pendingOrder = res.data.data.records[0]
+				} else {
+					this.pendingOrder = null
 				}
 			})
-		} else{
-			uni.navigateTo({
-				url: `/pages/login/wxLogin?shareType=${this.query.shareType}&shareContentId=${this.query.shareContentId}`
+		},
+		formatMoney(amount) {
+			const num = parseFloat(amount)
+			if (isNaN(num)) return '0.00'
+			return num.toFixed(2)
+		},
+		skillPriceUnit(item) {
+			if (item.cUnit) return `/${item.cUnit}`
+			if (item.nMinute) return '/小时'
+			return ''
+		},
+		formatOrderTime(timeStr) {
+			if (!timeStr) return ''
+			const str = String(timeStr)
+			const d = new Date(str.replace(/-/g, '/'))
+			if (Number.isNaN(d.getTime())) return str
+			const m = d.getMonth() + 1
+			const day = d.getDate()
+			const hh = String(d.getHours()).padStart(2, '0')
+			const mm = String(d.getMinutes()).padStart(2, '0')
+			if (str.includes('-') && str.length > 16) {
+				const end = str.slice(11, 16)
+				if (end && end !== `${hh}:${mm}`) {
+					return `${m}月${day}日 ${hh}:${mm}-${end}`
+				}
+			}
+			return `${m}月${day}日 ${hh}:${mm}`
+		},
+		getStatusDurationHours(type) {
+			const since =
+				type === 'working'
+					? this.workStartTime
+					: this.restStartTime
+			if (!since) return 0
+			const start = new Date(String(since).replace(/-/g, '/')).getTime()
+			if (Number.isNaN(start)) return 0
+			return Math.max(0, Math.floor((Date.now() - start) / 3600000))
+		},
+		getStatusDesc(type) {
+			const isCurrent =
+				type === 'working' ? this.isWorking : !this.isWorking
+			if (isCurrent) {
+				const hours = this.getStatusDurationHours(type)
+				if (type === 'working') {
+					return `已连续辛勤工作${hours}个小时`
+				}
+				return `已休息${hours}个小时`
+			}
+			if (type === 'working') {
+				return '早起的人已经接单,勤奋的人账户常满'
+			}
+			return '今日辛苦啦,早点下线休息,注意劳逸结合!'
+		},
+		openStatusPopup() {
+			this.statusDraft = this.isWorking ? 'working' : 'rest'
+			this.showStatusPopup = true
+		},
+		confirmStatusChange() {
+			const wantWorking = this.statusDraft === 'working'
+			this.showStatusPopup = false
+			if (wantWorking === this.isWorking) return
+			this.updateWorkStatus(wantWorking)
+		},
+		updateWorkStatus(working) {
+			if (!this.cJsId) {
+				uni.showToast({ title: '请先登录', icon: 'none' })
+				return
+			}
+			netStaffWork({
+				id: this.cJsId,
+				nStatus2: working ? '0' : '-1',
+			}).then(res => {
+				if (res.data.code == 200) {
+					this.isWorking = working
+					const now = new Date().toISOString()
+					if (working) {
+						this.workStartTime = now
+					} else {
+						this.restStartTime = now
+					}
+					uni.showToast({ title: working ? '已上线' : '已下线', icon: 'none' })
+				} else {
+					uni.showToast({ title: res.data.msg || '设置失败', icon: 'none' })
+				}
 			})
-		}
+		},
+		onRejectOrder() {
+			this.showRejectPopup = true
+			this.rejectReason = ''
+		},
+		closeRejectPopup() {
+			this.showRejectPopup = false
+			this.rejectReason = ''
+		},
+		submitReject() {
+			const reason = this.rejectReason.trim()
+			if (!reason) {
+				uni.showToast({ title: '拒绝原因不能为空', icon: 'none' })
+				return
+			}
+			refuseOrder({
+				cId: this.pendingOrder.cId,
+				reasonRefusal: reason,
+			}).then(res => {
+				if (res.data.code == 200) {
+					uni.showToast({ title: '已拒绝', icon: 'none' })
+					this.closeRejectPopup()
+					this.pendingOrder = null
+					this.fetchTodayOrders()
+				} else {
+					uni.showToast({ title: res.data.msg || '操作失败', icon: 'none' })
+				}
+			})
+		},
+		onSlideStart(e) {
+			this.isSliding = true
+			this.slideStartX = e.touches[0].clientX - this.slideX
+			const query = uni.createSelectorQuery().in(this)
+			query.select('.slide-track').boundingClientRect(rect => {
+				if (rect) {
+					this.slideMax = rect.width * 0.65
+				}
+			}).exec()
+		},
+		onSlideMove(e) {
+			if (!this.isSliding) return
+			let x = e.touches[0].clientX - this.slideStartX
+			if (x < 0) x = 0
+			if (x > this.slideMax) x = this.slideMax
+			this.slideX = x
+		},
+		onSlideEnd() {
+			if (!this.isSliding) return
+			this.isSliding = false
+			if (this.slideX >= this.slideMax * 0.85) {
+				this.acceptOrder()
+			}
+			this.slideX = 0
+		},
+		acceptOrder() {
+			if (!this.pendingOrder) return
+			uni.showToast({ title: '接单失败', icon: 'none' })
+			return;
+			takeOrder({ cId: this.pendingOrder.cId }).then(res => {
+				if (res.data.code == 200) {
+					uni.showToast({ title: '已接单', icon: 'none' })
+					this.pendingOrder = null
+					this.fetchTodayOrders()
+				} else {
+					uni.showToast({ title: res.data.msg || '接单失败', icon: 'none' })
+				}
+			})
+		},
+		goProfile() {
+			uni.navigateTo({ url: '/pages/my/indent?title=编辑资料' })
+		},
+		goTodayOrders() {
+			uni.navigateTo({ url: '/pages/my/js_order' })
+		},
+		goBankList() {
+			uni.navigateTo({ url: '/workbench/bank/index' })
+		},
+		goReviewList() {
+			uni.navigateTo({ url: '/workbench/rating/index' })
+		},
+		goWithdraw() {
+			uni.navigateTo({ url: '/workbench/withdraw/apply' })
+		},
+		goIncome() {
+			uni.navigateTo({ url: '/workbench/income/index' })
+		},
+		goWithdrawRecord() {
+			uni.navigateTo({ url: '/workbench/withdraw/record' })
+		},
+		goSkillManage() {
+			uni.navigateTo({ url: '/workbench/skill/index' })
+		},
+		goSwitchAddress() {
+			if (!this.cJsId) return
+			const str = uni.$u.queryParams({
+				id: this.cJsId,
+				name: this.merchantInfo.cNickName,
+				phone: this.merchantInfo.cPhone,
+			})
+			uni.navigateTo({ url: `/pages/address/virtual${str}` })
+		},
+		goUpdateAddress() {
+			this.goSwitchAddress()
+		},
+		onMoreFeature(item) {
+			if (!item.path) {
+				uni.showToast({ title: '功能开发中', icon: 'none' })
+				return
+			}
+			uni.navigateTo({ url: item.path })
+		},
 	},
-    // 获取城市
-    getCity() {
-      uni.request({
-        url: 'https://restapi.amap.com/v3/ip?parameters',
-        data: {
-          key: 'a4c460548bff03534a39e0985d210674',
-        },
-        success: res => {
-          console.log(res,'定位')
-          uni.setStorageSync('selectCity', res.data.city.slice(0, 2))
-          uni.setStorageSync('selectCitycode', res.data.adcode)
-        },
-      })
-    },
-    // 获取推荐数据
-    getRanking() {
-      let params = {
-        deptName: uni.getStorageSync('selectCity'),
-      }
-      this.rankList = []
-      getRecommendList(params).then(res => {
-        if (res.data.data) {
-          this.rankList = res.data.data.slice(0, 4)
-        }
-      })
-    },
-    // 获取坐标
-    getLocaltion() {
-      var _this = this;
-      uni.getLocation({
-        type: 'wgs84',
-        success: function (res) {
-          uni.setStorageSync('latitude', res.latitude)
-          uni.setStorageSync('longitude', res.longitude)
-          _this.getCityInfo(res.latitude,res.longitude)
-        },
-      })
-    },
-    async getCityInfo(latitude, longitude) {
-      if (!latitude || !longitude) {
-        console.error('经纬度参数不能为空');
-        return;
-      }
-      try {
-        const resquery = await getCityCode({longitude: longitude,latitude:latitude});
-        console.log(resquery);
-        uni.setStorageSync('selectCity', resquery.data.data.cityName);
-        uni.setStorageSync('selectCitycode', resquery.data.data.cityCode);
-      } catch (error) {
-        console.error('获取城市信息失败:', error);
-      }
-    },
-    // 获取商品分类
-    getCatetory() {
-      this.selectList = [{ cdescribe: '热门' }]
-      getCateList().then(res => {
-        if (res.data.code == 200 && Array.isArray(res.data.data)) {
-          this.selectList = [...this.selectList, ...res.data.data]
-        }
-      })
-    },
-    // 获取商品
-    getProject() {
-      this.pojectList = []
-      getProjectList({
-        current: 1,
-        size: 10,
-        cLdList: this.selectCurrent,
-      }).then(res => {
-        if (res.data.code == 200) {
-          this.pojectList = res.data.data
-        }
-      })
-    },
-    // 获取banner
-    getBanner() {
-      this.list = []
-      getBannerList({
-        current: 1,
-        size: 10,
-      }).then(res => {
-        if (res.data.code == 200) {
-          this.list = res.data.data.map(
-            item => this.$globalData.publicUrl + item.cImgUrl
-          )
-        }
-      })
-    },
-    // 获取优惠劵
-    getCoupon() {
-      let params = {
-        openid: uni.getStorageSync('wx_copenid'),
-        deptName: uni.getStorageSync('selectCity'),
-      }
-      getCouponList(params).then(res => {
-        if (res.data.data && res.data.data.length > 0) {
-          this.showCoupon = true
-          this.couponList = res.data.data
-        }
-      })
-    },
-    // 关闭事件
-    onClose(n) {
-      this[n] = false
-    },
-    // 一键领取优惠券
-    receiveAllCoupon() {
-      let params = {
-        openId: uni.getStorageSync('wx_copenid'),
-        couponIds: this.couponList.map(item => item.id),
-      }
-      netCouponReceive(params).then(res => {
-        this.showCoupon = false
-        uni.showToast({
-          title: '已领取',
-          icon: 'none',
-        })
-      })
-    },
-    // 领取优惠券
-    receiveCoupon(item) {
-      let params = {
-        couponIds: [item.id],
-        openId: uni.getStorageSync('wx_copenid'),
-      }
-      netCouponReceive(params).then(res => {
-        this.showCoupon = false
-        uni.showToast({
-          title: '已领取',
-          icon: 'none',
-        })
-      })
-    },
-    // 正则匹配请求地址中的参数函数
-    getUrlCode(name) {
-      return (
-        decodeURIComponent(
-          (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(
-            location.href
-          ) || [, ''])[1].replace(/\+/g, '%20')
-        ) || null
-      )
-    },
-    // 静默登录
-    onLogin(wxCode) {
-      let params = {
-        code: wxCode,
-      }
-      if (wxCode !== '') {
-        getAccessToken(params).then(res => {
-          if (res.data.code == 200) {
-            uni.setStorageSync('wx_copenid', res.data.data.copenid)
-            uni.setStorageSync('access-token', res.data.data.token)
-            uni.setStorageSync('userId', res.data.data.id)
-            if(res.data.data.cphone === null || res.data.data.cphone === ''){
-              uni.navigateTo({
-                url: '/setting/myNew/phone'
-              })
-            }else{
-              uni.setStorageSync('wx_phone', res.data.data.cphone)
-            }
-          }
-        })
-      }
-    },
-    // tab选择
-    onSelect(item, index) {
-      this.current = index
-      if (item.cdescribe == '热门') {
-        this.selectCurrent = ''
-      } else {
-        this.selectCurrent = item.cdescribe
-      }
-      this.getProject()
-    },
-    // 下单
-    onSubmit(cId) {
-      uni.setStorageSync('projectId', cId)
-      uni.switchTab({
-        url: '/pages/identify/identify',
-      })
-    },
-    // 跳转详情页
-    goGDetails(cid) {
-      let str = uni.$u.queryParams({
-        cid: cid,
-        deptName: uni.getStorageSync('selectCity'),
-      })
-      uni.navigateTo({
-        url: `/pages/index/details${str}`,
-      })
-    },
-    goPDetails(item) {
-      if (!this.$utils.checkLogin({ type: 'modal' })) return
-      let str = uni.$u.queryParams({
-        id: item.id,
-      })
-      uni.navigateTo({
-        url: `/pages/identify/details${str}`,
-      })
-    },
-  },
 }
 </script>
 
 <style lang="scss" scoped>
-.page-container {
-  width: 100vw;
-  height: 100%;
-  background: linear-gradient(
-    to bottom right,
-    rgb(246, 249, 242),
-    rgb(210, 241, 243)
-  );
-  overflow-y: auto;
-
-  .swiper {
-    padding: 16rpx 32rpx;
-
-    ::v-deep uni-swiper {
-      height: 320rpx !important;
-    }
-  }
-
-  .tag {
-    width: 100%;
-    padding: 0px 32rpx;
-    margin: 28rpx 0px;
-    box-sizing: border-box;
-    display: flex;
-    justify-content: space-around;
-
-    .item {
-      width: 160rpx;
-      height: 44rpx;
-      background: rgba(0, 184, 151, 0.06);
-      border-radius: 8rpx 8rpx 8rpx 8rpx;
-      font-size: 24rpx;
-      color: #0fb4ac;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      image {
-        width: 24rpx;
-        height: 24rpx;
-        margin-right: 8rpx;
-      }
-    }
-  }
-
-  .recommend {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    width: calc(100% - 64rpx);
-    margin: 12rpx auto 16rpx;
-    height: 218rpx;
-    padding: 40rpx 30rpx;
-    position: relative;
-    background: #ffffff;
-    box-shadow: 0px 4rpx 12rpx 0px rgba(88, 209, 187, 0.15);
-    border-radius: 24rpx;
-    box-sizing: border-box;
-
-    .tuijian_img {
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 75rpx;
-      height: 40rpx;
-      z-index: 9999;
-
-      image {
-        width: 100%;
-        height: 100%;
-      }
-    }
-
-    .recommend_js {
-      text-align: center;
-      flex: 1;
-      display: flex;
-      flex-direction: column;
-      justify-content: center;
-      align-items: center;
-      font-size: 24rpx;
-      color: #333333;
-      .js_img {
-        width: 120rpx;
-        height: 120rpx;
-        position: relative;
-        border-radius: 50%;
-        // overflow: hidden;
-        image {
-          width: 100%;
-          height: 100%;
-          margin-bottom: 12rpx;
-          // z-index: 9;
-          z-index: 999;
-        }
-        .border {
-          position: absolute;
-          top: 0;
-          left: 0;
-          width: 120rpx;
-          height: 120rpx;
-
-          image {
-            width: 100%;
-            height: 100%;
-            z-index: 99;
-            margin: 0;
-          }
-        }
-      }
-    }
-  }
-
-  .content {
-    width: 100%;
-    display: flex;
-    flex-direction: column;
-    background-color: #fff;
-    border-radius: 40rpx 40rpx 0px 0px;
-    margin-top: 28rpx;
-    // padding: 32rpx;
-    box-sizing: border-box;
-
-    .selcet_box {
-      width: 100%;
-      overflow-x: auto;
-      box-sizing: border-box;
-      white-space: nowrap;
-      padding: 20rpx 32rpx;
-
-      .options {
-        display: inline-block;
-        font-size: 30rpx;
-        margin-right: 24rpx;
-      }
-
-      .hover {
-        color: #03c8be;
-        font-weight: 700;
-        border-bottom: 4rpx solid #03c8be;
-      }
-    }
-
-    .menu {
-      flex: 1;
-      width: 100%;
-      padding: 0 24rpx 32rpx;
-
-      .project {
-        background: linear-gradient(180deg, #f7ffff 0%, #e9fffb 100%);
-        box-shadow: 0px 4rpx 12rpx 0px rgba(88, 209, 187, 0.2);
-        border-radius: 24rpx;
-        margin-bottom: 24rpx;
-        padding: 16rpx 32rpx;
-        box-sizing: border-box;
-        overflow: hidden;
-        display: flex;
-        justify-content: space-between;
-
-        .img {
-          width: 168rpx;
-          height: 168rpx;
-          border-radius: 8rpx;
-          overflow: hidden;
-          margin-right: 34rpx;
-
-          image {
-            width: 100%;
-            height: 100%;
-          }
-
-          // .time {
-          // 	position: absolute;
-          // 	top: 0;
-          // 	right: 0;
-          // 	padding: 0px 10px;
-          // 	background-color: #333;
-          // 	opacity: .7;
-          // 	border-radius: 0px 0px 0px 10px;
-          // 	color: #fff;
-          // }
-        }
-
-        .describe {
-          flex: 1;
-          height: 100%;
-          display: flex;
-          flex-direction: column;
-
-          .name {
-            width: 100%;
-            font-size: 32rpx;
-            line-height: 24rpx;
-            font-weight: 500;
-          }
-
-          .num {
-            font-size: 24rpx;
-            color: #808080;
-            display: flex;
-            margin-top: 16rpx;
-
-            .time {
-              font-size: 24rpx;
-              color: #666666;
-              display: flex;
-              align-items: center;
-              margin-right: 26rpx;
-
-              image {
-                width: 24rpx;
-                height: 24rpx;
-                margin-right: 4rpx;
-              }
-            }
-          }
-
-          .price_box {
-            width: 100%;
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-
-            .price {
-              font-size: 24rpx;
-              color: #f53e54;
-              display: flex;
-              align-items: baseline;
-
-              text {
-                font-weight: 500;
-                font-size: 48rpx;
-              }
-            }
-
-            .make {
-              width: 124rpx;
-              height: 56rpx;
-              font-size: 30rpx;
-              color: #fff;
-              background: linear-gradient(135deg, #1ad8cf 0%, #21c8c0 100%);
-              border-radius: 8rpx 8rpx 8rpx 8rpx;
-              padding: 0px 20rpx;
-              display: flex;
-              align-items: center;
-              justify-content: center;
-            }
-          }
-        }
-      }
-    }
-  }
-
-  .modal-container {
-    height: 100vh;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-
-    .coupon-content {
-      position: fixed;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 300rpx;
-      transform: translateY(100%);
-      border-radius: 20rpx 20rpx 0 0;
-      box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.1);
-      opacity: 1;
-      transition: all 0.3s;
-      z-index: 9999;
-
-      .coupon-box {
-        width: 350px;
-        height: 500px;
-        margin: 0 auto;
-        position: relative;
-
-        .imgs {
-          width: 350px;
-          height: 500px;
-        }
-
-        .close {
-          width: 80rpx;
-          height: 80rpx;
-          position: absolute;
-          bottom: 0;
-          left: 50%;
-          transform: translateX(-50%);
-          z-index: 99999;
-
-          image {
-            width: 100%;
-            height: 100%;
-          }
-        }
-
-        .border {
-          position: absolute;
-          top: 0;
-          left: 0;
-        }
-
-        .btn {
-          position: absolute;
-          bottom: 0;
-          left: 0;
-        }
-
-        .coupon {
-          width: 230px;
-          height: 230px;
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          transform: translate(-50%, -50%);
-          overflow-y: auto;
-
-          .item {
-            width: 85%;
-            height: 70px;
-            background-image: url('/static/index/item.png');
-            // background-repeat: no-repeat;
-            background-size: cover;
-            background-position: center;
-            padding: 10rpx 25rpx;
-            display: flex;
-            align-items: center;
-            position: relative;
-            margin-bottom: 10rpx;
-
-            .left {
-              color: #ff0000;
-            }
-
-            .right {
-              height: 100%;
-              padding-top: 30px;
-              margin-left: 20rpx;
-              font-size: 28rpx;
-            }
-
-            .one_btn {
-              position: absolute;
-              top: 10%;
-              right: 2%;
-              width: 30px;
-              height: 60px;
-
-              image {
-                width: 100%;
-                height: 100%;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    .coupon-header {
-      font-size: 36rpx;
-      font-weight: bold;
-      color: #333;
-      margin: 40rpx 0 20rpx 0;
-      text-align: center;
-    }
-
-    .coupon-amount {
-      font-size: 80rpx;
-      font-weight: bold;
-      color: #ff0000;
-      text-align: center;
-    }
-
-    .coupon-desc {
-      font-size: 28rpx;
-      color: #666;
-      margin: 30rpx 0;
-      text-align: center;
-    }
-
-    .coupon-btn {
-      margin: 20rpx auto 0 auto;
-      width: 500rpx;
-      height: 80rpx;
-      border: none;
-      border-radius: 40rpx;
-      background-color: #ff0000;
-      color: #fff;
-      font-size: 36rpx;
-      display: block;
-      text-align: center;
-      line-height: 80rpx;
-    }
-
-    .mask {
-      position: fixed;
-      background-color: rgba(0, 0, 0, 0.5);
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      z-index: 999;
-      opacity: 0;
-      transition: all 0.3s;
-    }
-
-    .show {
-      opacity: 1;
-    }
-  }
-  .popup-box{
-	  border-radius: 32rpx;
-	  ::v-deep .uni-popup__wrapper{
-		  background: none !important;
-	  }
-  }
-  .popup-content{
-	  width: 540rpx;
-	  height: 510rpx;
-	  border-radius: 32rpx;
-	  .coupon-centent{
-		  width: 100%;
-		  height: 400rpx;
-		  border-radius: 32rpx;
-		  background: linear-gradient( 0deg, #CBFFFB 0%, #F3FFFE 100%);
-		  padding: 52rpx 24rpx 40rpx;
-		  position: relative;
-	  }
-	  .coupon-title{
-		  width: 100%;
-		  display: flex;
-		  align-items: center;
-		  justify-content: center;
-		  margin-bottom: 28rpx;
-		  image{
-			  width: 32rpx;
-			  height: 2rpx;
-		  }
-		  .words{
-			  display: flex;
-			  align-items: center;
-			  justify-content: center;
-			  .special{
-				  color: #03C8BE;
-			  }
-		  }
-	  }
-	  .coupon-message{
-		  display: flex;
-		  align-items: center;
-		  justify-content: space-between;
-		  background: #ffffff;
-		  border-radius: 24rpx;
-		  .left{
-			  padding: 20rpx;
-			  .left-item{
-				  color: #333333;
-				  margin-bottom: 8rpx;
-				  &:nth-child(1) {
-					  font-size: 24rpx;
-				  }
-				  &:nth-child(2) {
-					  color: #999999;
-					  font-size: 20rpx;
-				  }
-				  &:nth-child(3) {
-					  font-size: 20rpx;
-				  }
-			  }
-		  }
-		  .right{
-			  padding: 56rpx;
-			  background-color: #E8FBFA;
-			  display: flex;
-			  align-items: center;
-			  justify-content: center;
-			  border-radius: 0 24rpx 24rpx 0;
-			  color: #03C8BE;
-		  }
-	  }
-	  .coupon-btn-box{
-		  width: 540rpx;
-		  position: absolute;
-		  left: 0;
-		  bottom: 0rpx;
-		  height: 160rpx;
-		  background: url('../../static/other/bottomBg.png') no-repeat;
-		  background-size: 100% 100%;
-		  .coupon-btns{
-			margin: 70rpx auto 40rpx;
-			height: 60rpx;
-			width: 400rpx;
-			line-height: 60rpx;
-			text-align: center;
-			  border-radius: 98rpx;
-			  background: linear-gradient( 80deg, #00DAAB 0%, #07CDC4 100%);
-			  color: #ffffff;
-		  }
-	  }
-	  .coupon-close{
-		  margin: 60rpx auto 0;
-		  width: 48rpx;
-		  height: 48rpx;
-		  display: flex;
-		  image{
-			  width: 100%;
-			  height: 100%;
-		  }
-	  }
-  }
+.workbench {
+	min-height: 100vh;
+	padding: 24rpx 24rpx 40rpx;
+	background: #f5f5f5;
+	box-sizing: border-box;
+}
+
+.section {
+	background: #fff;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	margin-bottom: 20rpx;
+	padding: 24rpx;
+	box-sizing: border-box;
+}
+
+/* 头部 */
+.header-section {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.header-main {
+	display: flex;
+	align-items: center;
+	flex: 1;
+}
+
+.avatar-wrap {
+	width: 96rpx;
+	height: 96rpx;
+	border-radius: 50%;
+	border: 1rpx solid #ccc;
+	overflow: hidden;
+	background: #eee;
+	flex-shrink: 0;
+}
 
+.avatar {
+	width: 100%;
+	height: 100%;
+}
+
+.header-info {
+	margin-left: 20rpx;
+}
+
+.nickname {
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.arrow {
+	margin-left: 8rpx;
+	color: #999;
+	font-size: 24rpx;
+}
+
+.status-bar {
+	display: flex;
+	align-items: center;
+	gap: 12rpx;
+	flex-shrink: 0;
+}
+
+.status-tag {
+	padding: 8rpx 16rpx;
+	font-size: 22rpx;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	color: #999;
+
+	&.rest {
+		border-color: #ddd;
+		color: #666;
+		background: #f7f7f7;
+	}
+}
+
+.status-select {
+	display: flex;
+	align-items: center;
+	padding: 8rpx 16rpx;
+	font-size: 22rpx;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	color: #666;
+}
+
+.arrow-down {
+	font-size: 18rpx;
+	margin-left: 6rpx;
+	color: #999;
+}
+
+/* 统计栏 */
+.stats-section {
+	display: flex;
+	justify-content: space-between;
+}
+
+.stat-item {
+	flex: 1;
+	text-align: center;
+}
+
+.stat-value {
+	font-size: 32rpx;
+	color: #333;
+	font-weight: 600;
+	line-height: 1.4;
+}
+
+.stat-label {
+	font-size: 22rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.unit {
+	font-size: 22rpx;
+	font-weight: 400;
+	margin-left: 2rpx;
+}
+
+/* 钱包 */
+.balance-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding-bottom: 24rpx;
+	border-bottom: 1rpx solid #eee;
+	margin-bottom: 24rpx;
+}
+
+.balance-main {
+	display: flex;
+	flex-direction: column;
+}
+
+.balance-amount {
+	font-size: 48rpx;
+	font-weight: 700;
+	color: #333;
+	line-height: 1.3;
+}
+
+.balance-label {
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.withdraw-btn {
+	padding: 14rpx 40rpx;
+	background: #f0f0f0;
+	border-radius: 40rpx;
+	font-size: 26rpx;
+	color: #333;
+	flex-shrink: 0;
+}
+
+.wallet-grid {
+	display: flex;
+	justify-content: space-between;
+}
+
+.wallet-item {
+	flex: 1;
+	text-align: center;
+}
+
+.wallet-value {
+	font-size: 26rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.wallet-label {
+	font-size: 22rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+/* 新订单 */
+.section-title {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 600;
+	margin-bottom: 20rpx;
+	display: flex;
+	align-items: center;
+}
+
+.bell {
+	margin-right: 8rpx;
+}
+
+.order-title {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+	line-height: 1.5;
+}
+
+.tag-new {
+	color: #666;
+	font-size: 24rpx;
+	margin-left: 8rpx;
+}
+
+.order-time {
+	font-size: 24rpx;
+	color: #666;
+	margin-top: 12rpx;
+}
+
+.order-address {
+	font-size: 24rpx;
+	color: #666;
+	margin-top: 12rpx;
+	line-height: 1.5;
+	display: flex;
+	align-items: flex-start;
+}
+
+.loc-icon {
+	margin-right: 6rpx;
+	flex-shrink: 0;
+}
+
+.order-actions {
+	display: flex;
+	align-items: center;
+	gap: 16rpx;
+	margin-top: 24rpx;
+}
+
+.reject-btn {
+	width: 120rpx;
+	height: 72rpx;
+	line-height: 72rpx;
+	text-align: center;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	font-size: 26rpx;
+	color: #666;
+	flex-shrink: 0;
+}
+
+.slide-wrap {
+	flex: 1;
+}
+
+.slide-track {
+	position: relative;
+	height: 72rpx;
+	background: #eee;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	overflow: hidden;
+}
+
+.slide-hint {
+	position: absolute;
+	left: 0;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	line-height: 72rpx;
+	text-align: center;
+	font-size: 26rpx;
+	color: #999;
+}
+
+.slide-thumb {
+	position: absolute;
+	left: 0;
+	top: 0;
+	height: 100%;
+	min-width: 160rpx;
+	padding: 0 24rpx;
+	line-height: 72rpx;
+	text-align: center;
+	background: #ccc;
+	border-radius: 6rpx;
+	font-size: 26rpx;
+	color: #333;
+	z-index: 1;
+}
+
+/* 技能 */
+.section-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 20rpx;
+}
+
+.section-heading {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 600;
+}
+
+.section-link {
+	font-size: 24rpx;
+	color: #666;
+}
+
+.skill-scroll {
+	white-space: nowrap;
+}
+
+.skill-list {
+	display: inline-flex;
+	gap: 16rpx;
+}
+
+.skill-card {
+	display: inline-block;
+	width: 200rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 6rpx;
+	padding: 12rpx;
+	vertical-align: top;
+}
+
+.skill-img {
+	width: 100%;
+	height: 120rpx;
+	background: #eee;
+	border: 1rpx solid #ddd;
+	border-radius: 4rpx;
+	overflow: hidden;
+	margin-bottom: 12rpx;
+
+	image {
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.skill-name {
+	font-size: 24rpx;
+	color: #333;
+	margin-bottom: 8rpx;
+}
+
+.skill-price {
+	font-size: 24rpx;
+	color: #666;
+}
+
+.empty-tip {
+	font-size: 26rpx;
+	color: #999;
+	text-align: center;
+	padding: 32rpx 0;
+}
+
+/* 地址 */
+.address-text {
+	font-size: 26rpx;
+	color: #666;
+	line-height: 1.6;
+	margin: 16rpx 0 24rpx;
+}
+
+.address-actions {
+	display: flex;
+	gap: 16rpx;
+}
+
+.addr-btn {
+	flex: 1;
+	height: 72rpx;
+	line-height: 72rpx;
+	text-align: center;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	font-size: 26rpx;
+	color: #666;
+
+	&.primary {
+		border-color: #333;
+		color: #333;
+	}
+}
+
+/* 更多功能 */
+.more-grid {
+	display: flex;
+	flex-wrap: wrap;
+	margin-top: 20rpx;
+}
+
+.more-item {
+	width: 25%;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 16rpx 0;
+}
+
+.more-icon {
+	width: 72rpx;
+	height: 72rpx;
+	line-height: 72rpx;
+	text-align: center;
+	border: 1rpx solid #ccc;
+	border-radius: 8rpx;
+	background: #eee;
+	font-size: 28rpx;
+	color: #666;
+	margin-bottom: 12rpx;
+}
+
+.more-label {
+	font-size: 22rpx;
+	color: #666;
+	text-align: center;
+}
+
+/* 弹窗 */
+.popup-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.4);
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.popup-box {
+	width: 600rpx;
+	background: #fff;
+	border-radius: 8rpx;
+	padding: 32rpx;
+}
+
+.popup-title {
+	font-size: 28rpx;
+	color: #333;
+	text-align: center;
+	margin-bottom: 24rpx;
+}
+
+.popup-input {
+	width: 100%;
+	height: 72rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 6rpx;
+	padding: 0 20rpx;
+	font-size: 26rpx;
+	box-sizing: border-box;
+}
+
+.popup-btns {
+	display: flex;
+	gap: 24rpx;
+	margin-top: 32rpx;
+}
+
+.popup-btn {
+	flex: 1;
+	height: 72rpx;
+	line-height: 72rpx;
+	text-align: center;
+	border: 1rpx solid #ccc;
+	border-radius: 6rpx;
+	font-size: 28rpx;
+	color: #666;
+
+	&.confirm {
+		border-color: #333;
+		color: #333;
+	}
+}
+
+.popup-mask.bottom {
+	align-items: flex-end;
+}
+
+.status-panel {
+	width: 100%;
+	background: #fff;
+	border-radius: 24rpx 24rpx 0 0;
+	padding: 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx);
+	box-sizing: border-box;
+}
+
+.panel-title {
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #333;
+	text-align: center;
+	margin-bottom: 32rpx;
+}
+
+.status-option {
+	display: flex;
+	align-items: center;
+	padding: 28rpx 0;
+	border-bottom: 1rpx solid #f5f5f5;
+
+	&.active .option-name {
+		color: #0879ff;
+	}
+}
+
+.option-body {
+	flex: 1;
+	min-width: 0;
+}
+
+.option-check {
+	font-size: 36rpx;
+	color: #0879ff;
+	font-weight: 600;
+	flex-shrink: 0;
+	margin-left: 16rpx;
+}
+
+.option-name {
+	display: block;
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.option-desc {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+	line-height: 1.5;
+}
+
+.panel-btns {
+	display: flex;
+	gap: 24rpx;
+	margin-top: 32rpx;
+}
+
+.panel-btn {
+	flex: 1;
+	height: 80rpx;
+	line-height: 80rpx;
+	text-align: center;
+	border: 1rpx solid #ccc;
+	border-radius: 8rpx;
+	font-size: 28rpx;
+	color: #666;
+
+	&.confirm {
+		background: #0879ff;
+		border-color: #0879ff;
+		color: #fff;
+	}
 }
 </style>

+ 129 - 0
src/workbench/bank/add.vue

@@ -0,0 +1,129 @@
+<template>
+	<view class="wb-page">
+		<view class="wb-section">
+			<view class="form-item">
+				<text class="label">银行名称</text>
+				<view class="picker-value" @click="showPicker = true">
+					{{ blankName }}
+					<text class="arrow">▼</text>
+				</view>
+			</view>
+			<view class="form-item">
+				<text class="label">开户行</text>
+				<input class="wb-input" v-model="openingBank" placeholder="请输入开户行" />
+			</view>
+			<view class="form-item">
+				<text class="label">银行卡号</text>
+				<input class="wb-input" v-model="cardNum" type="number" placeholder="请输入银行卡号" />
+			</view>
+			<view class="form-item">
+				<text class="label">姓名</text>
+				<input class="wb-input" v-model="name" placeholder="请输入您的姓名" />
+			</view>
+		</view>
+		<view class="wb-btn primary" @click="submit">添加银行卡</view>
+
+		<u-picker
+			:show="showPicker"
+			:columns="[banklist]"
+			keyName="bankName"
+			@cancel="showPicker = false"
+			@confirm="onSelect"
+		></u-picker>
+	</view>
+</template>
+
+<script>
+import { addBnak } from '@/api/index'
+
+const MOCK_BANK_LIST = [
+	{ id: '1', bankName: '中国工商银行' },
+	{ id: '2', bankName: '中国农业银行' },
+	{ id: '3', bankName: '中国银行' },
+	{ id: '4', bankName: '中国建设银行' },
+	{ id: '5', bankName: '交通银行' },
+	{ id: '6', bankName: '招商银行' },
+	{ id: '7', bankName: '中信银行' },
+	{ id: '8', bankName: '光大银行' },
+]
+
+export default {
+	data() {
+		return {
+			blankName: '请选择',
+			openingBank: '',
+			cardNum: '',
+			name: '',
+			showPicker: false,
+			banklist: MOCK_BANK_LIST,
+			bankId: '',
+		}
+	},
+	methods: {
+		onSelect(e) {
+			this.bankId = e.value[0].id
+			this.blankName = e.value[0].bankName
+			this.showPicker = false
+		},
+		submit() {
+			const bankCardRegex = /^[1-9]\d{15,18}$/
+			if (!bankCardRegex.test(this.cardNum)) {
+				uni.showToast({ title: '请输入正确的银行卡号', icon: 'none' })
+				return
+			}
+			if (!this.bankId) {
+				uni.showToast({ title: '请选择银行', icon: 'none' })
+				return
+			}
+			if (!this.openingBank) {
+				uni.showToast({ title: '请输入开户行', icon: 'none' })
+				return
+			}
+			addBnak({
+				bankCardNum: this.cardNum,
+				bankId: this.bankId,
+				openId: uni.getStorageSync('wx_copenid'),
+				openingBank: this.openingBank,
+			}).then(res => {
+				if (res.data.code == 200) {
+					uni.showToast({ title: '添加成功', icon: 'none' })
+					setTimeout(() => uni.navigateBack(), 1000)
+				}
+			})
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../common/page.scss';
+
+.form-item {
+	margin-bottom: 24rpx;
+}
+
+.label {
+	display: block;
+	font-size: 26rpx;
+	color: #666;
+	margin-bottom: 12rpx;
+}
+
+.picker-value {
+	height: 72rpx;
+	line-height: 72rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 6rpx;
+	padding: 0 20rpx;
+	font-size: 26rpx;
+	color: #333;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+.arrow {
+	font-size: 20rpx;
+	color: #999;
+}
+</style>

+ 42 - 0
src/workbench/bank/detail.vue

@@ -0,0 +1,42 @@
+<template>
+	<view class="wb-page">
+		<view class="wb-section">
+			<view class="wb-row">
+				<text class="wb-label">银行名称</text>
+				<text class="wb-value">{{ detail.bankName || '-' }}</text>
+			</view>
+			<view class="wb-row">
+				<text class="wb-label">银行卡号</text>
+				<text class="wb-value">{{ maskCard(detail.bankCardNum) }}</text>
+			</view>
+			<view class="wb-row">
+				<text class="wb-label">开户行</text>
+				<text class="wb-value">{{ detail.openingBank || '-' }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			detail: {},
+		}
+	},
+	onLoad(query) {
+		this.detail = { ...query }
+	},
+	methods: {
+		maskCard(num) {
+			if (!num) return '-'
+			const str = String(num)
+			return `**** **** **** ${str.slice(-4)}`
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../common/page.scss';
+</style>

+ 102 - 0
src/workbench/bank/index.vue

@@ -0,0 +1,102 @@
+<template>
+	<view class="wb-page">
+		<view class="wb-section" v-if="cardList.length">
+			<view
+				class="card-item"
+				v-for="(item, index) in cardList"
+				:key="index"
+				@click="goDetail(item)"
+			>
+				<view class="card-left">
+					<view class="bank-name">{{ item.bankName }}</view>
+					<view class="bank-num">**** **** **** {{ tailNum(item.bankCardNum) }}</view>
+					<view class="bank-branch">{{ item.openingBank || '' }}</view>
+				</view>
+				<text class="arrow">&gt;</text>
+			</view>
+		</view>
+		<view class="wb-empty" v-else>暂无银行卡</view>
+
+		<view class="wb-footer-bar">
+			<view class="wb-btn primary" @click="goAdd">添加新卡</view>
+		</view>
+		<u-gap height="120"></u-gap>
+	</view>
+</template>
+
+<script>
+import { myBank } from '@/api/index'
+
+export default {
+	data() {
+		return {
+			cardList: [],
+		}
+	},
+	onShow() {
+		this.loadList()
+	},
+	methods: {
+		loadList() {
+			myBank().then(res => {
+				if (res.data.code == 200 && Array.isArray(res.data.data)) {
+					this.cardList = res.data.data
+				}
+			})
+		},
+		tailNum(num) {
+			return num ? String(num).slice(-4) : '****'
+		},
+		goDetail(item) {
+			const str = uni.$u.queryParams({
+				id: item.id,
+				bankName: item.bankName,
+				bankCardNum: item.bankCardNum,
+				openingBank: item.openingBank || '',
+			})
+			uni.navigateTo({ url: `/workbench/bank/detail${str}` })
+		},
+		goAdd() {
+			uni.navigateTo({ url: '/workbench/bank/add' })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../common/page.scss';
+
+.card-item {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 24rpx 0;
+	border-bottom: 1rpx solid #eee;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.bank-name {
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.bank-num {
+	font-size: 28rpx;
+	color: #666;
+	margin: 12rpx 0;
+}
+
+.bank-branch {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.arrow {
+	color: #999;
+	font-size: 28rpx;
+}
+</style>

+ 246 - 0
src/workbench/city/apply.vue

@@ -0,0 +1,246 @@
+<template>
+	<view class="apply-page">
+		<view class="form-section">
+			<view class="form-row" @click="showCityPicker = true">
+				<text class="label">城市</text>
+				<text class="value" :class="{ placeholder: !cityText }">
+					{{ cityText || '选择城市' }}
+				</text>
+				<text class="chevron">&gt;</text>
+			</view>
+			<view class="form-row" @click="openCenterPicker">
+				<text class="label">运营中心</text>
+				<text class="value" :class="{ placeholder: !form.centerName }">
+					{{ form.centerName || '选择' }}
+				</text>
+				<text class="chevron">&gt;</text>
+			</view>
+			<view class="reason-block">
+				<text class="label">申请原因</text>
+				<textarea
+					class="reason-input"
+					v-model="form.reason"
+					placeholder="请输入原因"
+					maxlength="500"
+					@input="onReasonInput"
+				/>
+				<text class="char-count">{{ form.reason.length }}/500</text>
+			</view>
+		</view>
+
+		<view class="footer-bar">
+			<view class="footer-btn" @click="onSubmit">申请开通</view>
+		</view>
+
+		<!-- 省 / 市 二级联动 -->
+		<u-picker
+			:show="showCityPicker"
+			:columns="cityColumns"
+			keyName="name"
+			@change="onCityColumnChange"
+			@confirm="onCityConfirm"
+			@cancel="showCityPicker = false"
+		></u-picker>
+
+		<!-- 运营中心 -->
+		<u-picker
+			:show="showCenterPicker"
+			:columns="[centerOptions]"
+			keyName="name"
+			@confirm="onCenterConfirm"
+			@cancel="showCenterPicker = false"
+		></u-picker>
+	</view>
+</template>
+
+<script>
+import {
+	PROVINCE_CITY_TREE,
+	getCitiesByProvince,
+	getCentersByCity,
+} from './mock.js'
+
+export default {
+	data() {
+		return {
+			form: {
+				province: '',
+				city: '',
+				centerId: '',
+				centerName: '',
+				reason: '',
+			},
+			showCityPicker: false,
+			showCenterPicker: false,
+			cityColumns: [],
+			centerOptions: [],
+			provinceIndex: 0,
+		}
+	},
+	computed: {
+		cityText() {
+			if (!this.form.province || !this.form.city) return ''
+			if (this.form.province === this.form.city) return this.form.city
+			return `${this.form.province} ${this.form.city}`
+		},
+	},
+	onLoad() {
+		this.initCityPicker()
+	},
+	methods: {
+		initCityPicker() {
+			const provinces = PROVINCE_CITY_TREE.map(p => ({ name: p.name }))
+			const cities = getCitiesByProvince(provinces[0]?.name || '').map(c => ({ name: c.name }))
+			this.cityColumns = [provinces, cities]
+		},
+		onCityColumnChange(e) {
+			const { columnIndex, index } = e
+			if (columnIndex !== 0) return
+			this.provinceIndex = index
+			const provinceName = PROVINCE_CITY_TREE[index]?.name
+			const cities = getCitiesByProvince(provinceName).map(c => ({ name: c.name }))
+			this.cityColumns = [
+				this.cityColumns[0],
+				cities,
+			]
+		},
+		onCityConfirm(e) {
+			const province = e.value[0]?.name || ''
+			const city = e.value[1]?.name || ''
+			if (province !== this.form.province || city !== this.form.city) {
+				this.form.centerId = ''
+				this.form.centerName = ''
+				this.centerOptions = []
+			}
+			this.form.province = province
+			this.form.city = city
+			this.showCityPicker = false
+		},
+		openCenterPicker() {
+			if (!this.form.province || !this.form.city) {
+				uni.showToast({ title: '请先选择城市', icon: 'none' })
+				return
+			}
+			this.centerOptions = getCentersByCity(this.form.province, this.form.city)
+			if (!this.centerOptions.length) {
+				uni.showToast({ title: '该城市暂无运营中心', icon: 'none' })
+				return
+			}
+			this.showCenterPicker = true
+		},
+		onCenterConfirm(e) {
+			const center = e.value[0]
+			this.form.centerId = center.id
+			this.form.centerName = center.name
+			this.showCenterPicker = false
+		},
+		onReasonInput(e) {
+			const val = (e.detail.value || '').slice(0, 500)
+			this.form.reason = val
+		},
+		onSubmit() {
+			if (!this.form.province || !this.form.city) {
+				uni.showToast({ title: '请选择城市', icon: 'none' })
+				return
+			}
+			if (!this.form.centerId) {
+				uni.showToast({ title: '请选择运营中心', icon: 'none' })
+				return
+			}
+			if (!this.form.reason.trim()) {
+				uni.showToast({ title: '请输入申请原因', icon: 'none' })
+				return
+			}
+			// 提交接口待对接
+			uni.showToast({ title: '已提交,待审核', icon: 'none' })
+			setTimeout(() => uni.navigateBack(), 1000)
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.apply-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 140rpx;
+}
+
+.form-section {
+	background: #fff;
+}
+
+.form-row {
+	display: flex;
+	align-items: center;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.label {
+	width: 160rpx;
+	flex-shrink: 0;
+	font-size: 30rpx;
+	color: #333;
+}
+
+.value {
+	flex: 1;
+	text-align: right;
+	font-size: 28rpx;
+	color: #333;
+
+	&.placeholder {
+		color: #ccc;
+	}
+}
+
+.chevron {
+	margin-left: 8rpx;
+	font-size: 28rpx;
+	color: #999;
+}
+
+.reason-block {
+	padding: 28rpx 32rpx;
+}
+
+.reason-input {
+	width: 100%;
+	min-height: 240rpx;
+	margin-top: 16rpx;
+	font-size: 28rpx;
+	color: #333;
+	line-height: 1.6;
+	box-sizing: border-box;
+}
+
+.char-count {
+	display: block;
+	text-align: right;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 12rpx;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}
+
+.footer-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	background: #333;
+	color: #fff;
+	font-size: 32rpx;
+	border-radius: 12rpx;
+}
+</style>

+ 146 - 0
src/workbench/city/index.vue

@@ -0,0 +1,146 @@
+<template>
+	<view class="city-page">
+		<view class="city-list" v-if="recordList.length">
+			<view class="city-card" v-for="item in recordList" :key="item.id">
+				<view class="card-head">
+					<text class="center-name">{{ item.centerName }}</text>
+					<text class="status-tag" :class="statusClass(item.status)">
+						{{ statusLabel(item.status) }}
+					</text>
+				</view>
+				<text class="meta">开通原因:{{ item.reason || '暂无' }}</text>
+				<text class="meta">开通时间:{{ item.openTime }}</text>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无城市记录</view>
+
+		<view class="footer-bar">
+			<view class="footer-btn" @click="goApply">开通新城市</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { MOCK_CITY_RECORDS, CITY_STATUS_MAP } from './mock.js'
+
+export default {
+	data() {
+		return {
+			recordList: [],
+		}
+	},
+	onShow() {
+		this.loadList()
+	},
+	methods: {
+		loadList() {
+			this.recordList = [...MOCK_CITY_RECORDS]
+		},
+		statusLabel(status) {
+			return CITY_STATUS_MAP[status]?.label || status
+		},
+		statusClass(status) {
+			return CITY_STATUS_MAP[status]?.class || ''
+		},
+		goApply() {
+			uni.navigateTo({ url: '/workbench/city/apply' })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.city-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding: 24rpx 24rpx 140rpx;
+	box-sizing: border-box;
+}
+
+.city-list {
+	display: flex;
+	flex-direction: column;
+	gap: 20rpx;
+}
+
+.city-card {
+	background: #fff;
+	border-radius: 12rpx;
+	padding: 28rpx 32rpx;
+}
+
+.card-head {
+	display: flex;
+	align-items: flex-start;
+	justify-content: space-between;
+	margin-bottom: 16rpx;
+}
+
+.center-name {
+	flex: 1;
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 600;
+	line-height: 1.4;
+	padding-right: 16rpx;
+}
+
+.status-tag {
+	flex-shrink: 0;
+	font-size: 22rpx;
+	padding: 4rpx 12rpx;
+	border-radius: 6rpx;
+
+	&.opened {
+		color: #333;
+		background: #f0f0f0;
+	}
+
+	&.pending {
+		color: #666;
+		background: #f5f5f5;
+		border: 1rpx solid #ddd;
+	}
+
+	&.rejected {
+		color: #999;
+		background: #fafafa;
+		border: 1rpx solid #eee;
+	}
+}
+
+.meta {
+	display: block;
+	font-size: 26rpx;
+	color: #999;
+	line-height: 1.6;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}
+
+.footer-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	background: #333;
+	color: #fff;
+	font-size: 32rpx;
+	border-radius: 12rpx;
+}
+</style>

+ 82 - 0
src/workbench/city/mock.js

@@ -0,0 +1,82 @@
+/** 城市管理假数据,接口就绪后替换 */
+
+export const CITY_STATUS_MAP = {
+	opened: { label: '已开通', class: 'opened' },
+	pending: { label: '审核中', class: 'pending' },
+	rejected: { label: '审核驳回', class: 'rejected' },
+}
+
+export const MOCK_CITY_RECORDS = [
+	{
+		id: '1',
+		centerName: '北京朝阳-望京运营中心',
+		status: 'opened',
+		reason: '暂无',
+		openTime: '2025.02.01 12:00:46',
+	},
+	{
+		id: '2',
+		centerName: '太原小店-长风运营中心',
+		status: 'pending',
+		reason: '拓展业务区域',
+		openTime: '2025.03.10 14:20:00',
+	},
+	{
+		id: '3',
+		centerName: '西安雁塔-高新运营中心',
+		status: 'rejected',
+		reason: '申请开通新店',
+		openTime: '2025.02.15 09:00:00',
+	},
+]
+
+/** 省 / 市 / 运营中心 级联假数据 */
+export const PROVINCE_CITY_TREE = [
+	{
+		name: '北京市',
+		cities: [
+			{
+				name: '北京市',
+				centers: [
+					{ id: 'bj1', name: '北京朝阳-望京运营中心' },
+					{ id: 'bj2', name: '北京海淀-中关村运营中心' },
+				],
+			},
+		],
+	},
+	{
+		name: '山西省',
+		cities: [
+			{
+				name: '太原市',
+				centers: [
+					{ id: 'ty1', name: '太原小店-长风运营中心' },
+					{ id: 'ty2', name: '太原万柏林-信达运营中心' },
+				],
+			},
+		],
+	},
+	{
+		name: '陕西省',
+		cities: [
+			{
+				name: '西安市',
+				centers: [
+					{ id: 'xa1', name: '西安雁塔-高新运营中心' },
+					{ id: 'xa2', name: '西安未央-凤城运营中心' },
+				],
+			},
+		],
+	},
+]
+
+export function getCitiesByProvince(provinceName) {
+	const province = PROVINCE_CITY_TREE.find(p => p.name === provinceName)
+	return province ? province.cities : []
+}
+
+export function getCentersByCity(provinceName, cityName) {
+	const cities = getCitiesByProvince(provinceName)
+	const city = cities.find(c => c.name === cityName)
+	return city ? city.centers : []
+}

+ 125 - 0
src/workbench/common/page.scss

@@ -0,0 +1,125 @@
+.wb-page {
+	min-height: 100vh;
+	padding: 24rpx;
+	background: #f5f5f5;
+	box-sizing: border-box;
+}
+
+.wb-section {
+	background: #fff;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	padding: 24rpx;
+	margin-bottom: 20rpx;
+	box-sizing: border-box;
+}
+
+.wb-title {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 600;
+	margin-bottom: 20rpx;
+}
+
+.wb-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 20rpx 0;
+	border-bottom: 1rpx solid #eee;
+	font-size: 26rpx;
+	color: #333;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.wb-label {
+	color: #666;
+	flex-shrink: 0;
+	margin-right: 24rpx;
+}
+
+.wb-value {
+	flex: 1;
+	text-align: right;
+	color: #333;
+}
+
+.wb-input {
+	width: 100%;
+	height: 72rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 6rpx;
+	padding: 0 20rpx;
+	font-size: 26rpx;
+	box-sizing: border-box;
+}
+
+.wb-btn {
+	height: 80rpx;
+	line-height: 80rpx;
+	text-align: center;
+	border: 1rpx solid #333;
+	border-radius: 8rpx;
+	font-size: 28rpx;
+	color: #333;
+	margin-top: 32rpx;
+
+	&.primary {
+		background: #eee;
+	}
+}
+
+.wb-link {
+	font-size: 24rpx;
+	color: #666;
+}
+
+.wb-empty {
+	text-align: center;
+	padding: 80rpx 0;
+	font-size: 26rpx;
+	color: #999;
+}
+
+.wb-tabs {
+	display: flex;
+	margin-bottom: 20rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.wb-tab {
+	flex: 1;
+	text-align: center;
+	padding: 16rpx 0;
+	font-size: 26rpx;
+	color: #999;
+	border-bottom: 4rpx solid transparent;
+
+	&.active {
+		color: #333;
+		border-bottom-color: #333;
+	}
+}
+
+.wb-list-item {
+	padding: 24rpx 0;
+	border-bottom: 1rpx solid #eee;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.wb-footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20rpx 24rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}

+ 63 - 0
src/workbench/contract/detail.vue

@@ -0,0 +1,63 @@
+<template>
+	<view class="detail-page">
+		<!-- PDF 预览 -->
+		<web-view v-if="fileType === 'pdf' && fileUrl" :src="fileUrl"></web-view>
+
+		<!-- 图片预览 -->
+		<scroll-view v-else-if="fileType === 'image' && fileUrl" scroll-y class="image-scroll">
+			<image class="contract-image" :src="fileUrl" mode="widthFix" @click="previewImage" />
+		</scroll-view>
+
+		<view class="empty" v-else>暂无合同文件</view>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			fileType: '',
+			fileUrl: '',
+		}
+	},
+	onLoad(query) {
+		this.fileType = query.fileType || 'pdf'
+		this.fileUrl = decodeURIComponent(query.fileUrl || '')
+		if (query.title) {
+			uni.setNavigationBarTitle({ title: query.title })
+		}
+	},
+	methods: {
+		previewImage() {
+			if (!this.fileUrl) return
+			uni.previewImage({
+				urls: [this.fileUrl],
+				current: this.fileUrl,
+			})
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.detail-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+}
+
+.image-scroll {
+	height: 100vh;
+}
+
+.contract-image {
+	width: 100%;
+	display: block;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>

+ 90 - 0
src/workbench/contract/index.vue

@@ -0,0 +1,90 @@
+<template>
+	<view class="contract-page">
+		<view class="contract-list" v-if="contractList.length">
+			<view
+				class="contract-card"
+				v-for="item in contractList"
+				:key="item.id"
+				@click="viewContract(item)"
+			>
+				<text class="contract-name">{{ item.title }}</text>
+				<text class="contract-meta">签定时间:{{ item.signTime }}</text>
+				<text class="contract-meta">签订人:{{ item.signatory }}</text>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无合同</view>
+	</view>
+</template>
+
+<script>
+import { MOCK_CONTRACT_LIST } from './mock.js'
+
+export default {
+	data() {
+		return {
+			contractList: [],
+		}
+	},
+	onShow() {
+		this.loadList()
+	},
+	methods: {
+		loadList() {
+			// 接口就绪后替换
+			this.contractList = [...MOCK_CONTRACT_LIST]
+		},
+		viewContract(item) {
+			const str = uni.$u.queryParams({
+				id: item.id,
+				title: item.title,
+				fileType: item.fileType,
+				fileUrl: item.fileUrl,
+			})
+			uni.navigateTo({ url: `/workbench/contract/detail${str}` })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.contract-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding: 24rpx;
+	box-sizing: border-box;
+}
+
+.contract-list {
+	display: flex;
+	flex-direction: column;
+	gap: 20rpx;
+}
+
+.contract-card {
+	background: #fff;
+	border-radius: 12rpx;
+	padding: 28rpx 32rpx;
+}
+
+.contract-name {
+	display: block;
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 600;
+	margin-bottom: 16rpx;
+}
+
+.contract-meta {
+	display: block;
+	font-size: 26rpx;
+	color: #999;
+	line-height: 1.6;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>

+ 19 - 0
src/workbench/contract/mock.js

@@ -0,0 +1,19 @@
+/** 合同模块假数据,接口就绪后替换 */
+export const MOCK_CONTRACT_LIST = [
+	{
+		id: '1',
+		title: '合同名称',
+		signTime: '2025.02.01 12:00:46',
+		signatory: '**瑞',
+		fileType: 'pdf',
+		fileUrl: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
+	},
+	{
+		id: '2',
+		title: '商户服务协议',
+		signTime: '2024.11.15 09:30:20',
+		signatory: '张*',
+		fileType: 'image',
+		fileUrl: 'https://cdn.uviewui.com/uview/swiper/1.jpg',
+	},
+]

+ 415 - 0
src/workbench/fare/index.vue

@@ -0,0 +1,415 @@
+<template>
+	<view class="fare-page">
+		<!-- 设置方式 -->
+		<view class="form-section">
+			<view
+				class="mode-row"
+				:class="{ clickable: canSwitchMode }"
+				@click="openModeSheet"
+			>
+				<text class="mode-label">免车费</text>
+				<text class="mode-value">
+					{{ settingModeLabel }}
+					<text v-if="canSwitchMode" class="chevron">&gt;</text>
+				</text>
+			</view>
+		</view>
+
+		<!-- 统一设置 -->
+		<view class="form-section" v-if="settingMode === 'unified'">
+			<view class="km-row">
+				<view class="km-label-wrap">
+					<text class="km-label">白天免费公里数</text>
+					<text class="km-time">7:30 至 19:30</text>
+				</view>
+				<view class="km-input-wrap">
+					<input
+						class="km-input"
+						v-model="unifiedForm.dayKm"
+						type="number"
+						placeholder="请输入里程"
+						placeholder-class="placeholder"
+						@input="onKmInput('unified', 'dayKm', $event)"
+					/>
+					<text class="km-unit">km</text>
+				</view>
+			</view>
+			<view class="km-row">
+				<view class="km-label-wrap">
+					<text class="km-label">夜间免费公里数</text>
+					<text class="km-time">20:00 至 7:00</text>
+				</view>
+				<view class="km-input-wrap">
+					<input
+						class="km-input"
+						v-model="unifiedForm.nightKm"
+						type="number"
+						placeholder="请输入里程"
+						placeholder-class="placeholder"
+						@input="onKmInput('unified', 'nightKm', $event)"
+					/>
+					<text class="km-unit">km</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 按项目分类设置 -->
+		<view v-if="settingMode === 'byProject'">
+			<view
+				class="form-section project-block"
+				v-for="item in projectForms"
+				:key="item.categoryId"
+			>
+				<view class="service-row">
+					<text class="service-label">服务</text>
+					<text class="service-name">{{ item.categoryName }}</text>
+				</view>
+				<view class="km-row">
+					<view class="km-label-wrap">
+						<text class="km-label">白天免费公里数</text>
+						<text class="km-time">7:30 至 19:30</text>
+					</view>
+					<view class="km-input-wrap">
+						<input
+							class="km-input"
+							v-model="item.dayKm"
+							type="number"
+							placeholder="请输入里程"
+							placeholder-class="placeholder"
+							@input="onKmInput('project', 'dayKm', $event, item.categoryId)"
+						/>
+						<text class="km-unit">km</text>
+					</view>
+				</view>
+				<view class="km-row">
+					<view class="km-label-wrap">
+						<text class="km-label">夜间免费公里数</text>
+						<text class="km-time">20:00 至 7:00</text>
+					</view>
+					<view class="km-input-wrap">
+						<input
+							class="km-input"
+							v-model="item.nightKm"
+							type="number"
+							placeholder="请输入里程"
+							placeholder-class="placeholder"
+							@input="onKmInput('project', 'nightKm', $event, item.categoryId)"
+						/>
+						<text class="km-unit">km</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<view class="footer-bar">
+			<view class="save-btn" @click="onSave">保存</view>
+		</view>
+
+		<!-- 设置方式底部弹窗 -->
+		<view class="sheet-mask" v-if="showModeSheet" @click="showModeSheet = false">
+			<view class="sheet-panel" @click.stop>
+				<view class="sheet-header">
+					<text class="sheet-title">免车费设置</text>
+					<text class="sheet-close" @click="showModeSheet = false">×</text>
+				</view>
+				<view
+					class="sheet-option"
+					v-for="opt in modeOptions"
+					:key="opt.value"
+					@click="selectMode(opt.value)"
+				>
+					<text class="option-text">{{ opt.label }}</text>
+					<text class="option-check" v-if="settingMode === opt.value">✓</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+/** 假数据:用户已开通的服务分类,接口就绪后替换 */
+const MOCK_OPENED_CATEGORIES = [
+	{ id: '1', name: '上门按摩' },
+	{ id: '3', name: '爬山' },
+]
+
+const MODE_OPTIONS = [
+	{ value: 'unified', label: '统一设置' },
+	{ value: 'byProject', label: '按项目分类设置' },
+]
+
+export default {
+	data() {
+		return {
+			openedCategories: [...MOCK_OPENED_CATEGORIES],
+			settingMode: 'unified',
+			modeOptions: MODE_OPTIONS,
+			unifiedForm: {
+				dayKm: '',
+				nightKm: '',
+			},
+			projectForms: [],
+			showModeSheet: false,
+		}
+	},
+	computed: {
+		canSwitchMode() {
+			return this.openedCategories.length > 1
+		},
+		settingModeLabel() {
+			const opt = MODE_OPTIONS.find(o => o.value === this.settingMode)
+			return opt ? opt.label : '统一设置'
+		},
+	},
+	onLoad() {
+		this.initProjectForms()
+		if (this.openedCategories.length <= 1) {
+			this.settingMode = 'unified'
+		}
+	},
+	methods: {
+		initProjectForms() {
+			this.projectForms = this.openedCategories.map(cat => ({
+				categoryId: cat.id,
+				categoryName: cat.name,
+				dayKm: '',
+				nightKm: '',
+			}))
+		},
+		openModeSheet() {
+			if (!this.canSwitchMode) return
+			this.showModeSheet = true
+		},
+		selectMode(mode) {
+			this.settingMode = mode
+			this.showModeSheet = false
+		},
+		onKmInput(scope, field, e, categoryId) {
+			const val = String(e.detail.value || '').replace(/\D/g, '')
+			if (scope === 'unified') {
+				this.unifiedForm[field] = val
+			} else {
+				const item = this.projectForms.find(p => p.categoryId === categoryId)
+				if (item) item[field] = val
+			}
+		},
+		validateForm() {
+			if (this.settingMode === 'unified') {
+				if (!this.unifiedForm.dayKm || !this.unifiedForm.nightKm) {
+					return false
+				}
+				return true
+			}
+			return this.projectForms.every(item => item.dayKm && item.nightKm)
+		},
+		onSave() {
+			if (!this.validateForm()) {
+				uni.showToast({ title: '请输入公里数', icon: 'none' })
+				return
+			}
+			// 保存接口待对接
+			uni.showToast({ title: '设置成功', icon: 'none' })
+			setTimeout(() => uni.navigateBack(), 1000)
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.fare-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 140rpx;
+}
+
+.form-section {
+	background: #fff;
+	margin-bottom: 16rpx;
+}
+
+.mode-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+
+	&.clickable:active {
+		background: #fafafa;
+	}
+}
+
+.mode-label {
+	font-size: 30rpx;
+	color: #333;
+}
+
+.mode-value {
+	font-size: 28rpx;
+	color: #666;
+}
+
+.chevron {
+	margin-left: 8rpx;
+	color: #999;
+}
+
+.km-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.km-label-wrap {
+	flex-shrink: 0;
+}
+
+.km-label {
+	display: block;
+	font-size: 30rpx;
+	color: #333;
+	margin-bottom: 8rpx;
+}
+
+.km-time {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.km-input-wrap {
+	display: flex;
+	align-items: center;
+	flex: 1;
+	justify-content: flex-end;
+	margin-left: 24rpx;
+}
+
+.km-input {
+	flex: 1;
+	text-align: right;
+	font-size: 28rpx;
+	color: #333;
+	max-width: 240rpx;
+}
+
+.placeholder {
+	color: #ccc;
+	font-size: 28rpx;
+}
+
+.km-unit {
+	font-size: 28rpx;
+	color: #333;
+	margin-left: 8rpx;
+	flex-shrink: 0;
+}
+
+.project-block {
+	margin-bottom: 16rpx;
+}
+
+.service-row {
+	display: flex;
+	align-items: center;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.service-label {
+	font-size: 30rpx;
+	color: #333;
+	margin-right: 24rpx;
+}
+
+.service-name {
+	font-size: 28rpx;
+	color: #666;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}
+
+.save-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	background: #333;
+	color: #fff;
+	font-size: 32rpx;
+	border-radius: 12rpx;
+}
+
+.sheet-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 999;
+	display: flex;
+	align-items: flex-end;
+}
+
+.sheet-panel {
+	width: 100%;
+	background: #fff;
+	border-radius: 24rpx 24rpx 0 0;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 24rpx);
+}
+
+.sheet-header {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	position: relative;
+	padding: 32rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.sheet-title {
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #333;
+}
+
+.sheet-close {
+	position: absolute;
+	right: 32rpx;
+	font-size: 40rpx;
+	color: #999;
+	line-height: 1;
+}
+
+.sheet-option {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.option-text {
+	font-size: 30rpx;
+	color: #333;
+}
+
+.option-check {
+	font-size: 32rpx;
+	color: #0879ff;
+}
+</style>

+ 433 - 0
src/workbench/income/index.vue

@@ -0,0 +1,433 @@
+<template>
+	<view class="income-page">
+		<!-- 筛选栏 -->
+		<view class="filter-bar">
+			<view class="filter-item" @click="openFilterModal">
+				<text>{{ serviceFilterLabel }}</text>
+				<text class="arrow">▼</text>
+			</view>
+			<view class="filter-item" @click="openFilterModal">
+				<text>{{ statusFilterLabel }}</text>
+				<text class="arrow">▼</text>
+			</view>
+		</view>
+
+		<!-- 日期 & 合计 -->
+		<view class="summary-row">
+			<view class="date-range" @click="openDatePicker">
+				<text>{{ dateRangeText }}</text>
+				<text class="arrow">▼</text>
+			</view>
+			<text class="total" @click="onTotalClick">合计: {{ totalText }}</text>
+		</view>
+
+		<!-- 收入列表 -->
+		<view class="income-list" v-if="displayList.length">
+			<view class="income-card" v-for="item in displayList" :key="item.id">
+				<view class="card-left">
+					<view class="category-icon"></view>
+					<view class="card-info">
+						<view class="info-top">
+							<text class="category-name">{{ item.categoryName }}</text>
+						</view>
+						<text class="service-name">{{ item.serviceName }}</text>
+						<text class="order-time">{{ item.completionTime }}</text>
+					</view>
+				</view>
+				<view class="card-right">
+					<text class="settle-status" :class="item.status">
+						{{ item.status === 'settled' ? '已结算' : '待结算' }}
+					</text>
+					<text class="income-amount">+{{ formatAmount(item.incomeAmount) }}</text>
+					<text class="order-total">{{ formatAmount(item.orderTotal) }}</text>
+				</view>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无收入记录</view>
+
+		<!-- 筛选弹窗 -->
+		<view class="modal-mask" v-if="showFilterModal" @click="showFilterModal = false">
+			<view class="filter-panel" @click.stop>
+				<view class="panel-section">
+					<text class="section-title">服务分类</text>
+					<view class="tag-list">
+						<view
+							v-for="cat in serviceCategories"
+							:key="cat.id"
+							class="tag"
+							:class="{ active: draftCategoryId === cat.id }"
+							@click="draftCategoryId = cat.id"
+						>{{ cat.name }}</view>
+					</view>
+				</view>
+				<view class="panel-section">
+					<text class="section-title">结算状态</text>
+					<view class="tag-list">
+						<view
+							v-for="opt in settleOptions"
+							:key="opt.key"
+							class="tag"
+							:class="{ active: draftStatus === opt.key }"
+							@click="draftStatus = opt.key"
+						>{{ opt.label }}</view>
+					</view>
+				</view>
+				<view class="panel-btns">
+					<view class="btn cancel" @click="showFilterModal = false">取消</view>
+					<view class="btn confirm" @click="confirmFilter">确定</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 日期选择 -->
+		<u-datetime-picker
+			:show="showDatePicker"
+			v-model="pickerValue"
+			mode="date"
+			@confirm="onDateConfirm"
+			@cancel="showDatePicker = false"
+		></u-datetime-picker>
+	</view>
+</template>
+
+<script>
+import {
+	SERVICE_CATEGORIES,
+	SETTLE_STATUS_OPTIONS,
+	MOCK_INCOME_LIST,
+	formatIncomeTotal,
+	formatDateYmd,
+	getMockDateRange,
+	parseTimeStr,
+} from './mock.js'
+
+export default {
+	data() {
+		const range = getMockDateRange()
+		return {
+			allList: [...MOCK_INCOME_LIST],
+			serviceCategories: SERVICE_CATEGORIES,
+			settleOptions: SETTLE_STATUS_OPTIONS,
+			categoryId: 'all',
+			settleStatus: 'all',
+			draftCategoryId: 'all',
+			draftStatus: 'all',
+			dateStart: range.start,
+			dateEnd: range.end,
+			dateStartTs: range.startTs,
+			dateEndTs: range.endTs,
+			showFilterModal: false,
+			showDatePicker: false,
+			pickerValue: Date.now(),
+			datePickTarget: 'start',
+		}
+	},
+	computed: {
+		serviceFilterLabel() {
+			if (this.categoryId === 'all') return '全部服务'
+			const cat = this.serviceCategories.find(c => c.id === this.categoryId)
+			return cat ? cat.name : '全部服务'
+		},
+		statusFilterLabel() {
+			if (this.settleStatus === 'all') return '结算状态'
+			const opt = this.settleOptions.find(o => o.key === this.settleStatus)
+			return opt ? opt.label : '结算状态'
+		},
+		dateRangeText() {
+			return `${this.dateStart} 至 ${this.dateEnd}`
+		},
+		displayList() {
+			let list = [...this.allList]
+			if (this.categoryId !== 'all') {
+				list = list.filter(item => item.categoryId === this.categoryId)
+			}
+			if (this.settleStatus === 'settled') {
+				list = list.filter(item => item.status === 'settled')
+			} else if (this.settleStatus === 'unsettled') {
+				list = list.filter(item => item.status === 'unsettled')
+			}
+			list = list.filter(item => {
+				const ts = parseTimeStr(item.completionTime)
+				return ts >= this.dateStartTs && ts <= this.dateEndTs + 86400000 - 1
+			})
+			return list.sort((a, b) => parseTimeStr(b.completionTime) - parseTimeStr(a.completionTime))
+		},
+		totalAmount() {
+			return this.displayList.reduce((sum, item) => sum + Number(item.incomeAmount || 0), 0)
+		},
+		totalText() {
+			return formatIncomeTotal(this.totalAmount)
+		},
+	},
+	methods: {
+		formatAmount(val) {
+			return Number(val || 0).toFixed(2)
+		},
+		openFilterModal() {
+			this.draftCategoryId = this.categoryId
+			this.draftStatus = this.settleStatus
+			this.showFilterModal = true
+		},
+		confirmFilter() {
+			this.categoryId = this.draftCategoryId
+			this.settleStatus = this.draftStatus
+			this.showFilterModal = false
+		},
+		openDatePicker() {
+			this.datePickTarget = 'start'
+			this.pickerValue = this.dateStartTs
+			this.showDatePicker = true
+		},
+		onDateConfirm(e) {
+			const ts = e.value
+			const formatted = formatDateYmd(new Date(ts))
+			if (this.datePickTarget === 'start') {
+				this.dateStart = formatted
+				this.dateStartTs = ts
+				this.datePickTarget = 'end'
+				this.pickerValue = this.dateEndTs
+				this.showDatePicker = false
+				this.$nextTick(() => {
+					this.showDatePicker = true
+				})
+			} else {
+				this.dateEnd = formatted
+				this.dateEndTs = ts
+				if (this.dateStartTs > this.dateEndTs) {
+					const tmpS = this.dateStart
+					const tmpSt = this.dateStartTs
+					this.dateStart = this.dateEnd
+					this.dateEnd = tmpS
+					this.dateStartTs = this.dateEndTs
+					this.dateEndTs = tmpSt
+				}
+				this.showDatePicker = false
+			}
+		},
+		onTotalClick() {
+			uni.showToast({ title: '收入明细待对接', icon: 'none' })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.income-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+}
+
+.filter-bar {
+	display: flex;
+	background: #fff;
+	border-bottom: 1rpx solid #eee;
+}
+
+.filter-item {
+	flex: 1;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 24rpx 0;
+	font-size: 28rpx;
+	color: #333;
+}
+
+.arrow {
+	font-size: 20rpx;
+	color: #999;
+	margin-left: 8rpx;
+}
+
+.summary-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 20rpx 32rpx;
+	background: #fff;
+	border-bottom: 1rpx solid #eee;
+}
+
+.date-range {
+	display: flex;
+	align-items: center;
+	font-size: 26rpx;
+	color: #333;
+}
+
+.total {
+	font-size: 26rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.income-list {
+	padding: 16rpx 0;
+}
+
+.income-card {
+	display: flex;
+	justify-content: space-between;
+	background: #fff;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.card-left {
+	display: flex;
+	flex: 1;
+	overflow: hidden;
+}
+
+.category-icon {
+	width: 72rpx;
+	height: 72rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	flex-shrink: 0;
+	margin-right: 20rpx;
+}
+
+.card-info {
+	flex: 1;
+	overflow: hidden;
+}
+
+.category-name {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.service-name {
+	display: block;
+	font-size: 26rpx;
+	color: #666;
+	margin-top: 8rpx;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.order-time {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 12rpx;
+}
+
+.card-right {
+	text-align: right;
+	flex-shrink: 0;
+	margin-left: 16rpx;
+}
+
+.settle-status {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-bottom: 8rpx;
+
+	&.unsettled {
+		color: #666;
+	}
+}
+
+.income-amount {
+	display: block;
+	font-size: 32rpx;
+	color: #e54d42;
+	font-weight: 600;
+}
+
+.order-total {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 4rpx;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+
+.modal-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 999;
+	display: flex;
+	align-items: flex-end;
+}
+
+.filter-panel {
+	width: 100%;
+	background: #fff;
+	border-radius: 24rpx 24rpx 0 0;
+	padding: 32rpx 32rpx calc(env(safe-area-inset-bottom) + 32rpx);
+}
+
+.panel-section {
+	margin-bottom: 32rpx;
+}
+
+.section-title {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 600;
+	margin-bottom: 20rpx;
+}
+
+.tag-list {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 16rpx;
+}
+
+.tag {
+	padding: 12rpx 28rpx;
+	font-size: 26rpx;
+	color: #666;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	background: #fff;
+
+	&.active {
+		color: #0879ff;
+		border-color: #0879ff;
+		background: #f0f7ff;
+	}
+}
+
+.panel-btns {
+	display: flex;
+	gap: 20rpx;
+	margin-top: 16rpx;
+}
+
+.btn {
+	flex: 1;
+	height: 80rpx;
+	line-height: 80rpx;
+	text-align: center;
+	border-radius: 8rpx;
+	font-size: 30rpx;
+
+	&.cancel {
+		border: 1rpx solid #ddd;
+		color: #666;
+	}
+
+	&.confirm {
+		background: #0879ff;
+		color: #fff;
+	}
+}
+</style>

+ 223 - 0
src/workbench/income/mock.js

@@ -0,0 +1,223 @@
+/** 我的收入假数据,接口就绪后替换 */
+
+export const SERVICE_CATEGORIES = [
+	{ id: 'all', name: '全部' },
+	{ id: '1', name: '上门按摩' },
+	{ id: '2', name: '爬山' },
+	{ id: '3', name: '台球' },
+]
+
+export const SETTLE_STATUS_OPTIONS = [
+	{ key: 'all', label: '全部' },
+	{ key: 'settled', label: '已结算' },
+	{ key: 'unsettled', label: '未结算' },
+]
+
+/**
+ * 原型演示用默认日期:2025.01.01 至 2025.12.31
+ * 接口对接后可改回 getDefaultDateRange()
+ */
+export const MOCK_DEFAULT_DATE_RANGE = {
+	start: '2025.01.01',
+	end: '2025.12.31',
+	startTs: new Date('2025/01/01').getTime(),
+	endTs: new Date('2025/12/31 23:59:59').getTime(),
+}
+
+/** 合计约 1,253.12 元,与原型一致 */
+export const MOCK_INCOME_LIST = [
+	{
+		id: '1',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '会所水疗+中式推拿',
+		incomeAmount: 60.58,
+		orderTotal: 268.0,
+		completionTime: '2025-12-14 12:02:45',
+		status: 'unsettled',
+	},
+	{
+		id: '2',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '会所水疗+中式推拿',
+		incomeAmount: 60.58,
+		orderTotal: 268.0,
+		completionTime: '2025-12-13 15:20:30',
+		status: 'unsettled',
+	},
+	{
+		id: '3',
+		categoryId: '2',
+		categoryName: '爬山',
+		categoryIcon: '',
+		serviceName: '户外徒步向导',
+		incomeAmount: 88.0,
+		orderTotal: 150.0,
+		completionTime: '2025-12-12 08:30:00',
+		status: 'unsettled',
+	},
+	{
+		id: '4',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '会所水疗+中式推拿',
+		incomeAmount: 60.58,
+		orderTotal: 268.0,
+		completionTime: '2025-12-10 18:45:12',
+		status: 'unsettled',
+	},
+	{
+		id: '5',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '中式推拿-培元疏通',
+		incomeAmount: 128.5,
+		orderTotal: 200.0,
+		completionTime: '2025-11-28 16:45:33',
+		status: 'settled',
+	},
+	{
+		id: '6',
+		categoryId: '2',
+		categoryName: '爬山',
+		categoryIcon: '',
+		serviceName: '山地越野陪同',
+		incomeAmount: 95.0,
+		orderTotal: 180.0,
+		completionTime: '2025-11-15 07:00:00',
+		status: 'settled',
+	},
+	{
+		id: '7',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '泰式SPA',
+		incomeAmount: 72.3,
+		orderTotal: 268.0,
+		completionTime: '2025-10-20 20:10:08',
+		status: 'settled',
+	},
+	{
+		id: '8',
+		categoryId: '3',
+		categoryName: '台球',
+		categoryIcon: '',
+		serviceName: '中式8球教学',
+		incomeAmount: 45.0,
+		orderTotal: 128.0,
+		completionTime: '2025-09-08 14:30:00',
+		status: 'unsettled',
+	},
+	{
+		id: '9',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '会所水疗+中式推拿',
+		incomeAmount: 60.58,
+		orderTotal: 268.0,
+		completionTime: '2025-08-05 11:22:18',
+		status: 'settled',
+	},
+	{
+		id: '10',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '经络调理',
+		incomeAmount: 156.0,
+		orderTotal: 320.0,
+		completionTime: '2025-06-18 19:00:00',
+		status: 'settled',
+	},
+	{
+		id: '11',
+		categoryId: '2',
+		categoryName: '爬山',
+		categoryIcon: '',
+		serviceName: '户外徒步向导',
+		incomeAmount: 66.0,
+		orderTotal: 150.0,
+		completionTime: '2025-04-22 06:30:00',
+		status: 'unsettled',
+	},
+	{
+		id: '12',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '会所水疗+中式推拿',
+		incomeAmount: 60.58,
+		orderTotal: 268.0,
+		completionTime: '2025-02-01 12:00:46',
+		status: 'settled',
+	},
+	{
+		id: '13',
+		categoryId: '3',
+		categoryName: '台球',
+		categoryIcon: '',
+		serviceName: '斯诺克陪练',
+		incomeAmount: 88.82,
+		orderTotal: 200.0,
+		completionTime: '2025-01-15 16:00:00',
+		status: 'settled',
+	},
+	{
+		id: '14',
+		categoryId: '1',
+		categoryName: '上门按摩',
+		categoryIcon: '',
+		serviceName: '深度理疗套餐',
+		incomeAmount: 210.6,
+		orderTotal: 498.0,
+		completionTime: '2025-07-01 10:00:00',
+		status: 'settled',
+	},
+]
+
+export function formatIncomeTotal(amount) {
+	const num = parseFloat(amount) || 0
+	if (Math.floor(num) >= 10000) {
+		const wan = Math.floor((num / 10000) * 100) / 100
+		return `${wan.toFixed(2)}万`
+	}
+	const fixed = num.toFixed(2)
+	const [intPart, decPart] = fixed.split('.')
+	const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+	return decPart ? `${withComma}.${decPart}` : withComma
+}
+
+export function formatDateYmd(date) {
+	const d = date instanceof Date ? date : new Date(date)
+	const y = d.getFullYear()
+	const m = String(d.getMonth() + 1).padStart(2, '0')
+	const day = String(d.getDate()).padStart(2, '0')
+	return `${y}.${m}.${day}`
+}
+
+export function getDefaultDateRange() {
+	const now = new Date()
+	const start = new Date(now.getFullYear(), now.getMonth(), 1)
+	return {
+		start: formatDateYmd(start),
+		end: formatDateYmd(now),
+		startTs: start.getTime(),
+		endTs: now.getTime(),
+	}
+}
+
+export function parseTimeStr(str) {
+	return new Date(str.replace(/-/g, '/')).getTime()
+}
+
+/** 获取演示用初始日期范围 */
+export function getMockDateRange() {
+	return { ...MOCK_DEFAULT_DATE_RANGE }
+}

+ 225 - 0
src/workbench/password/index.vue

@@ -0,0 +1,225 @@
+<template>
+	<view class="password-page">
+		<view class="form-list">
+			<view class="form-row">
+				<text class="row-label">手机号</text>
+				<text class="row-value">{{ phoneDisplay }}</text>
+			</view>
+
+			<view class="form-row">
+				<text class="row-label">验证码</text>
+				<input
+					class="row-input"
+					v-model="code"
+					type="number"
+					maxlength="6"
+					placeholder="请输入验证码"
+					placeholder-class="placeholder"
+				/>
+				<text
+					class="code-btn"
+					:class="{ disabled: !canSendCode }"
+					@click="sendCode"
+				>{{ codeBtnText }}</text>
+			</view>
+
+			<view class="form-row">
+				<text class="row-label">新密码</text>
+				<input
+					class="row-input"
+					v-model="newPassword"
+					:password="!showPassword"
+					maxlength="20"
+					placeholder="8-20位数字/字母组合"
+					placeholder-class="placeholder"
+				/>
+				<view class="eye-btn" @click="showPassword = !showPassword">
+					<u-icon :name="showPassword ? 'eye-fill' : 'eye-off'" size="40rpx" color="#999"></u-icon>
+				</view>
+			</view>
+		</view>
+
+		<view
+			class="submit-btn"
+			:class="{ active: canSubmit }"
+			@click="onSubmit"
+		>确定</view>
+	</view>
+</template>
+
+<script>
+import { getInfo } from '@/api/index'
+
+export default {
+	data() {
+		return {
+			phone: '18576065113',
+			code: '',
+			newPassword: '',
+			showPassword: false,
+			countdown: 0,
+			timer: null,
+			isSending: false,
+			isSubmitting: false,
+		}
+	},
+	computed: {
+		phoneDisplay() {
+			if (!this.phone) return '+86 '
+			const pure = String(this.phone).replace(/\D/g, '')
+			if (pure.length === 11) {
+				return `+86 ${pure.slice(0, 3)} ${pure.slice(3, 7)} ${pure.slice(7)}`
+			}
+			return `+86 ${this.phone}`
+		},
+		canSendCode() {
+			return this.phone && this.countdown === 0 && !this.isSending
+		},
+		codeBtnText() {
+			return this.countdown > 0 ? `${this.countdown}s后重发` : '发送验证码'
+		},
+		canSubmit() {
+			return (
+				this.phone &&
+				this.code.length === 6 &&
+				this.validatePassword(this.newPassword) &&
+				!this.isSubmitting
+			)
+		},
+	},
+	onShow() {
+		this.loadPhone()
+	},
+	beforeDestroy() {
+		this.clearTimer()
+	},
+	methods: {
+		loadPhone() {
+			const cached = uni.getStorageSync('wx_phone')
+			if (cached) {
+				this.phone = cached
+				return
+			}
+			getInfo().then(res => {
+				if (res.data.code == 200 && res.data.data) {
+					const phone = res.data.data.cphone || res.data.data.cPhone || ''
+					this.phone = phone
+					if (phone) uni.setStorageSync('wx_phone', phone)
+				}
+			})
+		},
+		validatePassword(val) {
+			return /^[A-Za-z0-9]{8,20}$/.test(val)
+		},
+		sendCode() {
+			if (!this.canSendCode) return
+			this.isSending = true
+			// 验证码接口待对接
+			uni.showToast({ title: '验证码已发送', icon: 'none' })
+			this.countdown = 60
+			this.timer = setInterval(() => {
+				this.countdown--
+				if (this.countdown <= 0) this.clearTimer()
+			}, 1000)
+			this.isSending = false
+		},
+		clearTimer() {
+			if (this.timer) {
+				clearInterval(this.timer)
+				this.timer = null
+			}
+			this.countdown = 0
+		},
+		onSubmit() {
+			if (!this.canSubmit) return
+			if (!this.validatePassword(this.newPassword)) {
+				uni.showToast({ title: '8-20位数字/字母组合', icon: 'none' })
+				return
+			}
+			this.isSubmitting = true
+			// 修改密码接口待对接
+			uni.showToast({ title: '修改密码功能待对接', icon: 'none' })
+			this.isSubmitting = false
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.password-page {
+	min-height: 100vh;
+	background: #fff;
+	padding: 0 32rpx;
+	box-sizing: border-box;
+}
+
+.form-list {
+	padding-top: 8rpx;
+}
+
+.form-row {
+	display: flex;
+	align-items: center;
+	min-height: 100rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.row-label {
+	width: 140rpx;
+	flex-shrink: 0;
+	font-size: 30rpx;
+	color: #333;
+}
+
+.row-value {
+	flex: 1;
+	font-size: 30rpx;
+	color: #333;
+}
+
+.row-input {
+	flex: 1;
+	font-size: 30rpx;
+	color: #333;
+	height: 100rpx;
+}
+
+.placeholder {
+	color: #ccc;
+	font-size: 30rpx;
+}
+
+.code-btn {
+	flex-shrink: 0;
+	font-size: 28rpx;
+	color: #0879ff;
+	padding-left: 16rpx;
+
+	&.disabled {
+		color: #999;
+	}
+}
+
+.eye-btn {
+	flex-shrink: 0;
+	padding: 8rpx 0 8rpx 16rpx;
+	display: flex;
+	align-items: center;
+}
+
+.submit-btn {
+	margin-top: 80rpx;
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	border-radius: 12rpx;
+	font-size: 32rpx;
+	color: #999;
+	background: #f0f0f0;
+
+	&.active {
+		color: #333;
+		background: #e8e8e8;
+	}
+}
+</style>

+ 226 - 0
src/workbench/rating/index.vue

@@ -0,0 +1,226 @@
+<template>
+	<view class="rating-page">
+		<view class="rating-list" v-if="reviewList.length">
+			<view class="rating-card" v-for="item in reviewList" :key="item.id">
+				<text class="service-title">{{ item.serviceName }}</text>
+
+				<view class="star-row">
+					<u-rate
+						:value="item.score"
+						:count="5"
+						readonly
+						size="18"
+						gutter="4"
+						activeColor="#ffca28"
+						inactiveColor="#e5e5e5"
+					></u-rate>
+					<text class="score-text">{{ formatScore(item.score) }}分</text>
+				</view>
+
+				<view class="content-wrap">
+					<text
+						class="review-content"
+						:class="{ collapsed: item.needExpand && !isExpanded(item.id), empty: !item.hasContent }"
+					>{{ item.displayContent }}</text>
+					<text
+						v-if="item.needExpand"
+						class="toggle"
+						@click="toggleExpand(item.id)"
+					>{{ isExpanded(item.id) ? '收起' : '展开' }}</text>
+				</view>
+
+				<view class="card-footer">
+					<view class="user-info">
+						<image
+							v-if="item.avatar"
+							class="avatar"
+							:src="item.avatar"
+							mode="aspectFill"
+						></image>
+						<view v-else class="avatar placeholder"></view>
+						<text class="nickname">{{ item.nickName }}</text>
+					</view>
+					<text class="review-time">{{ formatReviewTime(item.createTime) }}</text>
+				</view>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无评价</view>
+	</view>
+</template>
+
+<script>
+import { userComment } from '@/api/index'
+import {
+	MOCK_RATING_LIST,
+	normalizeRatingItem,
+	sortByTimeDesc,
+	formatReviewTime,
+	formatScore,
+} from './mock.js'
+
+export default {
+	data() {
+		return {
+			reviewList: [],
+			expandedIds: {},
+		}
+	},
+	onShow() {
+		this.loadList()
+	},
+	methods: {
+		formatReviewTime,
+		formatScore,
+		loadList() {
+			const openId = uni.getStorageSync('wx_copenid')
+			userComment({ openId })
+				.then(res => {
+					if (res.data.code == 200) {
+						const data = res.data.data
+						const raw = Array.isArray(data) ? data : data?.records || []
+						if (raw.length) {
+							this.reviewList = sortByTimeDesc(
+								raw.map((item, index) => normalizeRatingItem(item, index))
+							)
+							return
+						}
+					}
+					this.useMock()
+				})
+				.catch(() => this.useMock())
+		},
+		useMock() {
+			this.reviewList = sortByTimeDesc(
+				MOCK_RATING_LIST.map((item, index) => normalizeRatingItem(item, index))
+			)
+		},
+		isExpanded(id) {
+			return !!this.expandedIds[id]
+		},
+		toggleExpand(id) {
+			this.$set(this.expandedIds, id, !this.expandedIds[id])
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.rating-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding: 16rpx 24rpx 32rpx;
+	box-sizing: border-box;
+}
+
+.rating-list {
+	display: flex;
+	flex-direction: column;
+	gap: 16rpx;
+}
+
+.rating-card {
+	background: #fff;
+	border-radius: 12rpx;
+	padding: 28rpx 24rpx;
+}
+
+.service-title {
+	display: block;
+	font-size: 30rpx;
+	font-weight: 600;
+	color: #333;
+	line-height: 1.4;
+	margin-bottom: 16rpx;
+}
+
+.star-row {
+	display: flex;
+	align-items: center;
+	margin-bottom: 16rpx;
+}
+
+.score-text {
+	font-size: 26rpx;
+	color: #666;
+	margin-left: 12rpx;
+}
+
+.content-wrap {
+	margin-bottom: 20rpx;
+}
+
+.review-content {
+	display: block;
+	font-size: 28rpx;
+	color: #666;
+	line-height: 1.6;
+	word-break: break-all;
+
+	&.empty {
+		color: #999;
+	}
+
+	&.collapsed {
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 3;
+		overflow: hidden;
+	}
+}
+
+.toggle {
+	display: inline-block;
+	font-size: 26rpx;
+	color: #0879ff;
+	margin-top: 8rpx;
+}
+
+.card-footer {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding-top: 16rpx;
+	border-top: 1rpx solid #f5f5f5;
+}
+
+.user-info {
+	display: flex;
+	align-items: center;
+	flex: 1;
+	overflow: hidden;
+	margin-right: 16rpx;
+}
+
+.avatar {
+	width: 48rpx;
+	height: 48rpx;
+	border-radius: 50%;
+	flex-shrink: 0;
+	margin-right: 12rpx;
+
+	&.placeholder {
+		background: #eee;
+	}
+}
+
+.nickname {
+	font-size: 24rpx;
+	color: #999;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.review-time {
+	font-size: 24rpx;
+	color: #999;
+	flex-shrink: 0;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>

+ 102 - 0
src/workbench/rating/mock.js

@@ -0,0 +1,102 @@
+/** 评分列表假数据,接口就绪后替换 */
+
+export const EMPTY_REVIEW_TEXT = '该用户很懒,什么都没有留下'
+
+export const CONTENT_COLLAPSE_LEN = 72
+
+export const MOCK_RATING_LIST = [
+	{
+		id: 'r1',
+		serviceName: '斯诺克技术指导-个性化陪练',
+		score: 3.2,
+		content:
+			'教练很专业,讲解耐心,练了一下午进步明显。场地环境也不错,就是周末人有点多,建议提前预约时间段。',
+		avatar: '',
+		nickName: '用户***8',
+		createTime: '2025-06-03 12:25:18',
+	},
+	{
+		id: 'r2',
+		serviceName: '中式八球入门教学',
+		score: 5.0,
+		content: '非常满意,从零基础到能稳定进球,老师节奏把控很好,强烈推荐!',
+		avatar: '',
+		nickName: '台球爱好者',
+		createTime: '2025-06-01 18:40:00',
+	},
+	{
+		id: 'r3',
+		serviceName: '九球战术陪练',
+		score: 4.5,
+		content: '',
+		avatar: '',
+		nickName: '阿***明',
+		createTime: '2025-05-28 09:15:33',
+	},
+	{
+		id: 'r4',
+		serviceName: '斯诺克技术指导-个性化陪练',
+		score: 2.0,
+		content: '一般般吧,时间有点赶,希望下次能多留一点练习时间。',
+		avatar: '',
+		nickName: '路人甲',
+		createTime: '2025-05-20 14:02:11',
+	},
+	{
+		id: 'r5',
+		serviceName: '中式八球进阶课程',
+		score: 4.8,
+		content:
+			'技术细节讲得很到位,尤其是走位和防守思路,对我帮助很大。已经预约了下一节课,期待继续提升。',
+		avatar: '',
+		nickName: '老球友***2',
+		createTime: '2025-05-01 12:25:00',
+	},
+]
+
+export function parseTime(str) {
+	if (!str) return 0
+	return new Date(String(str).replace(/-/g, '/')).getTime()
+}
+
+export function formatReviewTime(timeStr) {
+	if (!timeStr) return ''
+	const d = new Date(String(timeStr).replace(/-/g, '/'))
+	const m = d.getMonth() + 1
+	const day = d.getDate()
+	const hh = String(d.getHours()).padStart(2, '0')
+	const mm = String(d.getMinutes()).padStart(2, '0')
+	return `${m}月${day}日 ${hh}:${mm}`
+}
+
+export function formatScore(score) {
+	const num = Number(score)
+	if (!Number.isFinite(num)) return '0.0'
+	return num.toFixed(1)
+}
+
+export function normalizeRatingItem(raw, index) {
+	const content = (raw.cContent || raw.content || raw.cText || raw.text || '').trim()
+	const score = Number(raw.nStar ?? raw.score ?? 0)
+	return {
+		id: raw.id || raw.cId || `api-${index}`,
+		serviceName:
+			raw.cServiceName ||
+			raw.serviceName ||
+			raw.cOrderTitle ||
+			raw.cProjectName ||
+			'服务项目',
+		score: Number.isFinite(score) ? score : 0,
+		content,
+		displayContent: content || EMPTY_REVIEW_TEXT,
+		hasContent: !!content,
+		avatar: raw.cAvatar || raw.avatar || raw.cHeadImg || '',
+		nickName: raw.cNickName || raw.cName || raw.userName || '用户',
+		createTime: raw.dtCreateTime || raw.dTime || raw.cTime || raw.completionTime || '',
+		needExpand: content.length > CONTENT_COLLAPSE_LEN,
+	}
+}
+
+export function sortByTimeDesc(list) {
+	return [...list].sort((a, b) => parseTime(b.createTime) - parseTime(a.createTime))
+}

+ 307 - 0
src/workbench/skill/add.vue

@@ -0,0 +1,307 @@
+<template>
+	<view class="add-page">
+		<!-- 分类 Tab -->
+		<scroll-view scroll-x class="category-tabs" :show-scrollbar="false">
+			<view
+				v-for="cat in categories"
+				:key="cat.id"
+				class="category-tab"
+				:class="{ active: activeCategory === cat.id }"
+				@click="activeCategory = cat.id"
+			>{{ cat.name }}</view>
+		</scroll-view>
+
+		<!-- 可选服务列表 -->
+		<view class="service-list" v-if="filteredList.length">
+			<view
+				class="service-item"
+				v-for="item in filteredList"
+				:key="item.id"
+				@click="toggleSelect(item.id)"
+			>
+				<view class="checkbox" :class="{ checked: selectedIds.includes(item.id) }">
+					<text v-if="selectedIds.includes(item.id)">✓</text>
+				</view>
+				<view class="cover"></view>
+				<view class="info">
+					<text class="title">{{ item.title }}</text>
+					<text class="price">¥{{ item.price }}/{{ item.unit || '次' }}</text>
+				</view>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无可开通服务</view>
+
+		<!-- 底部栏 -->
+		<view class="footer-bar">
+			<text class="selected-count">已选{{ selectedIds.length }}</text>
+			<view class="apply-btn" @click="onApply">申请开通</view>
+		</view>
+
+		<!-- 申请开通弹窗 -->
+		<view class="modal-mask" v-if="showApplyModal" @click="showApplyModal = false">
+			<view class="modal-box" @click.stop>
+				<view class="modal-title">申请开通</view>
+				<view class="apply-tip">已申请开通{{ selectedIds.length }}项服务</view>
+				<textarea
+					class="apply-reason"
+					v-model="applyReason"
+					placeholder="请输入开通原因"
+					maxlength="200"
+				/>
+				<view class="apply-btns">
+					<view class="btn ghost flex1" @click="showApplyModal = false">取消</view>
+					<view class="btn primary flex1" @click="submitApply">提交申请</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { SKILL_CATEGORIES, MOCK_AVAILABLE_SKILLS } from './mock.js'
+
+export default {
+	data() {
+		return {
+			categories: SKILL_CATEGORIES,
+			serviceList: [...MOCK_AVAILABLE_SKILLS],
+			activeCategory: '1',
+			selectedIds: [],
+			showApplyModal: false,
+			applyReason: '',
+		}
+	},
+	computed: {
+		filteredList() {
+			return this.serviceList.filter(item => item.categoryId === this.activeCategory)
+		},
+	},
+	methods: {
+		toggleSelect(id) {
+			const idx = this.selectedIds.indexOf(id)
+			if (idx > -1) {
+				this.selectedIds.splice(idx, 1)
+			} else {
+				this.selectedIds.push(id)
+			}
+		},
+		onApply() {
+			if (this.selectedIds.length === 0) {
+				uni.showToast({ title: '请至少选择1条数据', icon: 'none' })
+				return
+			}
+			this.applyReason = ''
+			this.showApplyModal = true
+		},
+		submitApply() {
+			if (!this.applyReason.trim()) {
+				uni.showToast({ title: '请输入开通原因', icon: 'none' })
+				return
+			}
+			this.showApplyModal = false
+			this.selectedIds = []
+			uni.showToast({ title: '已提交,待审核', icon: 'none' })
+			setTimeout(() => uni.navigateBack(), 1200)
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.add-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 140rpx;
+}
+
+.category-tabs {
+	white-space: nowrap;
+	background: #fff;
+	padding: 16rpx 24rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.category-tab {
+	display: inline-block;
+	padding: 16rpx 24rpx;
+	font-size: 28rpx;
+	color: #666;
+	margin-right: 8rpx;
+	border-bottom: 4rpx solid transparent;
+
+	&.active {
+		color: #333;
+		font-weight: 600;
+		border-bottom-color: #333;
+	}
+}
+
+.service-list {
+	padding: 24rpx;
+}
+
+.service-item {
+	display: flex;
+	align-items: center;
+	background: #fff;
+	border: 1rpx solid #eee;
+	border-radius: 12rpx;
+	padding: 24rpx;
+	margin-bottom: 16rpx;
+}
+
+.checkbox {
+	width: 40rpx;
+	height: 40rpx;
+	border: 2rpx solid #ccc;
+	border-radius: 50%;
+	margin-right: 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-shrink: 0;
+	font-size: 24rpx;
+	color: #fff;
+
+	&.checked {
+		background: #333;
+		border-color: #333;
+	}
+}
+
+.cover {
+	width: 100rpx;
+	height: 100rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	flex-shrink: 0;
+	margin-right: 20rpx;
+}
+
+.info {
+	flex: 1;
+	overflow: hidden;
+}
+
+.title {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	margin-bottom: 8rpx;
+}
+
+.price {
+	font-size: 26rpx;
+	color: #e54d42;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}
+
+.selected-count {
+	font-size: 28rpx;
+	color: #333;
+}
+
+.apply-btn {
+	padding: 20rpx 48rpx;
+	background: #333;
+	color: #fff;
+	font-size: 28rpx;
+	border-radius: 8rpx;
+}
+
+.modal-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 48rpx;
+	box-sizing: border-box;
+}
+
+.modal-box {
+	width: 100%;
+	max-width: 600rpx;
+	background: #fff;
+	border-radius: 16rpx;
+	padding-bottom: 32rpx;
+}
+
+.modal-title {
+	font-size: 32rpx;
+	font-weight: 600;
+	text-align: center;
+	padding: 32rpx 32rpx 16rpx;
+}
+
+.apply-tip {
+	text-align: center;
+	font-size: 26rpx;
+	color: #666;
+	padding-bottom: 16rpx;
+}
+
+.apply-reason {
+	display: block;
+	width: calc(100% - 64rpx);
+	margin: 0 32rpx 24rpx;
+	min-height: 200rpx;
+	padding: 20rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	font-size: 26rpx;
+	box-sizing: border-box;
+}
+
+.apply-btns {
+	display: flex;
+	gap: 16rpx;
+	padding: 0 32rpx;
+}
+
+.btn {
+	padding: 20rpx;
+	font-size: 28rpx;
+	border-radius: 8rpx;
+	text-align: center;
+
+	&.ghost {
+		border: 1rpx solid #ccc;
+		color: #666;
+	}
+
+	&.primary {
+		background: #333;
+		color: #fff;
+	}
+
+	&.flex1 {
+		flex: 1;
+	}
+}
+</style>

+ 233 - 0
src/workbench/skill/edit.vue

@@ -0,0 +1,233 @@
+<template>
+	<view class="edit-page">
+		<!-- 项目摘要 -->
+		<view class="summary">
+			<view class="cover"></view>
+			<view class="summary-info">
+				<text class="title">{{ detail.title }}</text>
+				<text class="billing">{{ detail.billingType }}</text>
+			</view>
+		</view>
+
+		<!-- 只读信息行 -->
+		<view class="info-section">
+			<view class="info-row">
+				<text class="label">服务分类</text>
+				<text class="value">{{ detail.categoryName }} &gt;</text>
+			</view>
+			<view class="info-row">
+				<text class="label">价格</text>
+				<text class="value">¥{{ detail.platformPrice }} &gt;</text>
+			</view>
+			<view class="info-row">
+				<text class="label">价格区间</text>
+				<text class="value">{{ priceRangeText }}</text>
+			</view>
+		</view>
+
+		<!-- 我的售价 -->
+		<view class="price-section">
+			<text class="section-label">我的售价</text>
+			<input
+				class="price-input"
+				v-model="myPrice"
+				type="digit"
+				placeholder="请输入"
+			/>
+			<text class="price-tip">
+				设置您的服务价格,请参照价格区间的最高价和最低价,不允许高于最高价或者低于最低价
+			</text>
+		</view>
+
+		<view class="footer-bar">
+			<view class="submit-btn" :class="{ active: canSubmit }" @click="onSubmit">提交</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { formatPriceRange } from './mock.js'
+
+export default {
+	data() {
+		return {
+			detail: {
+				id: '',
+				title: '',
+				categoryName: '',
+				billingType: '',
+				platformPrice: '',
+				priceRangeMin: '',
+				priceRangeMax: '',
+			},
+			myPrice: '',
+		}
+	},
+	computed: {
+		priceRangeText() {
+			const { priceRangeMin, priceRangeMax } = this.detail
+			if (priceRangeMin && priceRangeMax) {
+				return formatPriceRange(priceRangeMin, priceRangeMax)
+			}
+			return '-'
+		},
+		canSubmit() {
+			return this.myPrice !== '' && this.validatePrice()
+		},
+	},
+	onLoad(query) {
+		this.detail = {
+			id: query.id || '',
+			title: query.title || '中式推拿-培元疏通',
+			categoryName: query.categoryName || '上门按摩',
+			billingType: query.billingType || '按分钟计费',
+			platformPrice: query.platformPrice || '368.00',
+			priceRangeMin: Number(query.priceRangeMin) || 288,
+			priceRangeMax: Number(query.priceRangeMax) || 368,
+		}
+		this.myPrice = query.myPrice || ''
+	},
+	methods: {
+		validatePrice() {
+			const val = Number(this.myPrice)
+			if (isNaN(val)) return false
+			return val >= this.detail.priceRangeMin && val <= this.detail.priceRangeMax
+		},
+		onSubmit() {
+			if (!this.myPrice) {
+				uni.showToast({ title: '请输入售价', icon: 'none' })
+				return
+			}
+			if (!this.validatePrice()) {
+				uni.showToast({
+					title: `价格需在${this.detail.priceRangeMin}-${this.detail.priceRangeMax}之间`,
+					icon: 'none',
+				})
+				return
+			}
+			uni.showToast({ title: '修改完成', icon: 'none' })
+			setTimeout(() => uni.navigateBack(), 1000)
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.edit-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 140rpx;
+}
+
+.summary {
+	display: flex;
+	align-items: center;
+	background: #fff;
+	padding: 32rpx;
+	margin-bottom: 16rpx;
+}
+
+.cover {
+	width: 120rpx;
+	height: 120rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	flex-shrink: 0;
+}
+
+.summary-info {
+	margin-left: 24rpx;
+}
+
+.title {
+	display: block;
+	font-size: 30rpx;
+	color: #333;
+	font-weight: 500;
+	margin-bottom: 8rpx;
+}
+
+.billing {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.info-section {
+	background: #fff;
+	margin-bottom: 16rpx;
+}
+
+.info-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #eee;
+	font-size: 28rpx;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.label {
+	color: #333;
+}
+
+.value {
+	color: #666;
+}
+
+.price-section {
+	background: #fff;
+	padding: 32rpx;
+}
+
+.section-label {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	margin-bottom: 20rpx;
+}
+
+.price-input {
+	width: 100%;
+	height: 80rpx;
+	border-bottom: 1rpx solid #eee;
+	font-size: 30rpx;
+	color: #333;
+	margin-bottom: 20rpx;
+}
+
+.price-tip {
+	font-size: 24rpx;
+	color: #999;
+	line-height: 1.6;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	background: #fff;
+	border-top: 1rpx solid #eee;
+}
+
+.submit-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	border-radius: 12rpx;
+	font-size: 32rpx;
+	color: #999;
+	background: #f0f0f0;
+
+	&.active {
+		color: #fff;
+		background: #333;
+	}
+}
+</style>

+ 472 - 0
src/workbench/skill/index.vue

@@ -0,0 +1,472 @@
+<template>
+	<view class="skill-page">
+		<!-- 顶栏:开通新服务 -->
+		<view class="top-bar">
+			<text class="top-link" @click="goAdd">开通新服务</text>
+		</view>
+
+		<!-- 分类 Tab -->
+		<scroll-view scroll-x class="category-tabs" :show-scrollbar="false">
+			<view
+				v-for="cat in categories"
+				:key="cat.id"
+				class="category-tab"
+				:class="{ active: activeCategory === cat.id }"
+				@click="activeCategory = cat.id"
+			>{{ cat.name }}</view>
+		</scroll-view>
+
+		<!-- 状态筛选 -->
+		<view class="status-tabs">
+			<view
+				v-for="tab in statusTabs"
+				:key="tab.key"
+				class="status-tab"
+				:class="{ active: activeStatus === tab.key }"
+				@click="activeStatus = tab.key"
+			>{{ tab.label }}</view>
+		</view>
+
+		<!-- 技能列表 -->
+		<view class="skill-list" v-if="filteredList.length">
+			<view class="skill-card" v-for="item in filteredList" :key="item.id">
+				<view class="card-main">
+					<view class="cover">
+						<image v-if="item.cover" :src="item.cover" mode="aspectFill" />
+					</view>
+					<view class="info">
+						<view class="title-row">
+							<text class="title">{{ item.title }}</text>
+							<text class="sold" v-if="item.status === 'opened' && item.soldCount">已售{{ item.soldCount }}</text>
+						</view>
+						<text class="price">{{ formatPrice(item) }}</text>
+					</view>
+				</view>
+
+				<!-- 状态条 -->
+				<view
+					class="status-bar"
+					v-if="statusBarText(item)"
+					@click="onStatusBarClick(item)"
+				>
+					<text class="status-text">{{ statusBarText(item) }}</text>
+					<text class="chevron" v-if="item.status === 'rejected'">&gt;</text>
+				</view>
+
+				<!-- 操作按钮 -->
+				<view class="card-actions" v-if="item.status !== 'pending'">
+					<template v-if="item.status === 'opened'">
+						<view class="btn ghost" @click="onOffShelf(item)">申请下架</view>
+						<view class="btn primary" v-if="item.canEdit" @click="goEdit(item)">编辑价格</view>
+					</template>
+					<template v-if="item.status === 'rejected'">
+						<view class="btn ghost" @click="onDelete(item)">删除</view>
+						<view class="btn primary" @click="openApplyModal([item])">重新申请</view>
+					</template>
+				</view>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无数据</view>
+
+		<!-- 驳回详情弹窗 -->
+		<view class="modal-mask" v-if="showRejectModal" @click="showRejectModal = false">
+			<view class="modal-box" @click.stop>
+				<view class="modal-title">申请开通服务驳回</view>
+				<view class="modal-body">{{ rejectDetailText }}</view>
+				<view class="modal-footer" @click="showRejectModal = false">关闭</view>
+			</view>
+		</view>
+
+		<!-- 申请开通弹窗 -->
+		<view class="modal-mask" v-if="showApplyModal" @click="closeApplyModal">
+			<view class="modal-box apply-box" @click.stop>
+				<view class="modal-title">申请开通</view>
+				<view class="apply-tip">已申请开通{{ applyItems.length }}项服务</view>
+				<textarea
+					class="apply-reason"
+					v-model="applyReason"
+					placeholder="请输入开通原因"
+					maxlength="200"
+				/>
+				<view class="apply-btns">
+					<view class="btn ghost flex1" @click="closeApplyModal">取消</view>
+					<view class="btn primary flex1" @click="submitApply">提交申请</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import {
+	SKILL_CATEGORIES,
+	MOCK_MY_SKILLS,
+	STATUS_TABS,
+	formatPrice,
+} from './mock.js'
+
+export default {
+	data() {
+		return {
+			categories: SKILL_CATEGORIES,
+			statusTabs: STATUS_TABS,
+			skillList: [...MOCK_MY_SKILLS],
+			activeCategory: '1',
+			activeStatus: 'opened',
+			showRejectModal: false,
+			rejectDetailText: '',
+			showApplyModal: false,
+			applyItems: [],
+			applyReason: '',
+		}
+	},
+	computed: {
+		filteredList() {
+			return this.skillList.filter(item => {
+				return item.categoryId === this.activeCategory && item.status === this.activeStatus
+			})
+		},
+	},
+	methods: {
+		formatPrice,
+		statusBarText(item) {
+			if (item.status === 'opened' && item.applyDate) return item.applyDate
+			if (item.status === 'pending') return '申请开通服务审核中'
+			if (item.status === 'rejected') {
+				const reason = item.rejectReason || ''
+				return reason.length > 28 ? reason.slice(0, 28) + '...' : reason
+			}
+			return ''
+		},
+		onStatusBarClick(item) {
+			if (item.status === 'rejected') {
+				this.rejectDetailText = item.rejectReason
+				this.showRejectModal = true
+			}
+		},
+		goAdd() {
+			uni.navigateTo({ url: '/workbench/skill/add' })
+		},
+		goEdit(item) {
+			const str = uni.$u.queryParams({
+				id: item.id,
+				title: item.title,
+				categoryName: this.categories.find(c => c.id === item.categoryId)?.name || '',
+				billingType: item.billingType || '',
+				platformPrice: item.platformPrice,
+				priceRangeMin: item.priceRangeMin,
+				priceRangeMax: item.priceRangeMax,
+				myPrice: item.price,
+			})
+			uni.navigateTo({ url: `/workbench/skill/edit${str}` })
+		},
+		onOffShelf(item) {
+			uni.showToast({ title: '申请下架待对接', icon: 'none' })
+		},
+		onDelete(item) {
+			uni.showModal({
+				title: '提示',
+				content: '确定删除该服务?',
+				success: res => {
+					if (res.confirm) {
+						this.skillList = this.skillList.filter(s => s.id !== item.id)
+						uni.showToast({ title: '已删除', icon: 'none' })
+					}
+				},
+			})
+		},
+		openApplyModal(items) {
+			this.applyItems = items
+			this.applyReason = ''
+			this.showApplyModal = true
+		},
+		closeApplyModal() {
+			this.showApplyModal = false
+			this.applyItems = []
+			this.applyReason = ''
+		},
+		submitApply() {
+			if (!this.applyReason.trim()) {
+				uni.showToast({ title: '请输入开通原因', icon: 'none' })
+				return
+			}
+			this.applyItems.forEach(item => {
+				const idx = this.skillList.findIndex(s => s.id === item.id)
+				if (idx > -1) {
+					this.skillList[idx].status = 'pending'
+					this.skillList[idx].rejectReason = ''
+				}
+			})
+			this.closeApplyModal()
+			this.activeStatus = 'pending'
+			uni.showToast({ title: '已提交,待审核', icon: 'none' })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.skill-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 40rpx;
+}
+
+.top-bar {
+	display: flex;
+	justify-content: flex-end;
+	padding: 16rpx 32rpx 0;
+	background: #fff;
+}
+
+.top-link {
+	font-size: 28rpx;
+	color: #333;
+}
+
+.category-tabs {
+	white-space: nowrap;
+	background: #fff;
+	padding: 16rpx 24rpx 0;
+	border-bottom: 1rpx solid #eee;
+}
+
+.category-tab {
+	display: inline-block;
+	padding: 16rpx 24rpx;
+	font-size: 28rpx;
+	color: #666;
+	margin-right: 8rpx;
+	border-bottom: 4rpx solid transparent;
+
+	&.active {
+		color: #333;
+		font-weight: 600;
+		border-bottom-color: #333;
+	}
+}
+
+.status-tabs {
+	display: flex;
+	gap: 16rpx;
+	padding: 24rpx 32rpx;
+	background: #fff;
+	margin-bottom: 16rpx;
+}
+
+.status-tab {
+	padding: 12rpx 28rpx;
+	font-size: 26rpx;
+	color: #666;
+	border: 1rpx solid #ddd;
+	border-radius: 32rpx;
+	background: #fff;
+
+	&.active {
+		color: #333;
+		background: #f0f0f0;
+		border-color: #ccc;
+	}
+}
+
+.skill-list {
+	padding: 0 24rpx;
+}
+
+.skill-card {
+	background: #fff;
+	border: 1rpx solid #eee;
+	border-radius: 12rpx;
+	padding: 24rpx;
+	margin-bottom: 20rpx;
+}
+
+.card-main {
+	display: flex;
+}
+
+.cover {
+	width: 120rpx;
+	height: 120rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	flex-shrink: 0;
+	overflow: hidden;
+
+	image {
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.info {
+	flex: 1;
+	margin-left: 20rpx;
+	overflow: hidden;
+}
+
+.title-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: flex-start;
+}
+
+.title {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+	flex: 1;
+	line-height: 1.4;
+}
+
+.sold {
+	font-size: 22rpx;
+	color: #999;
+	flex-shrink: 0;
+	margin-left: 12rpx;
+}
+
+.price {
+	display: block;
+	font-size: 28rpx;
+	color: #e54d42;
+	margin-top: 12rpx;
+}
+
+.status-bar {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-top: 20rpx;
+	padding: 16rpx 20rpx;
+	background: #f5f5f5;
+	border-radius: 8rpx;
+}
+
+.status-text {
+	flex: 1;
+	font-size: 24rpx;
+	color: #666;
+	line-height: 1.5;
+}
+
+.chevron {
+	color: #999;
+	font-size: 24rpx;
+	margin-left: 8rpx;
+}
+
+.card-actions {
+	display: flex;
+	justify-content: flex-end;
+	gap: 16rpx;
+	margin-top: 20rpx;
+}
+
+.btn {
+	padding: 12rpx 32rpx;
+	font-size: 26rpx;
+	border-radius: 8rpx;
+	text-align: center;
+
+	&.ghost {
+		border: 1rpx solid #ccc;
+		color: #666;
+		background: #fff;
+	}
+
+	&.primary {
+		background: #333;
+		color: #fff;
+		border: 1rpx solid #333;
+	}
+
+	&.flex1 {
+		flex: 1;
+	}
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+
+.modal-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 48rpx;
+	box-sizing: border-box;
+}
+
+.modal-box {
+	width: 100%;
+	max-width: 600rpx;
+	background: #fff;
+	border-radius: 16rpx;
+	overflow: hidden;
+}
+
+.modal-title {
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #333;
+	text-align: center;
+	padding: 32rpx 32rpx 16rpx;
+}
+
+.modal-body {
+	padding: 16rpx 32rpx 32rpx;
+	font-size: 26rpx;
+	color: #666;
+	line-height: 1.7;
+	max-height: 400rpx;
+	overflow-y: auto;
+}
+
+.modal-footer {
+	text-align: center;
+	padding: 24rpx;
+	font-size: 30rpx;
+	color: #0879ff;
+	border-top: 1rpx solid #eee;
+}
+
+.apply-box {
+	padding-bottom: 0;
+}
+
+.apply-tip {
+	text-align: center;
+	font-size: 26rpx;
+	color: #666;
+	padding: 0 32rpx 16rpx;
+}
+
+.apply-reason {
+	display: block;
+	width: calc(100% - 64rpx);
+	margin: 0 32rpx 24rpx;
+	min-height: 200rpx;
+	padding: 20rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	font-size: 26rpx;
+	box-sizing: border-box;
+}
+
+.apply-btns {
+	display: flex;
+	gap: 16rpx;
+	padding: 0 32rpx 32rpx;
+}
+</style>

+ 124 - 0
src/workbench/skill/mock.js

@@ -0,0 +1,124 @@
+/** 技能模块假数据,接口就绪后替换 */
+
+export const SKILL_CATEGORIES = [
+	{ id: '1', name: '上门按摩' },
+	{ id: '2', name: '台球' },
+	{ id: '3', name: '爬山' },
+	{ id: '4', name: '电影' },
+	{ id: '5', name: '健身运动' },
+	{ id: '6', name: '读书学习' },
+]
+
+/** status: opened | pending | rejected */
+export const MOCK_MY_SKILLS = [
+	{
+		id: 's1',
+		categoryId: '1',
+		title: '中式推拿-培元疏通',
+		cover: '',
+		price: 200,
+		priceMin: null,
+		priceMax: null,
+		unit: '次',
+		soldCount: 200,
+		status: 'opened',
+		applyDate: '2025年12月1日申请开通服务',
+		rejectReason: '',
+		billingType: '按分钟计费',
+		platformPrice: 368,
+		priceRangeMin: 288,
+		priceRangeMax: 368,
+		canEdit: true,
+	},
+	{
+		id: 's2',
+		categoryId: '1',
+		title: '中式推拿-培元疏通',
+		cover: '',
+		price: 200,
+		priceMin: 200,
+		priceMax: 260,
+		unit: '次',
+		soldCount: 0,
+		status: 'rejected',
+		applyDate: '',
+		rejectReason: '申请上架审核驳回:当前时段该服务不支持申请开通,请稍后再试或联系客服了解详情。',
+		billingType: '按分钟计费',
+		platformPrice: 368,
+		priceRangeMin: 288,
+		priceRangeMax: 368,
+		canEdit: false,
+	},
+	{
+		id: 's3',
+		categoryId: '1',
+		title: '中式推拿-培元疏通',
+		cover: '',
+		price: 200,
+		priceMin: 200,
+		priceMax: 260,
+		unit: '次',
+		soldCount: 0,
+		status: 'pending',
+		applyDate: '',
+		rejectReason: '',
+		billingType: '按分钟计费',
+		platformPrice: 368,
+		priceRangeMin: 288,
+		priceRangeMax: 368,
+		canEdit: false,
+	},
+]
+
+/** 可开通的服务(未开通) */
+export const MOCK_AVAILABLE_SKILLS = [
+	{
+		id: 'a1',
+		categoryId: '1',
+		title: '中式推拿-培元疏通',
+		cover: '',
+		price: 200,
+		unit: '次',
+	},
+	{
+		id: 'a2',
+		categoryId: '1',
+		title: '泰式SPA-舒缓放松',
+		cover: '',
+		price: 268,
+		unit: '次',
+	},
+	{
+		id: 'a3',
+		categoryId: '2',
+		title: '中式8球教学',
+		cover: '',
+		price: 128,
+		unit: '小时',
+	},
+	{
+		id: 'a4',
+		categoryId: '3',
+		title: '户外徒步向导',
+		cover: '',
+		price: 150,
+		unit: '次',
+	},
+]
+
+export const STATUS_TABS = [
+	{ key: 'opened', label: '已开通' },
+	{ key: 'pending', label: '申请中' },
+	{ key: 'rejected', label: '申请驳回' },
+]
+
+export function formatPrice(item) {
+	if (item.priceMin != null && item.priceMax != null) {
+		return `¥${item.priceMin} 至 ${item.priceMax}/${item.unit || '次'}`
+	}
+	return `¥${item.price}/${item.unit || '次'}`
+}
+
+export function formatPriceRange(min, max) {
+	return `¥${min} 至 ${max}`
+}

+ 360 - 0
src/workbench/withdraw/apply.vue

@@ -0,0 +1,360 @@
+<template>
+	<view class="apply-page">
+		<view class="form-section">
+			<text class="section-label">到账银行卡</text>
+			<view class="bank-row" @click="isSelect = true">
+				<view class="bank-icon"></view>
+				<text class="bank-text" :class="{ placeholder: !form.bankAccount }">
+					{{ selectedBankText }}
+				</text>
+				<text class="chevron">&gt;</text>
+			</view>
+			<text class="tip">注:仅支持整数,72小时内到账</text>
+		</view>
+
+		<view class="form-section">
+			<text class="section-label">提现金额</text>
+			<view class="amount-row">
+				<text class="symbol">¥</text>
+				<input
+					type="number"
+					v-model="form.dPrice"
+					placeholder="0"
+					@input="onAmountInput"
+				/>
+			</view>
+			<view class="balance-tip" v-if="getAmount > 0">
+				可提现金额为 {{ getAmount }} 元,
+				<text class="link" @click="allBalance">全部提现</text>
+			</view>
+			<view class="balance-tip" v-else>暂无可提现余额</view>
+			<text class="fee-tip">手续费 {{ serviceFee }} 元/笔</text>
+		</view>
+
+		<view class="footer-bar">
+			<view class="submit-btn" @click="submit">申请提现</view>
+		</view>
+		<view class="record-link" @click="goRecord">提现记录</view>
+
+		<!-- 选卡弹窗 -->
+		<view class="popup-mask" v-if="isSelect" @click="isSelect = false">
+			<view class="popup-panel" @click.stop>
+				<view class="popup-header">
+					<text class="popup-title">选择提现银行卡</text>
+					<text class="popup-sub">请留意各银行到账时间</text>
+				</view>
+				<view
+					class="bank-option"
+					v-for="(item, index) in blankList"
+					:key="index"
+					@click="selectBlank(item)"
+				>
+					<view class="option-icon"></view>
+					<view class="option-info">
+						<text class="option-name">{{ item.bankName }}({{ tailNum(item.bankCardNum) }})</text>
+						<text class="option-desc">72小时到账</text>
+					</view>
+				</view>
+				<view class="bank-option add" @click="goAddBank">
+					<text>+ 添加银行卡提现</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { getInfo, withdraw, myBank } from '@/api/index'
+import { MOCK_BANK_CARDS, SERVICE_FEE } from './mock.js'
+
+export default {
+	data() {
+		return {
+			isSelect: false,
+			getAmount: '0.00',
+			blankList: [],
+			selectedBankText: '选择银行卡',
+			serviceFee: SERVICE_FEE,
+			form: {
+				cOpenId: '',
+				dPrice: '',
+				bankId: '',
+				bankAccount: '',
+				openingBank: '',
+			},
+		}
+	},
+	onShow() {
+		this.form.cOpenId = uni.getStorageSync('wx_copenid')
+		this.loadInfo()
+		this.loadBank()
+	},
+	methods: {
+		loadInfo() {
+			getInfo().then(res => {
+				if (res.data.code == 200 && res.data.data) {
+					this.getAmount = Number(res.data.data.getAmount || 0).toFixed(2)
+				} else {
+					this.getAmount = '1253.12'
+				}
+			}).catch(() => {
+				this.getAmount = '1253.12'
+			})
+		},
+		loadBank() {
+			myBank().then(res => {
+				if (res.data.code == 200 && res.data.data?.length) {
+					this.blankList = res.data.data
+				} else {
+					this.blankList = [...MOCK_BANK_CARDS]
+				}
+			}).catch(() => {
+				this.blankList = [...MOCK_BANK_CARDS]
+			})
+		},
+		tailNum(num) {
+			return num ? String(num).slice(-4) : '****'
+		},
+		selectBlank(item) {
+			this.selectedBankText = `${item.bankName}(**** ${this.tailNum(item.bankCardNum)})`
+			this.form.bankId = item.bankId
+			this.form.bankAccount = item.bankCardNum
+			this.form.openingBank = item.openingBank
+			this.isSelect = false
+		},
+		onAmountInput(e) {
+			const val = String(e.detail.value || '').replace(/\D/g, '')
+			this.form.dPrice = val
+		},
+		allBalance() {
+			this.form.dPrice = String(Math.floor(Number(this.getAmount)))
+		},
+		goAddBank() {
+			this.isSelect = false
+			uni.navigateTo({ url: '/workbench/bank/add' })
+		},
+		goRecord() {
+			uni.navigateTo({ url: '/workbench/withdraw/record' })
+		},
+		submit() {
+			if (!this.form.bankAccount) {
+				uni.showToast({ title: '请选择银行卡', icon: 'none' })
+				return
+			}
+			if (!this.form.dPrice || Number(this.form.dPrice) <= 0) {
+				uni.showToast({ title: '请输入提现金额', icon: 'none' })
+				return
+			}
+			if (Number(this.form.dPrice) > Number(this.getAmount)) {
+				uni.showToast({ title: '超出可提现金额', icon: 'none' })
+				return
+			}
+			withdraw(this.form).then(res => {
+				if (res.data.code == 200) {
+					uni.showToast({ title: '已提交审核', icon: 'none' })
+					setTimeout(() => uni.navigateBack(), 1000)
+				} else {
+					uni.showToast({ title: res.data.msg || '提交失败', icon: 'none' })
+				}
+			}).catch(() => {
+				uni.showToast({ title: '已提交审核(演示)', icon: 'none' })
+				setTimeout(() => uni.navigateBack(), 1000)
+			})
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.apply-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding-bottom: 160rpx;
+}
+
+.form-section {
+	background: #fff;
+	padding: 28rpx 32rpx;
+	margin-bottom: 16rpx;
+}
+
+.section-label {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+	margin-bottom: 20rpx;
+}
+
+.bank-row {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 0;
+}
+
+.bank-icon {
+	width: 48rpx;
+	height: 48rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	margin-right: 16rpx;
+	flex-shrink: 0;
+}
+
+.bank-text {
+	flex: 1;
+	font-size: 28rpx;
+	color: #333;
+
+	&.placeholder {
+		color: #ccc;
+	}
+}
+
+.chevron {
+	color: #999;
+	font-size: 28rpx;
+}
+
+.tip {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 12rpx;
+}
+
+.amount-row {
+	display: flex;
+	align-items: center;
+	border-bottom: 1rpx solid #eee;
+	padding: 16rpx 0;
+
+	.symbol {
+		font-size: 48rpx;
+		color: #333;
+		margin-right: 12rpx;
+	}
+
+	input {
+		flex: 1;
+		font-size: 56rpx;
+		color: #333;
+		font-weight: 600;
+	}
+}
+
+.balance-tip {
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 16rpx;
+
+	.link {
+		color: #0879ff;
+	}
+}
+
+.fee-tip {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 12rpx;
+}
+
+.footer-bar {
+	padding: 32rpx;
+}
+
+.submit-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	background: #333;
+	color: #fff;
+	font-size: 32rpx;
+	border-radius: 12rpx;
+}
+
+.record-link {
+	text-align: center;
+	font-size: 26rpx;
+	color: #0879ff;
+	padding-bottom: 32rpx;
+}
+
+.popup-mask {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 99;
+	display: flex;
+	align-items: flex-end;
+}
+
+.popup-panel {
+	width: 100%;
+	background: #fff;
+	border-radius: 24rpx 24rpx 0 0;
+	padding: 32rpx;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx);
+	max-height: 70vh;
+	overflow-y: auto;
+}
+
+.popup-header {
+	text-align: center;
+	margin-bottom: 24rpx;
+}
+
+.popup-title {
+	display: block;
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #333;
+}
+
+.popup-sub {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.bank-option {
+	display: flex;
+	align-items: center;
+	padding: 24rpx 0;
+	border-bottom: 1rpx solid #f5f5f5;
+
+	&.add {
+		justify-content: center;
+		border-bottom: none;
+		font-size: 28rpx;
+		color: #666;
+		padding-top: 32rpx;
+	}
+}
+
+.option-icon {
+	width: 48rpx;
+	height: 48rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	margin-right: 16rpx;
+}
+
+.option-name {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+}
+
+.option-desc {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 4rpx;
+}
+</style>

+ 290 - 0
src/workbench/withdraw/detail.vue

@@ -0,0 +1,290 @@
+<template>
+	<view class="detail-page" v-if="detail">
+		<view class="main-card">
+			<!-- 顶部:银行 & 金额 & 状态 -->
+			<view class="head-section">
+				<view class="bank-logo"></view>
+				<text class="bank-name">{{ detail.bankName }}</text>
+				<text class="amount">{{ formatAmount(detail.amount) }}</text>
+				<text class="status" :class="detail.status">{{ statusLabel }}</text>
+			</view>
+
+			<!-- 详情字段 -->
+			<view class="info-list">
+				<view class="info-row">
+					<text class="label">提现时间</text>
+					<text class="value">{{ formatDateTime(detail.createTime) }}</text>
+				</view>
+				<view class="info-row">
+					<text class="label">到账时间</text>
+					<text class="value">{{ detail.arriveTime ? formatDateTime(detail.arriveTime) : '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="label">提现银行</text>
+					<text class="value">{{ accountText }}</text>
+				</view>
+				<view class="info-row">
+					<text class="label">到账金额</text>
+					<text class="value">{{ formatAmount(arriveAmount) }}</text>
+				</view>
+				<view class="info-row">
+					<text class="label">手续费</text>
+					<text class="value">{{ formatAmount(serviceFee) }}</text>
+				</view>
+			</view>
+
+			<!-- 处理进度 -->
+			<view class="timeline-section">
+				<text class="section-title">处理进度</text>
+				<view class="timeline">
+					<view
+						class="timeline-item"
+						v-for="(step, index) in timeline"
+						:key="index"
+					>
+						<view class="dot-wrap">
+							<view class="dot" :class="step.state">
+								<text v-if="step.state === 'fail'">×</text>
+								<text v-else-if="step.state === 'done'">✓</text>
+							</view>
+							<view class="line" v-if="index < timeline.length - 1"></view>
+						</view>
+						<view class="step-content">
+							<text class="step-title" :class="step.state">{{ step.title }}</text>
+							<text class="step-time" v-if="step.time">{{ step.time }}</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+	<view class="empty" v-else>记录不存在</view>
+</template>
+
+<script>
+import {
+	getWithdrawById,
+	formatAmount,
+	formatDateTime,
+	maskAccount,
+	calcArriveAmount,
+	SERVICE_FEE,
+	STATUS_LABEL,
+	getTimeline,
+} from './mock.js'
+
+export default {
+	data() {
+		return {
+			detail: null,
+		}
+	},
+	computed: {
+		statusLabel() {
+			return STATUS_LABEL[this.detail?.status] || '-'
+		},
+		accountText() {
+			if (!this.detail) return '-'
+			return maskAccount(this.detail.bankName, this.detail.bankTail, this.detail.accountName)
+		},
+		serviceFee() {
+			return SERVICE_FEE
+		},
+		arriveAmount() {
+			if (!this.detail) return 0
+			return calcArriveAmount(this.detail.amount, SERVICE_FEE)
+		},
+		timeline() {
+			return this.detail ? getTimeline(this.detail) : []
+		},
+	},
+	onLoad(query) {
+		if (query.id) {
+			this.detail = getWithdrawById(query.id)
+		}
+	},
+	methods: {
+		formatAmount,
+		formatDateTime,
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.detail-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+	padding: 24rpx;
+	box-sizing: border-box;
+}
+
+.main-card {
+	background: #fff;
+	border-radius: 12rpx;
+	overflow: hidden;
+}
+
+.head-section {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 48rpx 32rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.bank-logo {
+	width: 80rpx;
+	height: 80rpx;
+	background: #eee;
+	border-radius: 50%;
+	margin-bottom: 16rpx;
+}
+
+.bank-name {
+	font-size: 28rpx;
+	color: #666;
+	margin-bottom: 16rpx;
+}
+
+.amount {
+	font-size: 56rpx;
+	color: #333;
+	font-weight: 700;
+	margin-bottom: 12rpx;
+}
+
+.status {
+	font-size: 26rpx;
+	color: #666;
+
+	&.success {
+		color: #333;
+	}
+
+	&.failed {
+		color: #e54d42;
+	}
+}
+
+.info-list {
+	padding: 8rpx 32rpx;
+}
+
+.info-row {
+	display: flex;
+	justify-content: space-between;
+	padding: 24rpx 0;
+	border-bottom: 1rpx solid #f5f5f5;
+	font-size: 28rpx;
+
+	&:last-child {
+		border-bottom: none;
+	}
+}
+
+.label {
+	color: #999;
+	flex-shrink: 0;
+	margin-right: 24rpx;
+}
+
+.value {
+	color: #333;
+	text-align: right;
+	flex: 1;
+}
+
+.timeline-section {
+	padding: 32rpx;
+	border-top: 16rpx solid #f5f5f5;
+}
+
+.section-title {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 600;
+	margin-bottom: 32rpx;
+}
+
+.timeline-item {
+	display: flex;
+	min-height: 80rpx;
+}
+
+.dot-wrap {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	width: 40rpx;
+	margin-right: 20rpx;
+	flex-shrink: 0;
+}
+
+.dot {
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 20rpx;
+	color: #fff;
+	background: #0879ff;
+
+	&.active {
+		background: #fff;
+		border: 2rpx solid #0879ff;
+		color: #0879ff;
+	}
+
+	&.fail {
+		background: #e54d42;
+	}
+
+	&.done {
+		background: #0879ff;
+	}
+}
+
+.line {
+	flex: 1;
+	width: 2rpx;
+	background: #ddd;
+	margin: 8rpx 0;
+	min-height: 40rpx;
+}
+
+.step-content {
+	flex: 1;
+	padding-bottom: 32rpx;
+}
+
+.step-title {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+
+	&.fail {
+		color: #e54d42;
+	}
+
+	&.active {
+		color: #0879ff;
+	}
+}
+
+.step-time {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>

+ 201 - 0
src/workbench/withdraw/mock.js

@@ -0,0 +1,201 @@
+/** 提现模块假数据与工具,接口就绪后替换 */
+
+export const SERVICE_FEE = 1.0
+
+export const MOCK_BANK_CARDS = [
+	{
+		bankId: '1',
+		bankName: '招商银行',
+		bankCardNum: '6225882251001234',
+		openingBank: '招商银行太原分行',
+	},
+	{
+		bankId: '2',
+		bankName: '交通银行',
+		bankCardNum: '6222608800123456',
+		openingBank: '交通银行太原支行',
+	},
+]
+
+/** status: pending | success | failed */
+export const MOCK_WITHDRAW_LIST = [
+	{
+		id: 'w1',
+		bankName: '招商银行',
+		bankTail: '2251',
+		accountName: '**玉',
+		amount: 1256.57,
+		createTime: '2025-12-14 12:02:45',
+		arriveTime: '',
+		status: 'pending',
+	},
+	{
+		id: 'w2',
+		bankName: '招商银行',
+		bankTail: '2251',
+		accountName: '**玉',
+		amount: 25.62,
+		createTime: '2025-12-13 09:15:30',
+		arriveTime: '',
+		status: 'pending',
+	},
+	{
+		id: 'w3',
+		bankName: '交通银行',
+		bankTail: '8890',
+		accountName: '**明',
+		amount: 962.25,
+		createTime: '2025-12-10 18:30:00',
+		arriveTime: '2025-12-11 10:22:00',
+		status: 'success',
+	},
+	{
+		id: 'w4',
+		bankName: '招商银行',
+		bankTail: '2251',
+		accountName: '**玉',
+		amount: 500.0,
+		createTime: '2025-11-28 16:45:33',
+		arriveTime: '',
+		status: 'failed',
+		rejectReason: '银行卡信息异常,请核对后重试',
+	},
+	{
+		id: 'w5',
+		bankName: '交通银行',
+		bankTail: '8890',
+		accountName: '**明',
+		amount: 368.88,
+		createTime: '2025-11-25 11:20:18',
+		arriveTime: '2025-11-26 08:00:00',
+		status: 'success',
+	},
+	{
+		id: 'w6',
+		bankName: '招商银行',
+		bankTail: '2251',
+		accountName: '**玉',
+		amount: 100.0,
+		createTime: '2025-11-20 14:00:00',
+		arriveTime: '',
+		status: 'pending',
+	},
+]
+
+export function getWithdrawById(id) {
+	return MOCK_WITHDRAW_LIST.find(item => item.id === id) || null
+}
+
+export function formatAmount(amount) {
+	const num = parseFloat(amount) || 0
+	const fixed = num.toFixed(2)
+	const [intPart, decPart] = fixed.split('.')
+	const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+	return decPart ? `${withComma}.${decPart}` : withComma
+}
+
+export function formatDateYmd(date) {
+	const d = date instanceof Date ? date : new Date(date)
+	const y = d.getFullYear()
+	const m = String(d.getMonth() + 1).padStart(2, '0')
+	const day = String(d.getDate()).padStart(2, '0')
+	return `${y}.${m}.${day}`
+}
+
+export function formatDateTime(timeStr) {
+	if (!timeStr) return '--'
+	return timeStr.slice(0, 19)
+}
+
+export function getMonthRange(year, month) {
+	const start = new Date(year, month - 1, 1)
+	const end = new Date(year, month, 0, 23, 59, 59)
+	return {
+		start: formatDateYmd(start),
+		end: formatDateYmd(end),
+		startTs: start.getTime(),
+		endTs: end.getTime(),
+	}
+}
+
+/** 演示默认:2025年11月 */
+export function getMockMonthRange() {
+	return getMonthRange(2025, 11)
+}
+
+export function parseTimeStr(str) {
+	return new Date(String(str).replace(/-/g, '/')).getTime()
+}
+
+const WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六']
+
+function pad(n) {
+	return String(n).padStart(2, '0')
+}
+
+function getWeekKey(date) {
+	const d = new Date(date.getFullYear(), date.getMonth(), date.getDate())
+	d.setDate(d.getDate() + 4 - (d.getDay() || 7))
+	const yearStart = new Date(d.getFullYear(), 0, 1)
+	return `${d.getFullYear()}-${Math.ceil(((d - yearStart) / 86400000 + 1) / 7)}`
+}
+
+/**
+ * 列表时间显示规则(与原型流程图一致)
+ */
+export function formatListTime(timeStr) {
+	if (!timeStr) return ''
+	const date = new Date(String(timeStr).replace(/-/g, '/'))
+	const now = new Date()
+	const hhmm = `${pad(date.getHours())}:${pad(date.getMinutes())}`
+
+	if (date.getFullYear() !== now.getFullYear()) {
+		return `${date.getFullYear()}.${pad(date.getMonth() + 1)}.${pad(date.getDate())}`
+	}
+
+	const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
+	const eventStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
+	const dayDiff = Math.floor((todayStart - eventStart) / 86400000)
+
+	if (dayDiff === 0) return hhmm
+	if (dayDiff === 1) return `昨天 ${hhmm}`
+	if (dayDiff === 2) return `前天 ${hhmm}`
+	if (getWeekKey(date) === getWeekKey(now)) {
+		return `周${WEEK_LABELS[date.getDay()]} ${hhmm}`
+	}
+	return `${pad(date.getMonth() + 1)}.${pad(date.getDate())}`
+}
+
+export function maskAccount(bankName, tail, accountName) {
+	return `${bankName} (${tail}) ${accountName || ''}`
+}
+
+export function calcArriveAmount(amount, fee = SERVICE_FEE) {
+	return Math.max(0, Number(amount) - Number(fee))
+}
+
+export const STATUS_LABEL = {
+	pending: '提现审核中',
+	success: '提现成功',
+	failed: '提现失败',
+}
+
+export function getTimeline(item) {
+	const submitTime = formatDateTime(item.createTime)
+	if (item.status === 'pending') {
+		return [
+			{ title: '提交提现申请', time: submitTime, state: 'done' },
+			{ title: '系统处理中', time: '', state: 'active' },
+		]
+	}
+	if (item.status === 'success') {
+		return [
+			{ title: '提交提现申请', time: submitTime, state: 'done' },
+			{ title: '提现成功', time: formatDateTime(item.arriveTime), state: 'done' },
+		]
+	}
+	return [
+		{ title: '提交提现申请', time: submitTime, state: 'done' },
+		{ title: '提现失败,请重试', time: formatDateTime(item.arriveTime) || submitTime, state: 'fail' },
+	]
+}

+ 188 - 0
src/workbench/withdraw/record.vue

@@ -0,0 +1,188 @@
+<template>
+	<view class="record-page">
+		<!-- 月份筛选 -->
+		<view class="filter-row" @click="showMonthPicker = true">
+			<text>{{ dateRangeText }}</text>
+			<text class="arrow">▼</text>
+		</view>
+
+		<view class="record-list" v-if="displayList.length">
+			<view
+				class="record-card"
+				v-for="item in displayList"
+				:key="item.id"
+				@click="goDetail(item)"
+			>
+				<view class="card-left">
+					<view class="bank-icon"></view>
+					<view class="card-info">
+						<text class="title">提现到银行卡</text>
+						<text class="bank">{{ item.bankName }} ({{ item.bankTail }})</text>
+					</view>
+				</view>
+				<view class="card-right">
+					<text class="amount">{{ formatAmount(item.amount) }}</text>
+					<text class="time">{{ formatListTime(item.createTime) }}</text>
+				</view>
+			</view>
+		</view>
+		<view class="empty" v-else>暂无提现记录</view>
+
+		<u-datetime-picker
+			:show="showMonthPicker"
+			v-model="monthPickerValue"
+			mode="year-month"
+			@confirm="onMonthConfirm"
+			@cancel="showMonthPicker = false"
+		></u-datetime-picker>
+	</view>
+</template>
+
+<script>
+import {
+	MOCK_WITHDRAW_LIST,
+	formatAmount,
+	formatListTime,
+	getMockMonthRange,
+	getMonthRange,
+	parseTimeStr,
+} from './mock.js'
+
+export default {
+	data() {
+		const range = getMockMonthRange()
+		return {
+			allList: [...MOCK_WITHDRAW_LIST],
+			dateStart: range.start,
+			dateEnd: range.end,
+			dateStartTs: range.startTs,
+			dateEndTs: range.endTs,
+			showMonthPicker: false,
+			monthPickerValue: new Date('2025/11/01').getTime(),
+		}
+	},
+	computed: {
+		dateRangeText() {
+			return `${this.dateStart} 至 ${this.dateEnd}`
+		},
+		displayList() {
+			return this.allList
+				.filter(item => {
+					const ts = parseTimeStr(item.createTime)
+					return ts >= this.dateStartTs && ts <= this.dateEndTs
+				})
+				.sort((a, b) => parseTimeStr(b.createTime) - parseTimeStr(a.createTime))
+		},
+	},
+	methods: {
+		formatAmount,
+		formatListTime,
+		onMonthConfirm(e) {
+			const d = new Date(e.value)
+			const range = getMonthRange(d.getFullYear(), d.getMonth() + 1)
+			this.dateStart = range.start
+			this.dateEnd = range.end
+			this.dateStartTs = range.startTs
+			this.dateEndTs = range.endTs
+			this.showMonthPicker = false
+		},
+		goDetail(item) {
+			uni.navigateTo({ url: `/workbench/withdraw/detail?id=${item.id}` })
+		},
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+.record-page {
+	min-height: 100vh;
+	background: #f5f5f5;
+}
+
+.filter-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 24rpx 32rpx;
+	background: #fff;
+	border-bottom: 1rpx solid #eee;
+	font-size: 26rpx;
+	color: #333;
+}
+
+.arrow {
+	font-size: 20rpx;
+	color: #999;
+	margin-left: 8rpx;
+}
+
+.record-list {
+	padding: 16rpx 0;
+}
+
+.record-card {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	background: #fff;
+	padding: 28rpx 32rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.card-left {
+	display: flex;
+	align-items: center;
+	flex: 1;
+	overflow: hidden;
+}
+
+.bank-icon {
+	width: 72rpx;
+	height: 72rpx;
+	background: #eee;
+	border-radius: 8rpx;
+	flex-shrink: 0;
+	margin-right: 20rpx;
+}
+
+.title {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.bank {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.card-right {
+	text-align: right;
+	flex-shrink: 0;
+	margin-left: 16rpx;
+}
+
+.amount {
+	display: block;
+	font-size: 32rpx;
+	color: #333;
+	font-weight: 600;
+}
+
+.time {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.empty {
+	text-align: center;
+	padding: 120rpx 0;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>