Explorar o código

feat(frontend): add service role apply page with multi-select, upload, address

Xiaogang Liao hai 3 meses
pai
achega
62709199f8
Modificáronse 1 ficheiros con 393 adicións e 0 borrados
  1. 393 0
      cfc-frontend/pages/profile/service-role-apply.vue

+ 393 - 0
cfc-frontend/pages/profile/service-role-apply.vue

@@ -0,0 +1,393 @@
+<template>
+  <view class="page">
+    <view class="section">
+      <view class="section-title">选择申请角色 <text class="required">*</text></view>
+      <view class="role-grid">
+        <view
+          v-for="role in roleList"
+          :key="role.key"
+          class="role-card"
+          :class="{ selected: selectedRoles.indexOf(role.key) > -1 }"
+          @click="toggleRole(role.key)"
+        >
+          <text class="role-icon">{{ role.icon }}</text>
+          <text class="role-name">{{ role.name }}</text>
+          <text v-if="selectedRoles.indexOf(role.key) > -1" class="check-mark">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="section">
+      <view class="section-title">身份证信息</view>
+      <input class="input" v-model="form.idCard" placeholder="请输入身份证号码" maxlength="18" />
+      <view class="upload-row">
+        <view class="upload-item" @click="chooseIdCardImage('front')">
+          <image v-if="form.idCardFront" :src="form.idCardFront" mode="aspectFill" class="upload-preview" />
+          <view v-else class="upload-placeholder">
+            <text class="upload-icon">+</text>
+            <text class="upload-label">身份证正面</text>
+          </view>
+        </view>
+        <view class="upload-item" @click="chooseIdCardImage('back')">
+          <image v-if="form.idCardBack" :src="form.idCardBack" mode="aspectFill" class="upload-preview" />
+          <view v-else class="upload-placeholder">
+            <text class="upload-icon">+</text>
+            <text class="upload-label">身份证反面</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="section">
+      <view class="section-title">资质证书</view>
+      <view class="cert-list">
+        <view v-for="(img, idx) in form.certificateImages" :key="getCertKey(idx)" class="cert-item">
+          <image :src="img" mode="aspectFill" class="cert-preview" />
+          <text class="cert-del" @click="removeCert(idx)">×</text>
+        </view>
+        <view class="cert-add" @click="chooseCertificate">
+          <text class="upload-icon">+</text>
+          <text class="upload-label">添加证书</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="section">
+      <view class="section-title">联系地址 <text class="required">*</text></view>
+      <AddressPicker ref="addressPicker" @change="onAddressChange" />
+      <input class="input" v-model="form.addressDetail" placeholder="详细地址(门牌号等)" />
+    </view>
+
+    <button class="submit-btn" :disabled="submitting" @click="handleSubmit">
+      {{ submitting ? '提交中...' : '提交申请' }}
+    </button>
+  </view>
+</template>
+
+<script>
+import { submitServiceRoleApply, getMyServiceRoleApply, uploadServiceRoleImage } from '../../utils/api'
+import AddressPicker from '../../components/address-picker.vue'
+
+export default {
+  components: { AddressPicker },
+  data() {
+    return {
+      roleList: [
+        { key: 'planner', name: '规划师', icon: '📋' },
+        { key: 'nutritionist', name: '营养师', icon: '🥗' },
+        { key: 'butler', name: '管家', icon: '🔔' },
+        { key: 'vendor', name: '服务商', icon: '🏪' },
+        { key: 'article_admin', name: '文章管理员', icon: '📝' },
+        { key: 'activity_provider', name: '活动提供商', icon: '🎪' }
+      ],
+      selectedRoles: [],
+      form: {
+        idCard: '',
+        idCardFront: '',
+        idCardBack: '',
+        certificateImages: [],
+        province: '',
+        city: '',
+        district: '',
+        street: '',
+        addressDetail: ''
+      },
+      submitting: false
+    }
+  },
+  onLoad() {
+    this.checkExistingApply()
+  },
+  methods: {
+    async checkExistingApply() {
+      try {
+        const res = await getMyServiceRoleApply()
+        if (res.code === 200 && res.data && res.data.status === 0) {
+          uni.showModal({
+            title: '提示',
+            content: '您已提交过申请,请等待审核',
+            showCancel: false,
+            success: () => uni.navigateBack()
+          })
+        }
+      } catch (e) {
+        // 忽略错误
+      }
+    },
+    toggleRole(key) {
+      const idx = this.selectedRoles.indexOf(key)
+      if (idx > -1) {
+        this.selectedRoles.splice(idx, 1)
+      } else {
+        this.selectedRoles.push(key)
+      }
+    },
+    chooseIdCardImage(side) {
+      uni.chooseImage({
+        count: 1,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: async (res) => {
+          uni.showLoading({ title: '上传中...' })
+          try {
+            const uploadRes = await uploadServiceRoleImage(res.tempFilePaths[0])
+            uni.hideLoading()
+            if (uploadRes.code === 200) {
+              if (side === 'front') {
+                this.form.idCardFront = uploadRes.data.url
+              } else {
+                this.form.idCardBack = uploadRes.data.url
+              }
+              uni.showToast({ title: '上传成功', icon: 'success' })
+            } else {
+              uni.showToast({ title: uploadRes.message || '上传失败', icon: 'none' })
+            }
+          } catch (e) {
+            uni.hideLoading()
+            uni.showToast({ title: '上传失败', icon: 'none' })
+          }
+        }
+      })
+    },
+    async chooseCertificate() {
+      uni.chooseImage({
+        count: 9,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: async (res) => {
+          uni.showLoading({ title: '上传中...' })
+          try {
+            for (const filePath of res.tempFilePaths) {
+              const uploadRes = await uploadServiceRoleImage(filePath)
+              if (uploadRes.code === 200) {
+                this.form.certificateImages.push(uploadRes.data.url)
+              }
+            }
+            uni.hideLoading()
+            uni.showToast({ title: '上传成功', icon: 'success' })
+          } catch (e) {
+            uni.hideLoading()
+            uni.showToast({ title: '上传失败', icon: 'none' })
+          }
+        }
+      })
+    },
+    removeCert(idx) {
+      this.form.certificateImages.splice(idx, 1)
+    },
+    getCertKey(idx) {
+      return 'cert-' + idx
+    },
+    onAddressChange(e) {
+      this.form.province = e.province || ''
+      this.form.city = e.city || ''
+      this.form.district = e.district || ''
+      this.form.street = e.street || ''
+    },
+    async handleSubmit() {
+      if (this.selectedRoles.length === 0) {
+        uni.showToast({ title: '请至少选择一个角色', icon: 'none' })
+        return
+      }
+      if (!this.form.idCardFront || !this.form.idCardBack) {
+        uni.showToast({ title: '请上传身份证正反面照片', icon: 'none' })
+        return
+      }
+      if (!this.form.province) {
+        uni.showToast({ title: '请选择联系地址', icon: 'none' })
+        return
+      }
+
+      this.submitting = true
+      uni.showLoading({ title: '提交中...' })
+      try {
+        const res = await submitServiceRoleApply({
+          roles: this.selectedRoles,
+          idCard: this.form.idCard,
+          idCardImages: [this.form.idCardFront, this.form.idCardBack],
+          certificateImages: this.form.certificateImages,
+          province: this.form.province,
+          city: this.form.city,
+          district: this.form.district,
+          street: this.form.street,
+          addressDetail: this.form.addressDetail
+        })
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showModal({
+            title: '提交成功',
+            content: '申请已提交,请等待后台审核',
+            showCancel: false,
+            success: () => uni.navigateBack()
+          })
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+      } finally {
+        this.submitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style>
+.page {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 20rpx 30rpx 100rpx;
+}
+.section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  margin-bottom: 20rpx;
+  color: #333;
+}
+.required {
+  color: #ff4444;
+}
+.role-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+.role-card {
+  width: calc(33.33% - 14rpx);
+  background: #f8f8f8;
+  border-radius: 12rpx;
+  padding: 24rpx 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  border: 2rpx solid transparent;
+}
+.role-card.selected {
+  background: #fff7ed;
+  border-color: #f97316;
+}
+.role-icon {
+  font-size: 40rpx;
+  margin-bottom: 8rpx;
+}
+.role-name {
+  font-size: 24rpx;
+  color: #333;
+}
+.check-mark {
+  position: absolute;
+  top: 8rpx;
+  right: 12rpx;
+  color: #f97316;
+  font-size: 28rpx;
+  font-weight: bold;
+}
+.input {
+  width: 100%;
+  height: 72rpx;
+  border: 2rpx solid #eee;
+  border-radius: 8rpx;
+  padding: 0 20rpx;
+  font-size: 26rpx;
+  box-sizing: border-box;
+  margin-bottom: 16rpx;
+}
+.upload-row {
+  display: flex;
+  gap: 20rpx;
+}
+.upload-item {
+  flex: 1;
+  height: 200rpx;
+  border: 2rpx dashed #ddd;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+.upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.upload-icon {
+  font-size: 48rpx;
+  color: #ccc;
+}
+.upload-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.upload-preview {
+  width: 100%;
+  height: 100%;
+}
+.cert-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.cert-item {
+  width: 160rpx;
+  height: 160rpx;
+  position: relative;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.cert-preview {
+  width: 100%;
+  height: 100%;
+}
+.cert-del {
+  position: absolute;
+  top: 4rpx;
+  right: 4rpx;
+  width: 32rpx;
+  height: 32rpx;
+  background: rgba(0,0,0,0.5);
+  color: #fff;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 32rpx;
+  font-size: 24rpx;
+}
+.cert-add {
+  width: 160rpx;
+  height: 160rpx;
+  border: 2rpx dashed #ddd;
+  border-radius: 8rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+.submit-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #f97316, #fb923c);
+  color: #fff;
+  font-size: 32rpx;
+  border-radius: 44rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  width: calc(100% - 60rpx);
+}
+.submit-btn[disabled] {
+  opacity: 0.6;
+}
+</style>