소스 검색

1.完成app端履约者入驻相关功能开发
2.完成app端履约者登录功能开发
3.完成履约者个人中心功能开发

steelwei 1 개월 전
부모
커밋
529a420373

+ 81 - 0
api/auth.js

@@ -0,0 +1,81 @@
+/**
+ * 认证相关 API
+ * @author steelwei
+ */
+import request from '@/utils/request'
+import { CLIENT_ID, TENANT_ID, PLATFORM_ID } from '@/utils/config'
+
+/**
+ * 密码登录
+ * @param {string} username - 手机号
+ * @param {string} password - 密码
+ */
+export function loginByPassword(username, password) {
+  return request({
+    url: '/auth/login',
+    method: 'POST',
+    needToken: false,
+    data: {
+      tenantId: TENANT_ID,
+      platformId: PLATFORM_ID,
+      username,
+      password,
+      clientId: CLIENT_ID,
+      grantType: 'password'
+    }
+  })
+}
+
+/**
+ * 短信验证码登录
+ * @param {string} phonenumber - 手机号
+ * @param {string} smsCode - 验证码
+ */
+export function loginBySms(phonenumber, smsCode) {
+  return request({
+    url: '/auth/login',
+    method: 'POST',
+    needToken: false,
+    data: {
+      tenantId: TENANT_ID,
+      platformId: PLATFORM_ID,
+      phonenumber,
+      smsCode,
+      clientId: CLIENT_ID,
+      grantType: 'sms'
+    }
+  })
+}
+
+/**
+ * 发送短信验证码
+ * @param {string} phonenumber - 手机号
+ */
+export function sendSmsCode(phonenumber) {
+  return request({
+    url: '/resource/sms/code',
+    method: 'GET',
+    needToken: false,
+    data: { phonenumber }
+  })
+}
+
+/**
+ * 退出登录
+ */
+export function logout() {
+  return request({
+    url: '/auth/logout',
+    method: 'POST'
+  })
+}
+
+/**
+ * 获取当前登录用户信息
+ */
+export function getUserInfo() {
+  return request({
+    url: '/system/user/getInfo',
+    method: 'GET'
+  })
+}

+ 224 - 0
api/fulfiller.js

@@ -0,0 +1,224 @@
+/**
+ * 履约者业务 API
+ * @author steelwei
+ */
+import request from '@/utils/request'
+import { BASE_URL, CLIENT_ID, PLATFORM_CODE } from '@/utils/config'
+
+/**
+ * 获取当前履约者个人档案
+ */
+export function getMyProfile() {
+  return request({
+    url: '/fulfiller/fulfiller/my',
+    method: 'GET'
+  })
+}
+
+/**
+ * 提交入驻申请(招募表单)
+ * @param {Object} data - 申请数据
+ */
+export function submitAudit(data) {
+  return request({
+    url: '/fulfiller/app/audit/submit',
+    method: 'POST',
+    needToken: false,
+    data
+  })
+}
+
+/**
+ * 查询子级区域/站点列表(级联选择器用)
+ * @param {number} parentId - 父级ID,0或不传查顶级
+ */
+export function getAreaChildren(parentId = 0) {
+  return request({
+    url: '/fulfiller/app/area/children',
+    method: 'GET',
+    needToken: false,
+    data: { parentId }
+  })
+}
+
+/**
+ * 上传文件(图片等)
+ * @param {string} filePath - 本地文件路径
+ * @returns {Promise} - { url, fileName, ossId }
+ */
+export function uploadFile(filePath) {
+  return new Promise((resolve, reject) => {
+    uni.uploadFile({
+      url: BASE_URL + '/fulfiller/app/upload',
+      filePath: filePath,
+      name: 'file',
+      header: {
+        'clientid': CLIENT_ID,
+        'X-Platform-Code': PLATFORM_CODE
+      },
+      success: (res) => {
+        try {
+          const data = JSON.parse(res.data)
+          if (data.code === 200) {
+            resolve(data)
+          } else {
+            uni.showToast({ title: data.msg || '上传失败', icon: 'none' })
+            reject(data)
+          }
+        } catch (e) {
+          reject(e)
+        }
+      },
+      fail: (err) => {
+        uni.showToast({ title: '上传失败', icon: 'none' })
+        reject(err)
+      }
+    })
+  })
+}
+
+/**
+ * 查询我的审核状态
+ */
+export function getMyAuditStatus() {
+  return request({
+    url: '/fulfiller/audit/my',
+    method: 'GET'
+  })
+}
+
+/**
+ * 获取我的积分日志
+ */
+export function getMyPointsLog(params) {
+  return request({
+    url: '/fulfiller/log/points',
+    method: 'GET',
+    data: params
+  })
+}
+
+/**
+ * 获取我的余额日志
+ */
+export function getMyBalanceLog(params) {
+  return request({
+    url: '/fulfiller/log/balance',
+    method: 'GET',
+    data: params
+  })
+}
+
+/**
+ * 获取我的奖惩记录
+ */
+export function getMyRewardLog(params) {
+  return request({
+    url: '/fulfiller/log/reward',
+    method: 'GET',
+    data: params
+  })
+}
+
+/**
+ * 修改头像
+ * @param {string} avatar - 头像URL
+ * @author steelwei
+ */
+export function updateAvatar(avatar) {
+  return request({
+    url: '/fulfiller/fulfiller/my/avatar',
+    method: 'PUT',
+    data: { avatar }
+  })
+}
+
+/**
+ * 修改真实姓名
+ * @param {string} name - 真实姓名
+ * @author steelwei
+ */
+export function updateName(name) {
+  return request({
+    url: '/fulfiller/fulfiller/my/name',
+    method: 'PUT',
+    data: { name }
+  })
+}
+
+/**
+ * 修改工作状态
+ * @param {string} status - 工作状态 (resting:休息, busy:接单中)
+ * @author steelwei
+ */
+export function updateStatus(status) {
+  return request({
+    url: '/fulfiller/fulfiller/my/status',
+    method: 'PUT',
+    data: { status }
+  })
+}
+
+/**
+ * 修改工作城市
+ * @param {string} cityCode - 城市编码
+ * @param {string} cityName - 城市名称
+ * @author steelwei
+ */
+export function updateCity(cityCode, cityName) {
+  return request({
+    url: '/fulfiller/fulfiller/my/city',
+    method: 'PUT',
+    data: { cityCode, cityName }
+  })
+}
+
+/**
+ * 获取认证信息
+ * @author steelwei
+ */
+export function getAuthInfo() {
+  return request({
+    url: '/fulfiller/fulfiller/my/auth',
+    method: 'GET'
+  })
+}
+
+/**
+ * 修改手机号
+ * @param {string} phone - 新手机号
+ * @param {string} code - 验证码
+ * @author steelwei
+ */
+export function updatePhone(phone, code) {
+  return request({
+    url: '/fulfiller/fulfiller/my/phone',
+    method: 'PUT',
+    data: { phone, code }
+  })
+}
+
+/**
+ * 修改密码
+ * @param {string} oldPassword - 旧密码
+ * @param {string} newPassword - 新密码
+ * @author steelwei
+ */
+export function updatePassword(oldPassword, newPassword) {
+  return request({
+    url: '/fulfiller/fulfiller/my/password',
+    method: 'PUT',
+    data: { oldPassword, newPassword }
+  })
+}
+
+/**
+ * 注销账号
+ * @author steelwei
+ */
+export function deleteAccount() {
+  return request({
+    url: '/fulfiller/fulfiller/my/account',
+    method: 'DELETE'
+  })
+}

+ 12 - 0
pages.json

@@ -161,6 +161,18 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/mine/settings/security/change-password",
+			"style": {
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/mine/settings/security/change-phone",
+			"style": {
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/mine/settings/notification/index",
 			"style": {

+ 59 - 18
pages/login/logic.js

@@ -1,3 +1,6 @@
+import { loginByPassword, loginBySms, sendSmsCode } from '@/api/auth'
+import { setToken } from '@/utils/auth'
+
 export default {
     data() {
         return {
@@ -11,7 +14,8 @@ export default {
             timer: null,
             showAgreementModal: false,
             agreementTitle: '',
-            agreementContent: ''
+            agreementContent: '',
+            loginLoading: false
         }
     },
     methods: {
@@ -26,25 +30,37 @@ export default {
 
             this.showAgreementModal = true;
         },
-        getVerifyCode() {
-            if (this.currentTab === 1) return; // 密码登录不需要验证码
+        async getVerifyCode() {
+            if (this.currentTab === 1) return;
             if (this.countDown > 0) return;
             if (!this.mobile || this.mobile.length !== 11) {
                 uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
                 return;
             }
 
-            this.countDown = 60;
-            this.timer = setInterval(() => {
-                this.countDown--;
-                if (this.countDown <= 0) {
-                    clearInterval(this.timer);
+            try {
+                const res = await sendSmsCode(this.mobile);
+                // 发送成功,启动倒计时
+                this.countDown = 60;
+                this.timer = setInterval(() => {
+                    this.countDown--;
+                    if (this.countDown <= 0) {
+                        clearInterval(this.timer);
+                    }
+                }, 1000);
+                // TODO 【生产环境必须删除】开发模式下后端会返回验证码,自动填入方便测试
+                const devCode = res.data;
+                if (devCode) {
+                    this.code = devCode;
+                    uni.showToast({ title: '验证码: ' + devCode, icon: 'none', duration: 3000 });
+                } else {
+                    uni.showToast({ title: '验证码已发送', icon: 'none' });
                 }
-            }, 1000);
-
-            uni.showToast({ title: '验证码已发送', icon: 'none' });
+            } catch (err) {
+                console.error('发送验证码失败:', err);
+            }
         },
-        handleLogin() {
+        async handleLogin() {
             if (!this.isAgreed) {
                 uni.showToast({ title: '请先同意用户协议', icon: 'none' });
                 return;
@@ -68,12 +84,37 @@ export default {
                 }
             }
 
-            uni.showToast({ title: '登录成功', icon: 'success' });
-            setTimeout(() => {
-                uni.switchTab({
-                    url: '/pages/home/index'
-                });
-            }, 1000);
+            if (this.loginLoading) return;
+            this.loginLoading = true;
+
+            try {
+                let res;
+                if (this.currentTab === 0) {
+                    // 短信验证码登录
+                    res = await loginBySms(this.mobile, this.code);
+                } else {
+                    // 密码登录
+                    res = await loginByPassword(this.mobile, this.password);
+                }
+
+                // 保存 Token
+                const token = res.data?.access_token || res.access_token;
+                if (token) {
+                    setToken(token);
+                }
+
+                uni.showToast({ title: '登录成功', icon: 'success' });
+                setTimeout(() => {
+                    uni.switchTab({
+                        url: '/pages/home/index'
+                    });
+                }, 1000);
+            } catch (err) {
+                // 错误已在 request.js 中统一处理
+                console.error('登录失败:', err);
+            } finally {
+                this.loginLoading = false;
+            }
         },
         goToRecruit() {
             uni.navigateTo({

+ 15 - 13
pages/mine/index.vue

@@ -10,24 +10,26 @@
         <view class="header-section">
             <view class="title-bar">个人中心</view>
             <view class="user-card" @click="navToProfile">
-                <image class="avatar" src="/static/touxiang.png" mode="aspectFill"></image>
+                <image class="avatar" :src="profile?.avatarUrl || '/static/touxiang.png'" mode="aspectFill"></image>
                 <view class="info-content">
                     <view class="name-row">
-                        <text class="name">张*哥</text>
+                        <text class="name">{{ profile?.name || '未登录' }}</text>
                         <view class="tags">
-                            <view class="tag green">接单中</view>
-                            <view class="tag blue">全职</view>
+                            <view class="tag green" v-if="profile?.status === '0'">接单中</view>
+                            <view class="tag green" v-else-if="profile?.status === '1'">休息中</view>
+                            <view class="tag" style="background:#eee;color:#999" v-else-if="profile?.status === '2'">已禁用</view>
+                            <view class="tag blue" v-if="profile?.workType === 'full_time'">全职</view>
                             <image class="bike-icon" src="/static/icons/motorbike.svg"></image>
                         </view>
                     </view>
                     <view class="detail-row">
                         <image class="small-icon" src="/static/icons/location.svg"></image>
-                        <text>深圳市龙华区民治街道第一站</text>
+                        <text>{{ profile?.stationName || profile?.cityName || '暂无站点' }}</text>
                         <image class="arrow-icon-small" src="/static/icons/chevron_right_dark.svg"></image>
                     </view>
                     <view class="detail-row">
                         <image class="small-icon" src="/static/icons/calendar.svg"></image>
-                        <text>已注册250天</text>
+                        <text>已注册{{ profile?.registerDays || 0 }}天</text>
                     </view>
                 </view>
                 <image class="settings-icon" src="/static/icons/settings.svg" @click.stop="navToSettings"></image>
@@ -38,8 +40,8 @@
                 <view class="vip-left">
                     <image class="vip-icon" src="/static/icons/crown.svg"></image>
                     <view class="vip-text">
-                        <text class="vip-title">L3 黄金履约者</text>
-                        <text class="vip-desc">再完成 298 单可升级为 L4 钻石履约者</text>
+                        <text class="vip-title">{{ profile?.levelName || '普通履约者' }}</text>
+                        <text class="vip-desc">{{ profile?.levelDesc || '完成更多订单即可升级' }}</text>
                     </view>
                 </view>
                 <view class="vip-btn" @click="navToLevel">
@@ -58,10 +60,10 @@
                     <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
                 </view>
                 <view class="stat-value">
-                    <text class="num">2,575</text>
+                    <text class="num">{{ profile?.balance || 0 }}</text>
                     <text class="unit">元</text>
                 </view>
-                <text class="sub-text">本月预计收入</text>
+                <text class="sub-text">账户余额</text>
             </view>
             <view class="divider"></view> 
             <view class="stat-item" @click="navToOrderStats">
@@ -71,10 +73,10 @@
                     <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
                 </view>
                 <view class="stat-value">
-                    <text class="num">702</text>
+                    <text class="num">{{ profile?.orderCount || 0 }}</text>
                     <text class="unit">单</text>
                 </view>
-                <text class="sub-text">本月完成单量</text>
+                <text class="sub-text">累计服务单量</text>
             </view>
             <view class="divider"></view>
             <view class="stat-item" @click="navToPoints">
@@ -84,7 +86,7 @@
                     <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
                 </view>
                 <view class="stat-value">
-                    <text class="num">1200</text>
+                    <text class="num">{{ profile?.points || 0 }}</text>
                     <text class="unit">分</text>
                 </view>
                 <text class="sub-text">可兑换权益</text>

+ 31 - 3
pages/mine/logic.js

@@ -1,11 +1,34 @@
+import { logout as logoutApi } from '@/api/auth'
+import { getMyProfile } from '@/api/fulfiller'
+import { clearAuth, isLoggedIn } from '@/utils/auth'
+
 export default {
     data() {
         return {
             showServicePopup: false,
-            showLogoutPopup: false
+            showLogoutPopup: false,
+            profile: null,
+            profileLoading: false
+        }
+    },
+    onShow() {
+        if (isLoggedIn()) {
+            this.loadProfile()
         }
     },
     methods: {
+        async loadProfile() {
+            if (this.profileLoading) return
+            this.profileLoading = true
+            try {
+                const res = await getMyProfile()
+                this.profile = res.data || null
+            } catch (err) {
+                console.error('获取个人信息失败:', err)
+            } finally {
+                this.profileLoading = false
+            }
+        },
         navToSettings() {
             uni.navigateTo({
                 url: '/pages/mine/settings/index'
@@ -75,9 +98,14 @@ export default {
         cancelLogout() {
             this.showLogoutPopup = false;
         },
-        confirmLogout() {
+        async confirmLogout() {
             this.showLogoutPopup = false;
-            // 跳转到登录页,使用 reLaunch 关闭所有页面
+            try {
+                await logoutApi()
+            } catch (e) {
+                // 即使后端退出失败,也清除本地登录态
+            }
+            clearAuth()
             uni.reLaunch({
                 url: '/pages/login/login'
             });

+ 68 - 28
pages/mine/settings/auth/index.vue

@@ -19,18 +19,26 @@
             </view>
             <view class="info-row">
                 <text class="label">真实姓名</text>
-                <text class="value">张三哥</text>
+                <text class="value">{{ authInfo.realName || '未设置' }}</text>
             </view>
             <view class="info-row">
                 <text class="label">证件号码</text>
-                <text class="value">4403**********1234</text>
+                <text class="value">{{ maskIdCard(authInfo.idCard) || '未设置' }}</text>
             </view>
             <view class="id-card-row">
-                <view class="id-card-box green-bg">
+                <view class="id-card-box green-bg" v-if="authInfo.idCardFront">
+                    <image class="id-card-img" :src="authInfo.idCardFront" mode="aspectFill"></image>
+                    <view class="corner-tag">人像面</view>
+                </view>
+                <view class="id-card-box green-bg" v-else>
                     <text class="id-text">ID Front</text>
                     <view class="corner-tag">人像面</view>
                 </view>
-                <view class="id-card-box green-bg">
+                <view class="id-card-box green-bg" v-if="authInfo.idCardBack">
+                    <image class="id-card-img" :src="authInfo.idCardBack" mode="aspectFill"></image>
+                    <view class="corner-tag">国徽面</view>
+                </view>
+                <view class="id-card-box green-bg" v-else>
                     <text class="id-text">ID Back</text>
                     <view class="corner-tag">国徽面</view>
                 </view>
@@ -44,8 +52,8 @@
                 <text class="section-title">服务类型</text>
             </view>
             <view class="tags-row">
-                <view class="service-tag">宠物接送</view>
-                <view class="service-tag">上门喂遛</view>
+                <view class="service-tag" v-for="(type, index) in authInfo.serviceTypes" :key="index">{{ type }}</view>
+                <text v-if="authInfo.serviceTypes.length === 0" class="empty-text">暂无服务类型</text>
             </view>
         </view>
 
@@ -56,28 +64,12 @@
                 <text class="section-title">资质证书</text>
             </view>
             
-            <text class="sub-title">宠物接送服务资质</text>
-            <view class="cert-row">
-                <view class="cert-box yellow-bg">
-                    <text class="cert-text orange">Cert 1</text>
-                </view>
-                <view class="cert-box yellow-bg">
-                    <text class="cert-text orange">Cert 2</text>
-                </view>
-            </view>
-
-            <text class="sub-title">上门喂遛服务资质</text>
-            <view class="cert-row">
-                <view class="cert-box blue-bg">
-                    <text class="cert-text blue">Cert 3</text>
-                </view>
-            </view>
-
-            <text class="sub-title">上门洗护服务资质</text>
+            <text class="sub-title">{{ authInfo.authQual ? '已认证' : '未认证' }}</text>
             <view class="cert-row">
-                <view class="cert-box green-light-bg">
-                    <text class="cert-text green">Cert 4</text>
+                <view class="cert-box yellow-bg" v-for="(img, index) in authInfo.qualImages" :key="index">
+                    <image class="cert-img" :src="img" mode="aspectFill"></image>
                 </view>
+                <text v-if="authInfo.qualImages.length === 0" class="empty-text">暂无资质证书</text>
             </view>
         </view>
 
@@ -90,16 +82,54 @@
 </template>
 
 <script>
+import { getAuthInfo } from '@/api/fulfiller'
+
 export default {
     data() {
-        return {}
+        return {
+            authInfo: {
+                realName: '',
+                idCard: '',
+                idCardFront: '',
+                idCardBack: '',
+                serviceTypes: [],
+                authQual: '',
+                qualImages: []
+            }
+        }
+    },
+    onLoad() {
+        this.loadAuthInfo()
     },
     methods: {
         navBack() {
-             uni.navigateBack({
+            uni.navigateBack({
                 delta: 1
             });
         },
+        async loadAuthInfo() {
+            try {
+                const res = await getAuthInfo()
+                if (res.code === 200 && res.data) {
+                    this.authInfo = {
+                        realName: res.data.realName || '',
+                        idCard: res.data.idCard || '',
+                        idCardFront: res.data.idCardFrontUrl || '',
+                        idCardBack: res.data.idCardBackUrl || '',
+                        serviceTypes: res.data.serviceTypeList || [],
+                        authQual: res.data.authQual,
+                        qualImages: res.data.qualImageUrls || []
+                    }
+                }
+            } catch (e) {
+                console.error('加载认证信息失败', e)
+                uni.showToast({ title: '加载失败', icon: 'none' })
+            }
+        },
+        maskIdCard(idCard) {
+            if (!idCard || idCard.length < 8) return idCard
+            return idCard.substring(0, 4) + '**********' + idCard.substring(idCard.length - 4)
+        },
         editAuth() {
             uni.showToast({ title: '跳转修改认证页', icon: 'none' });
         }
@@ -274,6 +304,16 @@ page {
 .cert-text.blue { color: #2196F3; }
 .cert-text.green { color: #8BC34A; }
 
+.id-card-img, .cert-img {
+    width: 100%;
+    height: 100%;
+    border-radius: 12rpx;
+}
+.empty-text {
+    font-size: 26rpx;
+    color: #999;
+}
+
 .bottom-btn-area {
     margin-top: 40rpx;
     text-align: center;

+ 112 - 55
pages/mine/settings/profile/edit-name.vue

@@ -5,52 +5,97 @@
             <view class="header-left" @click="navBack">
                 <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);"></image>
             </view>
-            <text class="header-title">修改真实姓名</text>
-            <view class="header-right" @click="saveName">
-                <text class="save-btn">保存</text>
-            </view>
+            <text class="header-title">修改姓名</text>
+            <view class="header-right"></view>
         </view>
         <view class="header-placeholder"></view>
 
-        <view class="input-card">
-            <input class="name-input" type="text" v-model="name" placeholder="请输入真实姓名" />
-            <view class="clear-icon" v-if="name.length > 0" @click="clearName">
-                <text class="clear-text">×</text>
+        <view class="form-card">
+            <view class="form-item no-border">
+                <text class="form-label">真实姓名</text>
+                <input 
+                    class="form-input" 
+                    type="text" 
+                    v-model="name" 
+                    placeholder="请输入真实姓名"
+                    placeholder-class="placeholder"
+                    maxlength="20"
+                />
             </view>
         </view>
-        <text class="input-tip">请确保填写真实有效的信息,方便管理员审核。</text>
+
+        <view class="btn-area">
+            <button class="submit-btn" @click="submitChange">确认修改</button>
+        </view>
+
+        <view class="tips">
+            <text class="tips-text">• 请输入您的真实姓名</text>
+            <text class="tips-text">• 姓名将用于实名认证和订单服务</text>
+        </view>
     </view>
 </template>
 
 <script>
+// 引入 API @author steelwei
+import { updateName } from '@/api/fulfiller'
+
 export default {
     data() {
         return {
-            name: '张*哥'
+            name: ''
         }
     },
-    onLoad(option) {
-        if(option.name) {
-            this.name = option.name;
+    onLoad(options) {
+        if (options.name) {
+            this.name = decodeURIComponent(options.name);
         }
     },
     methods: {
         navBack() {
-             uni.navigateBack({
-                delta: 1
-            });
-        },
-        clearName() {
-            this.name = '';
+            uni.navigateBack({ delta: 1 });
         },
-        saveName() {
-            if(!this.name.trim()) {
-                uni.showToast({ title: '姓名不能为空', icon: 'none' });
+        
+        // 提交修改 @author steelwei
+        async submitChange() {
+            // 验证输入
+            if (!this.name || !this.name.trim()) {
+                uni.showToast({ title: '请输入姓名', icon: 'none' });
+                return;
+            }
+            if (this.name.trim().length < 2) {
+                uni.showToast({ title: '姓名至少2个字符', icon: 'none' });
                 return;
             }
-            // 模拟保存并通知上一个页面更新
-            uni.$emit('updateName', this.name);
-            uni.navigateBack();
+            
+            uni.showLoading({ title: '提交中...' });
+            try {
+                const res = await updateName(this.name.trim());
+                
+                if (res.code === 200) {
+                    uni.showToast({ 
+                        title: '修改成功', 
+                        icon: 'success',
+                        duration: 2000
+                    });
+                    
+                    // 通知上一页更新姓名
+                    uni.$emit('updateName', this.name.trim());
+                    
+                    setTimeout(() => {
+                        uni.navigateBack({ delta: 1 });
+                    }, 2000);
+                } else {
+                    uni.showToast({ 
+                        title: res.msg || '修改失败', 
+                        icon: 'none' 
+                    });
+                }
+            } catch (error) {
+                console.error('修改姓名失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
         }
     }
 }
@@ -59,7 +104,6 @@ export default {
 <style>
 page {
     background-color: #F8F8F8;
-    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
 }
 .custom-header {
     position: fixed;
@@ -74,7 +118,7 @@ page {
     justify-content: space-between;
     padding-left: 30rpx;
     padding-right: 30rpx;
-    box-sizing: border-box;
+    box-sizing: content-box;
     z-index: 100;
 }
 .header-placeholder {
@@ -85,53 +129,66 @@ page {
     height: 40rpx;
 }
 .header-title {
-    font-size: 32rpx; /* 16pt based on design req */
+    font-size: 28rpx;
     font-weight: bold;
     color: #333;
 }
 .header-right {
-    /* width: 40rpx; removed fixed width to fit text */
-}
-.save-btn {
-    font-size: 28rpx;
-    color: #FF5722;
-    font-weight: bold;
+    width: 40rpx;
 }
 
 .container {
-    padding: 30rpx;
+    padding: 20rpx 30rpx;
 }
-.input-card {
+.form-card {
     background-color: #fff;
-    border-radius: 12rpx;
-    padding: 20rpx 30rpx;
+    border-radius: 20rpx;
+    padding: 0 30rpx;
+    margin-bottom: 30rpx;
+}
+.form-item {
     display: flex;
     align-items: center;
-    margin-bottom: 20rpx;
+    padding: 30rpx 0;
+    border-bottom: 1px solid #F5F5F5;
 }
-.name-input {
+.form-item.no-border {
+    border-bottom: none;
+}
+.form-label {
+    width: 160rpx;
+    font-size: 28rpx;
+    color: #333;
+}
+.form-input {
     flex: 1;
-    font-size: 32rpx;
+    font-size: 28rpx;
     color: #333;
-    height: 60rpx;
 }
-.clear-icon {
-    width: 36rpx;
-    height: 36rpx;
-    background-color: #CCCCCC;
-    border-radius: 50%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+.placeholder {
+    color: #999;
 }
-.clear-text {
+
+.btn-area {
+    padding: 40rpx 0;
+}
+.submit-btn {
+    background-color: #FF5722;
     color: #fff;
-    font-size: 28rpx;
-    line-height: 28rpx;
-    margin-top: -2rpx; /* Visual adjustment */
+    font-size: 32rpx;
+    border-radius: 44rpx;
+    height: 88rpx;
+    line-height: 88rpx;
+    border: none;
+}
+
+.tips {
+    padding: 20rpx 30rpx;
 }
-.input-tip {
+.tips-text {
+    display: block;
     font-size: 24rpx;
     color: #999;
+    line-height: 40rpx;
 }
 </style>

+ 251 - 146
pages/mine/settings/profile/index.vue

@@ -52,7 +52,7 @@
             <view class="list-item no-border">
                 <text class="item-title">所属站点</text>
                 <view class="item-right">
-                    <text class="item-value">深圳市龙华区民治街道第一站</text>
+                    <text class="item-value">{{ userInfo.stationName || '未分配站点' }}</text>
                 </view>
             </view>
         </view>
@@ -67,7 +67,7 @@
             </view>
         </view>
 
-        <!-- 城市选择弹窗 (模拟) -->
+        <!-- 城市选择弹窗 (级联版,与我要加入页面一致) -->
         <view class="popup-mask" v-if="isCityPickerShow" @click="closeCityPicker">
             <view class="popup-content" @click.stop>
                 <view class="popup-header-row">
@@ -75,38 +75,41 @@
                     <text class="popup-title-text">请选择工作城市</text>
                     <text class="popup-btn-confirm" @click="confirmCity">确定</text>
                 </view>
-                <view class="city-tabs">
-                    <view 
-                        class="city-tab-item" 
-                        :class="{ 'active': cityStep > 0, 'active-red': cityStep === 0 }"
-                        @click="changeCityStep(0)">
-                        {{ selectedProvince || '请选择' }}
-                    </view>
-                    <view 
-                        class="city-tab-item" 
-                        :class="{ 'active': cityStep > 1, 'active-red': cityStep === 1 }"
-                        v-if="selectedProvince"
-                        @click="changeCityStep(1)">
-                        {{ selectedCity || '请选择' }}
-                    </view>
-                     <view 
-                        class="city-tab-item" 
-                        :class="{ 'active-red': cityStep === 2 }"
-                        v-if="selectedCity"
-                        @click="changeCityStep(2)">
-                        {{ selectedDistrict || '请选择' }}
+                <view class="picker-body">
+                    <!-- 左侧:垂直路径 -->
+                    <view class="timeline-area">
+                        <view 
+                            class="timeline-item" 
+                            v-for="(item, index) in selectedPathway" 
+                            :key="index"
+                            @click="jumpToStep(index)"
+                        >
+                            <view class="timeline-dot"></view>
+                            <text>{{ item.name }}</text>
+                        </view>
+                        <view 
+                            class="timeline-item active" 
+                            v-if="selectStep === selectedPathway.length"
+                        >
+                            <view class="timeline-dot"></view>
+                            <text>请选择</text>
+                        </view>
                     </view>
+                    <!-- 右侧:待选项列表 -->
+                    <scroll-view scroll-y class="list-area">
+                        <view 
+                            class="list-item" 
+                            v-for="item in currentCityList" 
+                            :key="item.id"
+                            @click="selectCityItem(item)"
+                        >
+                            {{ item.name }}
+                        </view>
+                        <view v-if="currentCityList.length === 0" style="padding:20rpx;color:#999">
+                            无数据
+                        </view>
+                    </scroll-view>
                 </view>
-                <scroll-view scroll-y class="city-list">
-                    <view 
-                        class="city-item" 
-                        v-for="(item, index) in currentList" 
-                        :key="index"
-                        @click="selectItem(item)">
-                        {{ item }}
-                        <text v-if="isSelected(item)" style="float: right; color: #FF5722;">✓</text>
-                    </view>
-                </scroll-view>
             </view>
         </view>
 
@@ -114,55 +117,32 @@
 </template>
 
 <script>
+// 引入 API @author steelwei
+import { getMyProfile, updateAvatar, updateName, updateStatus, updateCity, uploadFile, getAreaChildren } from '@/api/fulfiller'
+
 export default {
     data() {
         return {
             userInfo: {
-                name: '张*哥',
-                workType: '全职',
-                workStatus: '接单中',
-                city: '广东省 深圳市 龙华区',
-                avatar: '/static/touxiang.png'
+                name: '',
+                workType: '',
+                workStatus: '',
+                city: '',
+                avatar: '/static/touxiang.png',
+                stationName: ''
             },
             isStatusPickerShow: false,
             isCityPickerShow: false,
             
-            // 城市选择相关
-            cityStep: 0, // 0: 省, 1: 市, 2: 区
-            selectedProvince: '',
-            selectedCity: '',
-            selectedDistrict: '',
-            
-            // 模拟数据
-            provinces: ['广东省', '湖南省', '江西省'],
-            cities: {
-                '广东省': ['深圳市', '广州市', '东莞市'],
-                '湖南省': ['长沙市', '株洲市'],
-                '江西省': ['南昌市', '九江市']
-            },
-            districts: {
-                '深圳市': ['龙华区', '南山区', '福田区', '宝安区'],
-                '广州市': ['天河区', '越秀区', '海珠区'],
-                '东莞市': ['南城区', '东城区'],
-                '长沙市': ['岳麓区', '芙蓉区'],
-                '南昌市': ['红谷滩区', '东湖区']
-            }
-        }
-    },
-    computed: {
-        currentList() {
-            if (this.cityStep === 0) {
-                return this.provinces;
-            } else if (this.cityStep === 1) {
-                return this.cities[this.selectedProvince] || [];
-            } else if (this.cityStep === 2) {
-                return this.districts[this.selectedCity] || [];
-            }
-            return [];
+            // 城市级联选择器(与我要加入页面一致)
+            selectStep: 0,
+            selectedPathway: [],
+            currentCityList: [],
+            selectedCityId: null
         }
     },
     onLoad() {
-        // 监听姓名修改
+        this.loadUserInfo();
         uni.$on('updateName', (newName) => {
             this.userInfo.name = newName;
         });
@@ -171,87 +151,200 @@ export default {
         uni.$off('updateName');
     },
     methods: {
+        // 加载用户信息 @author steelwei
+        async loadUserInfo() {
+            uni.showLoading({ title: '加载中...' });
+            try {
+                const res = await getMyProfile();
+                if (res.code === 200) {
+                    const data = res.data;
+                    this.userInfo = {
+                        name: data.realName || data.name,
+                        workType: data.workType === 'full_time' ? '全职' : '兼职',
+                        workStatus: this.formatStatus(data.status),
+                        city: data.cityName || '',
+                        avatar: data.avatarUrl || '/static/touxiang.png',
+                        stationName: data.stationName || '未分配站点'
+                    };
+                } else {
+                    uni.showToast({ title: res.msg || '加载失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('加载用户信息失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        },
+        
+        // 格式化状态 @author steelwei
+        formatStatus(status) {
+            const statusMap = {
+                'busy': '接单中',
+                'resting': '休息中',
+                'disabled': '已禁用'
+            };
+            return statusMap[status] || status;
+        },
+        
         navBack() {
-             uni.navigateBack({
-                delta: 1
-            });
+            uni.navigateBack({ delta: 1 });
         },
+        
+        // 修改头像 @author steelwei
         changeAvatar() {
             uni.chooseImage({
                 count: 1,
-                success: (res) => {
-                    console.log(res.tempFilePaths);
-                    this.userInfo.avatar = res.tempFilePaths[0];
+                success: async (res) => {
+                    const tempFilePath = res.tempFilePaths[0];
+                    
+                    // 上传图片到服务器
+                    uni.showLoading({ title: '上传中...' });
+                    try {
+                        const uploadRes = await uploadFile(tempFilePath);
+                        if (uploadRes.code === 200) {
+                            const avatarUrl = uploadRes.data.url;
+                            
+                            // 调用接口更新头像
+                            const result = await updateAvatar(avatarUrl);
+                            if (result.code === 200) {
+                                this.userInfo.avatar = avatarUrl;
+                                uni.showToast({ title: '修改成功', icon: 'success' });
+                            } else {
+                                uni.showToast({ title: result.msg || '修改失败', icon: 'none' });
+                            }
+                        }
+                    } catch (error) {
+                        console.error('修改头像失败:', error);
+                        uni.showToast({ title: '上传失败', icon: 'none' });
+                    } finally {
+                        uni.hideLoading();
+                    }
                 }
             });
         },
+        
         editName() {
             uni.navigateTo({
                 url: `/pages/mine/settings/profile/edit-name?name=${this.userInfo.name}`
             });
         },
-        toggleWorkType() {
-            // 简单模拟切换
-            this.userInfo.workType = this.userInfo.workType === '全职' ? '兼职' : '全职';
-        },
+        
         showStatusPicker() {
             this.isStatusPickerShow = true;
         },
+        
         closeStatusPicker() {
             this.isStatusPickerShow = false;
         },
-        selectStatus(status) {
-            this.userInfo.workStatus = status;
-            this.closeStatusPicker();
+        
+        // 选择状态 @author steelwei
+        async selectStatus(statusText) {
+            const statusMap = {
+                '接单中': 'busy',
+                '休息中': 'resting'
+            };
+            const status = statusMap[statusText];
+            
+            try {
+                const res = await updateStatus(status);
+                if (res.code === 200) {
+                    this.userInfo.workStatus = statusText;
+                    uni.showToast({ title: '状态已更新', icon: 'success' });
+                } else {
+                    uni.showToast({ title: res.msg || '修改失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('修改状态失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
+            } finally {
+                this.closeStatusPicker();
+            }
         },
         
-        // 城市选择逻辑
-        showCityPicker() {
+        // 城市级联选择器(与我要加入页面一致) @author steelwei
+        async showCityPicker() {
             this.isCityPickerShow = true;
-            // 初始化/重置
-            this.cityStep = 0;
-            this.selectedProvince = '';
-            this.selectedCity = '';
-            this.selectedDistrict = '';
-            
-            // 如果已有值,尝试回显 (这里简化处理,只重置)
-            // 实际开发可解析 this.userInfo.city 进行回显
+            if (this.selectedPathway.length === 0) {
+                await this.resetCityPicker();
+            }
+        },
+        async resetCityPicker() {
+            this.selectStep = 0;
+            this.selectedPathway = [];
+            await this.loadAreaChildren(0);
         },
         closeCityPicker() {
             this.isCityPickerShow = false;
         },
-        changeCityStep(step) {
-            this.cityStep = step;
-        },
-        selectItem(item) {
-            if (this.cityStep === 0) {
-                this.selectedProvince = item;
-                this.cityStep = 1;
-                this.selectedCity = ''; // 重置下级
-                this.selectedDistrict = '';
-            } else if (this.cityStep === 1) {
-                this.selectedCity = item;
-                this.cityStep = 2;
-                this.selectedDistrict = '';
-            } else if (this.cityStep === 2) {
-                this.selectedDistrict = item;
+        async loadAreaChildren(parentId) {
+            try {
+                const res = await getAreaChildren(parentId);
+                // 城市选择器只显示 城市(0) 和 区域(1),不显示站点(2)
+                this.currentCityList = (res.data || [])
+                    .filter(item => item.type !== 2)
+                    .map(item => ({
+                        id: item.id,
+                        name: item.name,
+                        type: item.type,
+                        parentId: item.parentId
+                    }));
+            } catch (err) {
+                console.error('加载区域数据失败:', err);
+                this.currentCityList = [];
             }
         },
-        isSelected(item) {
-            if (this.cityStep === 0) return this.selectedProvince === item;
-            if (this.cityStep === 1) return this.selectedCity === item;
-            if (this.cityStep === 2) return this.selectedDistrict === item;
-            return false;
+        async selectCityItem(item) {
+            this.selectedPathway[this.selectStep] = item;
+            // type: 0=城市, 1=区域
+            if (item.type === 0) {
+                this.selectStep++;
+                this.selectedPathway = this.selectedPathway.slice(0, this.selectStep);
+                await this.loadAreaChildren(item.id);
+                if (this.currentCityList.length === 0) {
+                    this.selectedCityId = item.id;
+                    this.confirmCity();
+                }
+            } else {
+                // 区域级(1)选完即确认
+                this.selectedCityId = item.id;
+                this.confirmCity();
+            }
         },
-        confirmCity() {
-            if (this.selectedProvince && this.selectedCity && this.selectedDistrict) {
-                this.userInfo.city = `${this.selectedProvince} ${this.selectedCity} ${this.selectedDistrict}`;
-                this.closeCityPicker();
+        async jumpToStep(step) {
+            this.selectStep = step;
+            if (step === 0) {
+                await this.loadAreaChildren(0);
             } else {
-                uni.showToast({
-                    title: '请选择完整的省市区',
-                    icon: 'none'
-                });
+                const parent = this.selectedPathway[step - 1];
+                if (parent) {
+                    await this.loadAreaChildren(parent.id);
+                }
+            }
+        },
+        // 确认城市选择 @author steelwei
+        async confirmCity() {
+            if (this.selectedPathway.length === 0) {
+                uni.showToast({ title: '请选择城市', icon: 'none' });
+                return;
+            }
+            const cityName = this.selectedPathway.map(i => i.name).join(' ');
+            const cityCode = String(this.selectedCityId);
+            
+            try {
+                const res = await updateCity(cityCode, cityName);
+                if (res.code === 200) {
+                    this.userInfo.city = cityName;
+                    uni.showToast({ title: '修改成功', icon: 'success' });
+                    this.closeCityPicker();
+                    // 重置选择器,下次打开重新加载
+                    this.selectedPathway = [];
+                } else {
+                    uni.showToast({ title: res.msg || '修改失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('修改城市失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
             }
         }
     }
@@ -403,36 +496,48 @@ page {
 .popup-title-text { font-size: 32rpx; font-weight: bold; color: #333; }
 .popup-btn-confirm { font-size: 28rpx; color: #FF5722; font-weight: bold; }
 
-.city-tabs {
+/* 级联城市选择器(与我要加入页面一致) */
+.picker-body {
     display: flex;
-    padding: 20rpx 30rpx;
-    border-bottom: 1px solid #eee;
+    height: 500rpx;
 }
-.city-tab-item {
-    font-size: 28rpx;
-    margin-right: 40rpx;
-    position: relative;
-    padding-bottom: 10rpx;
-}
-.city-tab-item.active { color: #333; }
-.city-tab-item.active-red { color: #FF5722; font-weight: bold; }
-.city-tab-item.active-red::after {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    left: 50%;
-    transform: translateX(-50%);
-    width: 40rpx;
-    height: 4rpx;
-    background-color: #FF5722;
-    border-radius: 2rpx;
+.timeline-area {
+    width: 240rpx;
+    padding: 20rpx;
+    background: #f8f8f8;
+    border-right: 1px solid #eee;
+    overflow-y: auto;
 }
-.city-list {
-    height: 400rpx;
+.timeline-item {
+    display: flex;
+    align-items: center;
+    padding: 16rpx 0;
+    font-size: 26rpx;
+    color: #666;
 }
-.city-item {
-    padding: 30rpx;
+.timeline-item.active {
+    color: #FF5722;
+    font-weight: bold;
+}
+.timeline-dot {
+    width: 16rpx;
+    height: 16rpx;
+    border-radius: 50%;
+    background: #ccc;
+    margin-right: 12rpx;
+    flex-shrink: 0;
+}
+.timeline-item.active .timeline-dot {
+    background: #FF5722;
+}
+.list-area {
+    flex: 1;
+    height: 100%;
+}
+.list-area .list-item {
+    padding: 24rpx 30rpx;
     font-size: 28rpx;
     color: #333;
+    border-bottom: 1px solid #f5f5f5;
 }
 </style>

+ 199 - 0
pages/mine/settings/security/change-password.vue

@@ -0,0 +1,199 @@
+<template>
+    <view class="container">
+        <!-- 自定义头部 -->
+        <view class="custom-header">
+            <view class="header-left" @click="navBack">
+                <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);"></image>
+            </view>
+            <text class="header-title">修改密码</text>
+            <view class="header-right"></view>
+        </view>
+        <view class="header-placeholder"></view>
+
+        <view class="form-card">
+            <view class="form-item">
+                <text class="form-label">旧密码</text>
+                <input 
+                    class="form-input" 
+                    type="password" 
+                    v-model="oldPassword" 
+                    placeholder="请输入旧密码"
+                    placeholder-class="placeholder"
+                />
+            </view>
+            <view class="form-item">
+                <text class="form-label">新密码</text>
+                <input 
+                    class="form-input" 
+                    type="password" 
+                    v-model="newPassword" 
+                    placeholder="请输入新密码(6-20位)"
+                    placeholder-class="placeholder"
+                />
+            </view>
+            <view class="form-item no-border">
+                <text class="form-label">确认密码</text>
+                <input 
+                    class="form-input" 
+                    type="password" 
+                    v-model="confirmPassword" 
+                    placeholder="请再次输入新密码"
+                    placeholder-class="placeholder"
+                />
+            </view>
+        </view>
+
+        <view class="btn-area">
+            <button class="submit-btn" @click="submitChange">确认修改</button>
+        </view>
+    </view>
+</template>
+
+<script>
+// 引入 API @author steelwei
+import { updatePassword } from '@/api/fulfiller'
+
+export default {
+    data() {
+        return {
+            oldPassword: '',
+            newPassword: '',
+            confirmPassword: ''
+        }
+    },
+    methods: {
+        navBack() {
+            uni.navigateBack({ delta: 1 });
+        },
+        
+        // 提交修改 @author steelwei
+        async submitChange() {
+            // 验证输入
+            if (!this.oldPassword) {
+                uni.showToast({ title: '请输入旧密码', icon: 'none' });
+                return;
+            }
+            if (!this.newPassword) {
+                uni.showToast({ title: '请输入新密码', icon: 'none' });
+                return;
+            }
+            if (this.newPassword.length < 6 || this.newPassword.length > 20) {
+                uni.showToast({ title: '密码长度为6-20位', icon: 'none' });
+                return;
+            }
+            if (this.newPassword !== this.confirmPassword) {
+                uni.showToast({ title: '两次密码输入不一致', icon: 'none' });
+                return;
+            }
+            
+            uni.showLoading({ title: '提交中...' });
+            try {
+                const res = await updatePassword(this.oldPassword, this.newPassword);
+                
+                if (res.code === 200) {
+                    uni.showToast({ 
+                        title: '修改成功', 
+                        icon: 'success',
+                        duration: 2000
+                    });
+                    setTimeout(() => {
+                        uni.navigateBack({ delta: 1 });
+                    }, 2000);
+                } else {
+                    uni.showToast({ 
+                        title: res.msg || '修改失败', 
+                        icon: 'none' 
+                    });
+                }
+            } catch (error) {
+                console.error('修改密码失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        }
+    }
+}
+</script>
+
+<style>
+page {
+    background-color: #F8F8F8;
+}
+.custom-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 88rpx;
+    padding-top: var(--status-bar-height);
+    background-color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-left: 30rpx;
+    padding-right: 30rpx;
+    box-sizing: content-box;
+    z-index: 100;
+}
+.header-placeholder {
+    height: calc(88rpx + var(--status-bar-height));
+}
+.back-icon {
+    width: 40rpx;
+    height: 40rpx;
+}
+.header-title {
+    font-size: 28rpx;
+    font-weight: bold;
+    color: #333;
+}
+.header-right {
+    width: 40rpx;
+}
+
+.container {
+    padding: 20rpx 30rpx;
+}
+.form-card {
+    background-color: #fff;
+    border-radius: 20rpx;
+    padding: 0 30rpx;
+    margin-bottom: 30rpx;
+}
+.form-item {
+    display: flex;
+    align-items: center;
+    padding: 30rpx 0;
+    border-bottom: 1px solid #F5F5F5;
+}
+.form-item.no-border {
+    border-bottom: none;
+}
+.form-label {
+    width: 160rpx;
+    font-size: 28rpx;
+    color: #333;
+}
+.form-input {
+    flex: 1;
+    font-size: 28rpx;
+    color: #333;
+}
+.placeholder {
+    color: #999;
+}
+
+.btn-area {
+    padding: 40rpx 0;
+}
+.submit-btn {
+    background-color: #FF5722;
+    color: #fff;
+    font-size: 32rpx;
+    border-radius: 44rpx;
+    height: 88rpx;
+    line-height: 88rpx;
+    border: none;
+}
+</style>

+ 253 - 0
pages/mine/settings/security/change-phone.vue

@@ -0,0 +1,253 @@
+<template>
+    <view class="container">
+        <!-- 自定义头部 -->
+        <view class="custom-header">
+            <view class="header-left" @click="navBack">
+                <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);"></image>
+            </view>
+            <text class="header-title">修改手机号</text>
+            <view class="header-right"></view>
+        </view>
+        <view class="header-placeholder"></view>
+
+        <view class="form-card">
+            <view class="form-item">
+                <text class="form-label">新手机号</text>
+                <input 
+                    class="form-input" 
+                    type="number" 
+                    v-model="phone" 
+                    placeholder="请输入新手机号"
+                    placeholder-class="placeholder"
+                    maxlength="11"
+                />
+            </view>
+            <view class="form-item no-border">
+                <text class="form-label">验证码</text>
+                <input 
+                    class="form-input" 
+                    type="number" 
+                    v-model="code" 
+                    placeholder="请输入验证码"
+                    placeholder-class="placeholder"
+                    maxlength="6"
+                />
+                <button 
+                    class="code-btn" 
+                    :disabled="countdown > 0"
+                    @click="sendCode">
+                    {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
+                </button>
+            </view>
+        </view>
+
+        <view class="btn-area">
+            <button class="submit-btn" @click="submitChange">确认修改</button>
+        </view>
+
+        <view class="tips">
+            <text class="tips-text">• 修改手机号后,新手机号将作为登录账号</text>
+            <text class="tips-text">• 请确保新手机号可以正常接收短信</text>
+        </view>
+    </view>
+</template>
+
+<script>
+// 引入 API @author steelwei
+import { updatePhone } from '@/api/fulfiller'
+
+export default {
+    data() {
+        return {
+            phone: '',
+            code: '',
+            countdown: 0,
+            timer: null
+        }
+    },
+    onUnload() {
+        if (this.timer) {
+            clearInterval(this.timer);
+        }
+    },
+    methods: {
+        navBack() {
+            uni.navigateBack({ delta: 1 });
+        },
+        
+        // 发送验证码 @author steelwei
+        sendCode() {
+            // 验证手机号
+            if (!this.phone) {
+                uni.showToast({ title: '请输入手机号', icon: 'none' });
+                return;
+            }
+            if (!/^1[3-9]\d{9}$/.test(this.phone)) {
+                uni.showToast({ title: '手机号格式不正确', icon: 'none' });
+                return;
+            }
+            
+            // TODO: 调用发送验证码接口
+            uni.showToast({ title: '验证码已发送', icon: 'success' });
+            
+            // 开始倒计时
+            this.countdown = 60;
+            this.timer = setInterval(() => {
+                this.countdown--;
+                if (this.countdown <= 0) {
+                    clearInterval(this.timer);
+                }
+            }, 1000);
+        },
+        
+        // 提交修改 @author steelwei
+        async submitChange() {
+            // 验证输入
+            if (!this.phone) {
+                uni.showToast({ title: '请输入手机号', icon: 'none' });
+                return;
+            }
+            if (!/^1[3-9]\d{9}$/.test(this.phone)) {
+                uni.showToast({ title: '手机号格式不正确', icon: 'none' });
+                return;
+            }
+            if (!this.code) {
+                uni.showToast({ title: '请输入验证码', icon: 'none' });
+                return;
+            }
+            
+            uni.showLoading({ title: '提交中...' });
+            try {
+                const res = await updatePhone(this.phone, this.code);
+                
+                if (res.code === 200) {
+                    uni.showToast({ 
+                        title: '修改成功', 
+                        icon: 'success',
+                        duration: 2000
+                    });
+                    setTimeout(() => {
+                        uni.navigateBack({ delta: 1 });
+                    }, 2000);
+                } else {
+                    uni.showToast({ 
+                        title: res.msg || '修改失败', 
+                        icon: 'none' 
+                    });
+                }
+            } catch (error) {
+                console.error('修改手机号失败:', error);
+                uni.showToast({ title: '网络错误', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        }
+    }
+}
+</script>
+
+<style>
+page {
+    background-color: #F8F8F8;
+}
+.custom-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 88rpx;
+    padding-top: var(--status-bar-height);
+    background-color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-left: 30rpx;
+    padding-right: 30rpx;
+    box-sizing: content-box;
+    z-index: 100;
+}
+.header-placeholder {
+    height: calc(88rpx + var(--status-bar-height));
+}
+.back-icon {
+    width: 40rpx;
+    height: 40rpx;
+}
+.header-title {
+    font-size: 28rpx;
+    font-weight: bold;
+    color: #333;
+}
+.header-right {
+    width: 40rpx;
+}
+
+.container {
+    padding: 20rpx 30rpx;
+}
+.form-card {
+    background-color: #fff;
+    border-radius: 20rpx;
+    padding: 0 30rpx;
+    margin-bottom: 30rpx;
+}
+.form-item {
+    display: flex;
+    align-items: center;
+    padding: 30rpx 0;
+    border-bottom: 1px solid #F5F5F5;
+}
+.form-item.no-border {
+    border-bottom: none;
+}
+.form-label {
+    width: 160rpx;
+    font-size: 28rpx;
+    color: #333;
+}
+.form-input {
+    flex: 1;
+    font-size: 28rpx;
+    color: #333;
+}
+.placeholder {
+    color: #999;
+}
+.code-btn {
+    width: 180rpx;
+    height: 60rpx;
+    line-height: 60rpx;
+    background-color: #FF5722;
+    color: #fff;
+    font-size: 24rpx;
+    border-radius: 30rpx;
+    padding: 0;
+    margin-left: 20rpx;
+}
+.code-btn[disabled] {
+    background-color: #ccc;
+}
+
+.btn-area {
+    padding: 40rpx 0;
+}
+.submit-btn {
+    background-color: #FF5722;
+    color: #fff;
+    font-size: 32rpx;
+    border-radius: 44rpx;
+    height: 88rpx;
+    line-height: 88rpx;
+    border: none;
+}
+
+.tips {
+    padding: 20rpx 30rpx;
+}
+.tips-text {
+    display: block;
+    font-size: 24rpx;
+    color: #999;
+    line-height: 40rpx;
+}
+</style>

+ 53 - 10
pages/mine/settings/security/index.vue

@@ -15,14 +15,14 @@
             <view class="list-item" @click="changeMobile">
                 <text class="item-title">手机号</text>
                 <view class="item-right">
-                    <text class="item-value">136****5678</text>
+                    <text class="item-value">{{ maskPhone(phone) || '未设置' }}</text>
                     <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
                 </view>
             </view>
-            <view class="list-item">
+            <view class="list-item" @click="changePassword">
                 <text class="item-title">登录密码</text>
                 <view class="item-right">
-                    <text class="item-value">未设置</text>
+                    <text class="item-value">{{ hasPassword ? '已设置' : '未设置' }}</text>
                     <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
                 </view>
             </view>
@@ -39,26 +39,69 @@
 </template>
 
 <script>
+import { getMyProfile, deleteAccount } from '@/api/fulfiller'
+
 export default {
     data() {
-        return {}
+        return {
+            phone: '',
+            hasPassword: false
+        }
+    },
+    onLoad() {
+        this.loadProfile()
     },
     methods: {
         navBack() {
-             uni.navigateBack({
+            uni.navigateBack({
                 delta: 1
             });
         },
+        async loadProfile() {
+            try {
+                const res = await getMyProfile()
+                if (res.code === 200 && res.data) {
+                    this.phone = res.data.phone || ''
+                    this.hasPassword = !!res.data.hasPassword
+                }
+            } catch (e) {
+                console.error('加载个人信息失败', e)
+            }
+        },
+        maskPhone(phone) {
+            if (!phone || phone.length < 11) return phone
+            return phone.substring(0, 3) + '****' + phone.substring(7)
+        },
         changeMobile() {
-            // 跳转修改手机号
+            uni.navigateTo({
+                url: '/pages/mine/settings/security/change-phone'
+            })
+        },
+        changePassword() {
+            uni.navigateTo({
+                url: '/pages/mine/settings/security/change-password'
+            })
         },
-        deleteAccount() {
-             uni.showModal({
+        async deleteAccount() {
+            uni.showModal({
                 title: '警示',
                 content: '注销账号后将无法恢复,确定要继续吗?',
-                success: function (res) {
+                success: async (res) => {
                     if (res.confirm) {
-                        uni.showToast({ title: '已提交注销申请', icon: 'none' });
+                        try {
+                            const result = await deleteAccount()
+                            if (result.code === 200) {
+                                uni.showToast({ title: '账号已注销', icon: 'success' })
+                                setTimeout(() => {
+                                    uni.reLaunch({ url: '/pages/login/login' })
+                                }, 1500)
+                            } else {
+                                uni.showToast({ title: result.msg || '注销失败', icon: 'none' })
+                            }
+                        } catch (e) {
+                            console.error('注销账号失败', e)
+                            uni.showToast({ title: '注销失败', icon: 'none' })
+                        }
                     }
                 }
             });

+ 81 - 6
pages/recruit/auth_logic.js

@@ -1,3 +1,5 @@
+import { uploadFile } from '@/api/fulfiller'
+
 export default {
     data() {
         return {
@@ -7,8 +9,10 @@ export default {
                 idNumber: '',
                 expiryDate: ''
             },
-            idCardFront: '', // 身份证正面路径
-            idCardBack: '',  // 身份证反面路径
+            idCardFront: '',      // 身份证正面本地预览路径
+            idCardBack: '',       // 身份证反面本地预览路径
+            idCardFrontOssId: '', // 身份证正面 OSS ID
+            idCardBackOssId: '',  // 身份证反面 OSS ID
             showDatePicker: false,
             pickerValue: [0, 0, 0], // YYYY-MM-DD
             years: [],
@@ -26,6 +30,8 @@ export default {
             }
         }
         this.initDateData();
+        // 从本地缓存恢复实名认证数据(返回上一级再进来时不丢失)
+        this.restoreAuthData();
     },
     methods: {
         // --- 日期选择器逻辑 (简化版, 仅示意) ---
@@ -52,17 +58,67 @@ export default {
             this.formData.expiryDate = e.detail.value;
         },
 
-        // --- 图片上传 ---
+        // --- 数据持久化(防止返回上一级丢失) ---
+        restoreAuthData() {
+            try {
+                const saved = uni.getStorageSync('recruit_auth_data');
+                if (saved) {
+                    const d = JSON.parse(saved);
+                    this.formData.name = d.name || '';
+                    this.formData.idNumber = d.idNumber || '';
+                    this.formData.expiryDate = d.expiryDate || '';
+                    this.idCardFront = d.idCardFront || '';
+                    this.idCardBack = d.idCardBack || '';
+                    this.idCardFrontOssId = d.idCardFrontOssId || '';
+                    this.idCardBackOssId = d.idCardBackOssId || '';
+                }
+            } catch (e) {
+                console.error('恢复认证数据失败', e);
+            }
+        },
+        saveAuthData() {
+            try {
+                uni.setStorageSync('recruit_auth_data', JSON.stringify({
+                    name: this.formData.name,
+                    idNumber: this.formData.idNumber,
+                    expiryDate: this.formData.expiryDate,
+                    idCardFront: this.idCardFront,
+                    idCardBack: this.idCardBack,
+                    idCardFrontOssId: this.idCardFrontOssId,
+                    idCardBackOssId: this.idCardBackOssId
+                }));
+            } catch (e) {
+                console.error('保存认证数据失败', e);
+            }
+        },
+
+        // --- 图片上传(选择后自动上传到OSS) ---
         chooseImage(side) {
             uni.chooseImage({
                 count: 1,
                 sizeType: ['compressed'],
                 sourceType: ['album', 'camera'],
-                success: (res) => {
+                success: async (res) => {
+                    const tempPath = res.tempFilePaths[0];
                     if (side === 'front') {
-                        this.idCardFront = res.tempFilePaths[0];
+                        this.idCardFront = tempPath;
                     } else {
-                        this.idCardBack = res.tempFilePaths[0];
+                        this.idCardBack = tempPath;
+                    }
+                    // 上传到OSS
+                    try {
+                        uni.showLoading({ title: '上传中...' });
+                        const uploadRes = await uploadFile(tempPath);
+                        if (side === 'front') {
+                            this.idCardFrontOssId = uploadRes.data.ossId;
+                        } else {
+                            this.idCardBackOssId = uploadRes.data.ossId;
+                        }
+                        uni.hideLoading();
+                        this.saveAuthData();
+                    } catch (err) {
+                        uni.hideLoading();
+                        console.error('上传身份证图片失败:', err);
                     }
                 }
             });
@@ -83,6 +139,25 @@ export default {
             }
             */
 
+            // 保存认证数据到缓存
+            this.saveAuthData();
+
+            // 合并身份认证数据到已暂存的招募表单
+            try {
+                const stored = uni.getStorageSync('recruit_form_data')
+                if (stored) {
+                    const data = JSON.parse(stored)
+                    data.realName = this.formData.name
+                    data.idNumber = this.formData.idNumber
+                    data.expiryDate = this.formData.expiryDate
+                    data.idCardFrontOssId = this.idCardFrontOssId
+                    data.idCardBackOssId = this.idCardBackOssId
+                    uni.setStorageSync('recruit_form_data', JSON.stringify(data))
+                }
+            } catch (e) {
+                console.error('保存认证数据失败', e)
+            }
+
             // 传递数据
             const services = JSON.stringify(this.serviceType);
             uni.navigateTo({

+ 3 - 3
pages/recruit/form.vue

@@ -19,7 +19,7 @@
         <text class="label">验证码</text>
         <view class="input-box">
           <input class="input" type="number" v-model="formData.code" placeholder="验证码" />
-          <text class="get-code-text">获取验证码</text>
+          <text class="get-code-text" @click="getVerifyCode">{{ countDown > 0 ? countDown + 's' : '获取验证码' }}</text>
         </view>
       </view>
 
@@ -106,7 +106,7 @@
       <view class="form-item">
         <text class="label">工作城市</text>
         <view class="input-box" @click="openCityPicker">
-            <text>{{ formData.city || '广东省 深圳市 龙华区' }}</text>
+            <text :style="{color: formData.city ? '#333' : '#ccc'}">{{ formData.city || '请选择工作城市' }}</text>
             <!-- 灰色箭头 SVG -->
             <svg class="arrow-right" style="width:24rpx; height:24rpx; margin-left: auto;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
                 <path d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-45.056L382.592 149.312a29.12 29.12 0 0 0-41.728 0z" fill="#CCCCCC"></path>
@@ -195,7 +195,7 @@
                     :key="index"
                     @click="selectStation(item)"
                 >
-                    {{ item }}
+                    {{ item.name }}
                 </view>
             </scroll-view>
         </view>

+ 133 - 69
pages/recruit/logic.js

@@ -1,73 +1,49 @@
+import { sendSmsCode } from '@/api/auth'
+import { getAreaChildren } from '@/api/fulfiller'
+
 export default {
     data() {
         return {
             formData: {
-                mobile: '13612345678',
+                mobile: '',
                 code: '',
-                name: '张三哥',
+                name: '',
                 gender: 1, // 1男 2女
-                birthday: '2026-02-13',
+                birthday: '',
                 password: '',
-                serviceType: ['宠物接送'],
+                serviceType: [],
                 city: '',
-                station: ''
+                station: '',
+                stationId: null
             },
             showPwd: false,
             isAgreed: false,
             serviceTypes: ['宠物接送', '上门喂遛', '上门洗护'],
 
+            // 验证码倒计时
+            countDown: 0,
+            timer: null,
+
             // 日期选择器相关
             showPicker: false,
             years: [],
             months: [],
             days: [],
-            pickerValue: [0, 0, 0], // 选中项索引
+            pickerValue: [0, 0, 0],
             tempYear: 0,
             tempMonth: 0,
             tempDay: 0,
 
-            // 城市选择器相关
+            // 城市选择器相关(从后端加载)
             showCityPicker: false,
-            // 模拟级联数据
-            cityData: [
-                {
-                    id: 11, name: '北京市', children: [
-                        {
-                            id: 1101, name: '北京市', children: [
-                                { id: 110101, name: '东城区' },
-                                { id: 110105, name: '朝阳区' },
-                                { id: 110108, name: '海淀区' }
-                            ]
-                        }
-                    ]
-                },
-                {
-                    id: 44, name: '广东省', children: [
-                        {
-                            id: 4401, name: '广州市', children: [
-                                { id: 440106, name: '天河区' },
-                                { id: 440104, name: '越秀区' }
-                            ]
-                        },
-                        {
-                            id: 4403, name: '深圳市', children: [
-                                { id: 440304, name: '福田区' },
-                                { id: 440305, name: '南山区' },
-                                { id: 440306, name: '宝安区' },
-                                { id: 440309, name: '龙华区' }
-                            ]
-                        }
-                    ]
-                }
-            ],
-            selectStep: 0, // 0:省, 1:市, 2:区
-            selectedPathway: [], // 已选节点 [{id, name}, ...]
-            currentList: [], // 当前可选列表
-
+            selectStep: 0,
+            selectedPathway: [],
+            currentList: [],
+            selectedCityId: null,
 
-            // 站点选择器相关
+            // 站点选择器相关(从后端加载)
             showStationPicker: false,
-            stationList: ['民治街道第一站', '民治街道第二站', '龙华中心站', '福田区分站'],
+            stationList: [],
 
             // 协议弹窗
             showPrivacy: false,
@@ -78,6 +54,9 @@ export default {
     created() {
         this.initDateData();
     },
+    beforeDestroy() {
+        if (this.timer) clearInterval(this.timer);
+    },
     methods: {
         initDateData() {
             const now = new Date();
@@ -149,66 +128,137 @@ export default {
             }
         },
 
-        // 城市选择器 logic
-        openCityPicker() {
+        // 验证码
+        async getVerifyCode() {
+            if (this.countDown > 0) return;
+            if (!this.formData.mobile || this.formData.mobile.length !== 11) {
+                uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
+                return;
+            }
+            try {
+                const res = await sendSmsCode(this.formData.mobile);
+                this.countDown = 60;
+                this.timer = setInterval(() => {
+                    this.countDown--;
+                    if (this.countDown <= 0) clearInterval(this.timer);
+                }, 1000);
+                // TODO 【生产环境必须删除】开发模式自动填入验证码
+                const devCode = res.data;
+                if (devCode) {
+                    this.formData.code = devCode;
+                    uni.showToast({ title: '验证码: ' + devCode, icon: 'none', duration: 3000 });
+                } else {
+                    uni.showToast({ title: '验证码已发送', icon: 'none' });
+                }
+            } catch (err) {
+                console.error('发送验证码失败:', err);
+            }
+        },
+
+        // 城市选择器 logic(从后端加载)
+        async openCityPicker() {
             this.showCityPicker = true;
-            // 如果未选择过,初始化第一级
             if (this.selectedPathway.length === 0) {
-                this.resetCityPicker();
+                await this.resetCityPicker();
             }
         },
-        resetCityPicker() {
+        async resetCityPicker() {
             this.selectStep = 0;
             this.selectedPathway = [];
-            this.currentList = this.cityData;
+            await this.loadAreaChildren(0);
         },
         closeCityPicker() {
             this.showCityPicker = false;
         },
-        // 点击列表项
-        selectCityItem(item) {
-            // 记录当前选择
+        async loadAreaChildren(parentId) {
+            try {
+                const res = await getAreaChildren(parentId);
+                // 城市选择器只显示 城市(0) 和 区域(1),不显示站点(2)
+                this.currentList = (res.data || [])
+                    .filter(item => item.type !== 2)
+                    .map(item => ({
+                        id: item.id,
+                        name: item.name,
+                        type: item.type,
+                        parentId: item.parentId
+                    }));
+            } catch (err) {
+                console.error('加载区域数据失败:', err);
+                this.currentList = [];
+            }
+        },
+        async selectCityItem(item) {
             this.selectedPathway[this.selectStep] = item;
-
-            // 尝试获取下一级
-            if (item.children && item.children.length > 0) {
+            // type: 0=城市, 1=区域
+            // 城市级(0)继续加载子级区域
+            if (item.type === 0) {
                 this.selectStep++;
-                this.currentList = item.children;
-                // 清理后续旧数据(如果是重新选择的情况)
                 this.selectedPathway = this.selectedPathway.slice(0, this.selectStep);
+                await this.loadAreaChildren(item.id);
+                // 如果已无子级区域,自动确认
+                if (this.currentList.length === 0) {
+                    this.selectedCityId = item.id;
+                    this.confirmCity();
+                }
             } else {
-                // 没有下一级了,自动确认
+                // 区域级(1)选完即确认,站点由站点选择器单独加载
+                this.selectedCityId = item.id;
                 this.confirmCity();
             }
         },
-        // 点击“已选路径”回溯
-        jumpToStep(step) {
+        async jumpToStep(step) {
             this.selectStep = step;
-            // 重新计算该级的列表
             if (step === 0) {
-                this.currentList = this.cityData;
+                await this.loadAreaChildren(0);
             } else {
-                // 列表是上一级选择的 item 的 children
                 const parent = this.selectedPathway[step - 1];
-                this.currentList = parent ? parent.children : [];
+                if (parent) {
+                    await this.loadAreaChildren(parent.id);
+                }
             }
         },
         confirmCity() {
-            // 拼接完整名称
             const fullPath = this.selectedPathway.map(i => i.name).join(' ');
             this.formData.city = fullPath;
+            // 重置已选站点
+            this.formData.station = '';
+            this.formData.stationId = null;
+            // 选完城市/区域后加载该区域下的站点(type=2)
+            const lastSelected = this.selectedPathway[this.selectedPathway.length - 1];
+            if (lastSelected) {
+                this.loadStations(lastSelected.id);
+            }
             this.closeCityPicker();
         },
 
-        // 站点选择器
+        // 站点选择器(从后端加载,只取type=2的站点)
+        async loadStations(parentId) {
+            try {
+                const res = await getAreaChildren(parentId);
+                this.stationList = (res.data || [])
+                    .filter(item => item.type === 2)
+                    .map(item => ({
+                        id: item.id,
+                        name: item.name
+                    }));
+            } catch (err) {
+                console.error('加载站点数据失败:', err);
+                this.stationList = [];
+            }
+        },
         openStationPicker() {
+            if (this.stationList.length === 0) {
+                uni.showToast({ title: '请先选择工作城市', icon: 'none' });
+                return;
+            }
             this.showStationPicker = true;
         },
         closeStationPicker() {
             this.showStationPicker = false;
         },
         selectStation(item) {
-            this.formData.station = item;
+            this.formData.station = item.name;
+            this.formData.stationId = item.id;
             this.closeStationPicker();
         },
 
@@ -223,6 +273,20 @@ export default {
                 uni.showToast({ title: '请勾选协议', icon: 'none' });
                 return;
             }
+            if (!this.formData.mobile || this.formData.mobile.length !== 11) {
+                uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
+                return;
+            }
+            if (!this.formData.name) {
+                uni.showToast({ title: '请输入姓名', icon: 'none' });
+                return;
+            }
+            if (this.formData.serviceType.length === 0) {
+                uni.showToast({ title: '请选择服务类型', icon: 'none' });
+                return;
+            }
+            // 暂存表单数据到本地,供后续页面组装提交
+            uni.setStorageSync('recruit_form_data', JSON.stringify(this.formData));
             const services = JSON.stringify(this.formData.serviceType);
             uni.navigateTo({
                 url: `/pages/recruit/auth?services=${services}`

+ 66 - 19
pages/recruit/qualifications_logic.js

@@ -1,8 +1,11 @@
+import { submitAudit, uploadFile } from '@/api/fulfiller'
+
 export default {
     data() {
         return {
             serviceTypes: [], // 从上一页传递过来的服务类型列表
-            qualifications: {}, // 存储图片路径 { '宠物接送': 'path/to/img', ... }
+            qualifications: {}, // 存储本地预览路径 { '宠物接送': ['path1', ...], ... }
+            qualOssIds: {},    // 存储OSS ID { '宠物接送': ['id1', ...], ... }
         }
     },
     onLoad(options) {
@@ -11,8 +14,8 @@ export default {
                 this.serviceTypes = JSON.parse(options.services);
                 // 初始化 qualifications 对象
                 this.serviceTypes.forEach(type => {
-                    // 使用 Vue.set 或直接赋值 
                     this.qualifications[type] = [];
+                    this.qualOssIds[type] = [];
                 });
             } catch (e) {
                 console.error('Parse services failed', e);
@@ -25,17 +28,30 @@ export default {
                 count: 9,
                 sizeType: ['compressed'],
                 sourceType: ['album', 'camera'],
-                success: (res) => {
+                success: async (res) => {
                     if (!this.qualifications[serviceName]) {
                         this.qualifications[serviceName] = [];
+                        this.qualOssIds[serviceName] = [];
+                    }
+                    // 逐个上传到OSS
+                    for (const tempPath of res.tempFilePaths) {
+                        this.qualifications[serviceName].push(tempPath);
+                        this.$forceUpdate();
+                        try {
+                            const uploadRes = await uploadFile(tempPath);
+                            this.qualOssIds[serviceName].push(uploadRes.data.ossId);
+                        } catch (err) {
+                            console.error('上传资质图片失败:', err);
+                        }
                     }
-                    this.qualifications[serviceName] = this.qualifications[serviceName].concat(res.tempFilePaths);
-                    this.$forceUpdate();
                 }
             });
         },
         deleteImage(serviceName, index) {
             this.qualifications[serviceName].splice(index, 1);
+            if (this.qualOssIds[serviceName]) {
+                this.qualOssIds[serviceName].splice(index, 1);
+            }
             this.$forceUpdate();
         },
         goBackToForm() {
@@ -52,22 +68,53 @@ export default {
                 });
             }
         },
-        submit() {
-            // 校验是否所有资质都已上传
-            // const missing = this.serviceTypes.find(type => !this.qualifications[type]);
-            // if (missing) {
-            //     uni.showToast({ title: `请上传${missing}服务资质`, icon: 'none' });
-            //     return;
-            // }
-
-            // 实际可能允许部分上传或者有后台逻辑,这里假设全部必填
-            // 为了演示方便,暂时不强制校验,或者校验全部
+        async submit() {
+            // 收集前面页面暂存在本地的表单数据
+            let recruitData = {}
+            try {
+                const stored = uni.getStorageSync('recruit_form_data')
+                if (stored) {
+                    recruitData = JSON.parse(stored)
+                }
+            } catch (e) {
+                console.error('读取招募表单数据失败', e)
+            }
 
-            // 提交成功,跳转到成功页
-            // 提交成功,跳转到成功页
-            uni.reLaunch({
-                url: '/pages/recruit/success'
+            // 收集所有资质图片OSS ID
+            const allQualOssIds = [];
+            Object.values(this.qualOssIds).forEach(ids => {
+                allQualOssIds.push(...ids);
             });
+
+            // 组装提交数据(匹配 FlfAuditBo 字段)
+            const auditData = {
+                name: recruitData.name || '',
+                phone: recruitData.mobile || '',
+                password: recruitData.password || '',
+                gender: recruitData.gender === 1 ? '0' : '1',
+                birthday: recruitData.birthday || '',
+                serviceTypes: JSON.stringify(recruitData.serviceType || []),
+                city: recruitData.city || '',
+                stationId: recruitData.stationId || null,
+                realName: recruitData.realName || '',
+                idCard: recruitData.idNumber || '',
+                idValidDate: recruitData.expiryDate || '',
+                idCardFront: recruitData.idCardFrontOssId || null,
+                idCardBack: recruitData.idCardBackOssId || null,
+                qualifications: JSON.stringify(allQualOssIds)
+            }
+
+            uni.showLoading({ title: '提交中...' })
+            try {
+                await submitAudit(auditData)
+                uni.hideLoading()
+                uni.reLaunch({
+                    url: '/pages/recruit/success'
+                })
+            } catch (err) {
+                uni.hideLoading()
+                console.error('提交申请失败:', err)
+            }
         }
     }
 }

+ 3 - 3
unpackage/dist/cache/.vite/deps/_metadata.json

@@ -1,8 +1,8 @@
 {
-  "hash": "fd71fe9a",
-  "configHash": "b19c648e",
+  "hash": "a87165aa",
+  "configHash": "1c48c81f",
   "lockfileHash": "e3b0c442",
-  "browserHash": "262e9795",
+  "browserHash": "a288dfff",
   "optimized": {},
   "chunks": {}
 }

+ 72 - 0
utils/auth.js

@@ -0,0 +1,72 @@
+/**
+ * Token 存储管理
+ * @author steelwei
+ */
+
+const TOKEN_KEY = 'fulfiller_token'
+const USER_INFO_KEY = 'fulfiller_user_info'
+
+/**
+ * 获取 Token
+ */
+export function getToken() {
+  return uni.getStorageSync(TOKEN_KEY) || ''
+}
+
+/**
+ * 设置 Token
+ */
+export function setToken(token) {
+  uni.setStorageSync(TOKEN_KEY, token)
+}
+
+/**
+ * 移除 Token
+ */
+export function removeToken() {
+  uni.removeStorageSync(TOKEN_KEY)
+}
+
+/**
+ * 是否已登录
+ */
+export function isLoggedIn() {
+  return !!getToken()
+}
+
+/**
+ * 获取缓存的用户信息
+ */
+export function getUserInfo() {
+  const str = uni.getStorageSync(USER_INFO_KEY)
+  if (str) {
+    try {
+      return JSON.parse(str)
+    } catch (e) {
+      return null
+    }
+  }
+  return null
+}
+
+/**
+ * 设置用户信息缓存
+ */
+export function setUserInfo(info) {
+  uni.setStorageSync(USER_INFO_KEY, JSON.stringify(info))
+}
+
+/**
+ * 清除用户信息缓存
+ */
+export function removeUserInfo() {
+  uni.removeStorageSync(USER_INFO_KEY)
+}
+
+/**
+ * 清除所有登录信息
+ */
+export function clearAuth() {
+  removeToken()
+  removeUserInfo()
+}

+ 19 - 0
utils/config.js

@@ -0,0 +1,19 @@
+/**
+ * 履约者App全局配置
+ * @author steelwei
+ */
+
+// API 基础地址(开发环境)
+export const BASE_URL = 'http://localhost:8080'
+
+// 履约者App客户端ID(需要在 sys_client 表中配置)
+export const CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
+
+// 履约者App平台码
+export const PLATFORM_CODE = 'FlfAppPlatformCodeX9kR7mT3wQ5vZ8nB1jY6pD4sL0hC2gA'
+
+// 履约者平台ID(对应 Platform.FULFILLER.id = 2)
+export const PLATFORM_ID = 2
+
+// 租户ID(默认)
+export const TENANT_ID = '000000'

+ 85 - 0
utils/request.js

@@ -0,0 +1,85 @@
+/**
+ * uni-app 网络请求封装
+ * @author steelwei
+ */
+import { BASE_URL, CLIENT_ID, PLATFORM_CODE, TENANT_ID } from './config'
+import { getToken, clearAuth } from './auth'
+
+/**
+ * 通用请求方法
+ * @param {Object} options - 请求配置
+ * @param {string} options.url - 请求路径(不含 BASE_URL)
+ * @param {string} [options.method='GET'] - 请求方法
+ * @param {Object} [options.data] - 请求数据
+ * @param {Object} [options.header] - 额外请求头
+ * @param {boolean} [options.needToken=true] - 是否需要携带 Token
+ * @returns {Promise}
+ */
+export default function request(options = {}) {
+  const {
+    url,
+    method = 'GET',
+    data,
+    header = {},
+    needToken = true
+  } = options
+
+  // 构建请求头
+  const headers = {
+    'Content-Type': 'application/json;charset=utf-8',
+    'clientid': CLIENT_ID,
+    'X-Platform-Code': PLATFORM_CODE,
+    ...header
+  }
+
+  // 携带 Token
+  if (needToken) {
+    const token = getToken()
+    if (token) {
+      headers['Authorization'] = 'Bearer ' + token
+    }
+  }
+
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url: BASE_URL + url,
+      method: method.toUpperCase(),
+      data,
+      header: headers,
+      success: (res) => {
+        const statusCode = res.statusCode
+        const result = res.data
+
+        // HTTP 状态码错误
+        if (statusCode === 401) {
+          // Token 过期或未登录
+          clearAuth()
+          uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
+          setTimeout(() => {
+            uni.reLaunch({ url: '/pages/login/login' })
+          }, 1500)
+          return reject(new Error('未授权'))
+        }
+
+        if (statusCode !== 200) {
+          const msg = result?.msg || `请求失败(${statusCode})`
+          uni.showToast({ title: msg, icon: 'none' })
+          return reject(new Error(msg))
+        }
+
+        // 业务状态码
+        if (result.code !== undefined && result.code !== 200) {
+          const msg = result.msg || '操作失败'
+          uni.showToast({ title: msg, icon: 'none' })
+          return reject(new Error(msg))
+        }
+
+        resolve(result)
+      },
+      fail: (err) => {
+        uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' })
+        reject(err)
+      }
+    })
+  })
+}