<template>
|
<view class="page">
|
<view class="custom-nav">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">申请表单</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
</view>
|
|
<view v-if="pageLoading" class="page-state"><view class="loading-spinner"></view><text>正在加载申请资料</text></view>
|
<view v-else-if="pageLoadFailed" class="page-state error-state"><text>申请资料加载失败</text><text class="retry-text" @tap="loadUpgrade">重新加载</text></view>
|
<template v-else-if="!isSubmitted">
|
<scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false">
|
<view class="form-list">
|
<view v-if="rejectReason.length > 0" class="reject-hint"><text>驳回原因:{{rejectReason}}</text></view>
|
<view class="form-group target-group">
|
<text class="field-label">申请升级至</text>
|
<picker v-if="targetLevels.length > 0" class="date-picker" mode="selector" :range="targetLevels" @change="chooseTargetLevel"><view class="picker-box"><text class="picker-value">{{targetCardLevel}}</text><text class="picker-arrow">›</text></view></picker>
|
<text v-else class="target-unavailable">当前已是最高人才卡等级</text>
|
</view>
|
<view class="form-group" v-for="(item, index) in honorForms" :key="item.id" :class="index > 0 ? 'with-divider' : ''">
|
<view class="field-title-row">
|
<text class="field-label">新获得的荣誉称号/大奖奖项</text>
|
<view v-if="index > 0" class="delete-button" hover-class="delete-button-active" @tap="removeHonor(index)"><image src="/static/profile-assets/trash.svg" mode="aspectFit"></image><text>删除</text></view>
|
</view>
|
<view class="input-box"><input :value="item.name" placeholder="请输入" placeholder-class="input-placeholder" @input="updateHonorName($event, index)" /></view>
|
|
<text class="field-label field-spacing">获得时间</text>
|
<picker class="date-picker" mode="date" :value="item.date" @change="chooseDate($event, index)">
|
<view class="picker-box"><text :class="item.date.length > 0 ? 'picker-value' : 'picker-placeholder'">{{item.date.length > 0 ? item.date : '选择时间'}}</text><text class="picker-arrow">›</text></view>
|
</picker>
|
|
<text class="field-label field-spacing">荣誉称号/奖项印证资料</text>
|
<view class="proof-box" hover-class="proof-box-active" @tap="chooseProof(index)">
|
<image v-if="item.proof.length > 0" class="proof-image" :src="item.proof" mode="aspectFill"></image>
|
<image v-else class="proof-plus" src="/static/profile-assets/upload-plus.svg" mode="aspectFit"></image>
|
<text v-if="item.uploadState == 'uploading'" class="upload-status">上传中 {{item.progress}}%</text>
|
<text v-else-if="item.uploadState == 'failed'" class="upload-status retry-status" @tap.stop="retryProof(index)">重试</text>
|
</view>
|
</view>
|
<view class="scroll-bottom"></view>
|
</view>
|
</scroll-view>
|
|
<view class="bottom-actions">
|
<view class="submit-button" hover-class="primary-active" @tap="submitApplication"><text>{{isSubmitting ? '提交中' : '提交'}}</text></view>
|
<view class="add-button" hover-class="secondary-active" @tap="addHonor"><text>添加荣誉/奖项</text></view>
|
</view>
|
</template>
|
|
<view v-else class="success-content">
|
<view class="success-card">
|
<image src="/static/profile-assets/upgrade-success.svg" mode="aspectFit"></image>
|
<text>{{submittedStatus == 'approved' ? '升级申请已通过' : '提交成功,待管理员确认'}}</text>
|
</view>
|
<view class="return-button" hover-class="primary-active" @tap="goBack"><text>返回</text></view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment } from '../../config/api.uts'
|
import { getCardUpgrade, submitCardUpgrade } from '../../services/portal.uts'
|
import { resolveApiUrl } from '../../services/http.uts'
|
import { clearSession, getSession, hasActiveSession } from '../../services/session.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
import { createIdempotencyKey, uploadProofFile } from '../../services/business.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
const honorForms = ref([{ id: 1, name: '', date: '', proof: '', proofFileId: '', uploadState: '', progress: 0, error: '', abort: null }])
|
const nextFormId = ref(2)
|
const talentId = ref('')
|
const targetLevels = ref<string[]>([])
|
const targetCardLevel = ref('')
|
const rejectReason = ref('')
|
const pageLoading = ref(true)
|
const pageLoadFailed = ref(false)
|
const isSubmitted = ref(false)
|
const isSubmitting = ref(false)
|
const submittedStatus = ref('')
|
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const systemInfo = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = systemInfo.windowWidth - capsule.right
|
// #endif
|
const previewMember = apiEnvironment == 'development' && options != null && options['preview'] == 'member'
|
if (previewMember) talentId.value = options['talentId'] != null ? options['talentId'] as string : '2'
|
if (!hasActiveSession() && !previewMember) {
|
clearSession()
|
uni.reLaunch({ url: '/pages/login/login' })
|
return
|
}
|
const session = getSession()
|
if (!previewMember && session != null && session.talentId != null) talentId.value = session.talentId as string
|
loadUpgrade()
|
})
|
|
const draftKey = (): string => 'card-upgrade-draft-' + talentId.value
|
const newHonor = (id: number): any => ({ id: id, name: '', date: '', proof: '', proofFileId: '', uploadState: '', progress: 0, error: '', abort: null })
|
const isApprovedApplication = (status: any): boolean => status == 'approved' || status == '已通过'
|
const saveDraft = () => {
|
if (talentId.value == '' || isSubmitted.value) return
|
const honors: any[] = []
|
for (let index = 0; index < honorForms.value.length; index++) {
|
const item = honorForms.value[index]
|
honors.push({ name: item.name, date: item.date, proof: item.proof, proofFileId: item.proofFileId })
|
}
|
uni.setStorageSync(draftKey(), { targetCardLevel: targetCardLevel.value, honors: honors })
|
}
|
const restoreDraft = () => {
|
const draft: any = uni.getStorageSync(draftKey())
|
if (draft == null || !(draft.honors instanceof Array) || draft.honors.length == 0) return
|
const restored: any[] = []
|
for (let index = 0; index < draft.honors.length; index++) {
|
const source: any = draft.honors[index]
|
const item = newHonor(index + 1)
|
item.name = source.name != null ? source.name as string : ''
|
item.date = source.date != null ? source.date as string : ''
|
item.proof = source.proof != null ? source.proof as string : ''
|
item.proofFileId = source.proofFileId != null ? source.proofFileId as string : ''
|
item.uploadState = item.proofFileId.length > 0 ? 'uploaded' : ''
|
item.progress = item.proofFileId.length > 0 ? 100 : 0
|
restored.push(item)
|
}
|
honorForms.value = restored
|
nextFormId.value = restored.length + 1
|
if (draft.targetCardLevel != null && targetLevels.value.indexOf(draft.targetCardLevel as string) >= 0) targetCardLevel.value = draft.targetCardLevel as string
|
}
|
const applyReturnedHonors = (honors: any[]) => {
|
if (honors.length == 0) return
|
const restored: any[] = []
|
for (let index = 0; index < honors.length; index++) {
|
const source: any = honors[index]
|
const item = newHonor(index + 1)
|
item.name = source.name != null ? source.name as string : ''
|
item.date = source.obtainedDate != null ? source.obtainedDate as string : ''
|
const proofIds: any[] = source.proofFileIds instanceof Array ? source.proofFileIds as any[] : []
|
item.proofFileId = proofIds.length > 0 ? proofIds[0] as string : ''
|
item.proof = item.proofFileId.length > 0 ? resolveApiUrl(item.proofFileId) : ''
|
item.uploadState = item.proofFileId.length > 0 ? 'uploaded' : ''
|
item.progress = item.proofFileId.length > 0 ? 100 : 0
|
restored.push(item)
|
}
|
honorForms.value = restored
|
nextFormId.value = restored.length + 1
|
}
|
const loadUpgrade = () => {
|
pageLoading.value = true
|
pageLoadFailed.value = false
|
isSubmitted.value = false
|
submittedStatus.value = ''
|
rejectReason.value = ''
|
honorForms.value = [newHonor(1)]
|
nextFormId.value = 2
|
getCardUpgrade(talentId.value, (data: any) => {
|
pageLoading.value = false
|
targetLevels.value = data != null && data.targetCardLevels instanceof Array ? data.targetCardLevels as string[] : []
|
targetCardLevel.value = targetLevels.value.length > 0 ? targetLevels.value[0] : ''
|
const application: any = data != null ? data.application : null
|
if (application != null) {
|
rejectReason.value = application.status == 'rejected' && application.rejectReason != null ? application.rejectReason as string : ''
|
if (application.targetCardLevel != null && targetLevels.value.indexOf(application.targetCardLevel as string) >= 0) targetCardLevel.value = application.targetCardLevel as string
|
const honors: any[] = application.honors instanceof Array ? application.honors as any[] : []
|
if (application.status == 'reviewing' || application.status == 'submitted') {
|
isSubmitted.value = true
|
submittedStatus.value = application.status as string
|
return
|
}
|
if (isApprovedApplication(application.status)) {
|
// 已通过的申请资料属于上一轮升级,不能作为下一次升级的草稿回显。
|
uni.removeStorageSync(draftKey())
|
return
|
}
|
applyReturnedHonors(honors)
|
}
|
restoreDraft()
|
}, (error: any) => {
|
pageLoading.value = false
|
if (error != null && error.statusCode == 401) { clearSession(); uni.reLaunch({ url: '/pages/login/login' }); return }
|
pageLoadFailed.value = true
|
})
|
}
|
|
const goBack = () => safeBack('/pages/profile/profile')
|
const addHonor = () => {
|
if (honorForms.value.length >= 5) { uni.showToast({ title: '最多添加 5 组资料', icon: 'none' }); return }
|
const next = honorForms.value.slice()
|
next.push(newHonor(nextFormId.value))
|
nextFormId.value++
|
honorForms.value = next
|
saveDraft()
|
}
|
const removeHonor = (index: number) => {
|
if (honorForms.value.length <= 1) return
|
const next = honorForms.value.slice()
|
if (next[index].abort != null) next[index].abort()
|
next.splice(index, 1)
|
honorForms.value = next
|
saveDraft()
|
}
|
const updateHonorName = (event: any, index: number) => {
|
const next = honorForms.value.slice()
|
next[index].name = event.detail.value as string
|
honorForms.value = next
|
saveDraft()
|
}
|
const chooseDate = (event: any, index: number) => {
|
const next = honorForms.value.slice()
|
next[index].date = event.detail.value as string
|
honorForms.value = next
|
saveDraft()
|
}
|
const chooseTargetLevel = (event: any) => {
|
const index = Number(event.detail.value)
|
if (targetLevels.value[index] != null) targetCardLevel.value = targetLevels.value[index]
|
saveDraft()
|
}
|
const chooseProof = (index: number) => {
|
uni.chooseImage({
|
count: 1,
|
sizeType: ['compressed'],
|
sourceType: ['album', 'camera'],
|
success: (result) => {
|
if (result.tempFilePaths.length == 0) return
|
const next = honorForms.value.slice()
|
next[index].proof = result.tempFilePaths[0]
|
next[index].proofFileId = ''
|
next[index].uploadState = 'uploading'
|
next[index].progress = 0
|
next[index].error = ''
|
honorForms.value = next
|
uploadProof(index, result.tempFilePaths[0] as string)
|
},
|
fail: () => uni.showToast({ title: '图片选择失败,请重新选择', icon: 'none' })
|
})
|
}
|
const uploadProof = (index: number, path: string) => {
|
const abort = uploadProofFile(path, 'cardUpgradeProof', (data: any) => {
|
const next = honorForms.value.slice()
|
if (next[index] == null || next[index].proof != path) return
|
const fileId = data != null && data.fileId != null ? data.fileId as string : ''
|
if (fileId.length == 0) { next[index].uploadState = 'failed'; next[index].error = '上传响应缺少文件 ID'; next[index].abort = null; honorForms.value = next; return }
|
next[index].proofFileId = fileId
|
next[index].uploadState = 'uploaded'
|
next[index].progress = 100
|
next[index].abort = null
|
honorForms.value = next
|
saveDraft()
|
}, (error) => {
|
const next = honorForms.value.slice()
|
if (next[index] == null || next[index].proof != path) return
|
next[index].uploadState = 'failed'
|
next[index].error = error.message
|
next[index].abort = null
|
honorForms.value = next
|
saveDraft()
|
}, (progress) => {
|
const next = honorForms.value.slice()
|
if (next[index] != null && next[index].proof == path) { next[index].progress = progress; honorForms.value = next }
|
})
|
const next = honorForms.value.slice()
|
if (next[index] != null && next[index].proof == path) { next[index].abort = abort; honorForms.value = next }
|
}
|
const retryProof = (index: number) => {
|
const item = honorForms.value[index]
|
if (item == null || item.proof.length == 0) return
|
item.uploadState = 'uploading'
|
item.progress = 0
|
item.error = ''
|
honorForms.value = honorForms.value.slice()
|
uploadProof(index, item.proof as string)
|
}
|
const submitApplication = () => {
|
if (isSubmitting.value) return
|
if (targetCardLevel.value.length == 0) { uni.showToast({ title: '当前暂无可升级的人才卡等级', icon: 'none' }); return }
|
let index = 0
|
while (index < honorForms.value.length) {
|
const item = honorForms.value[index]
|
if (item.name.trim().length == 0) {
|
uni.showToast({ title: '请填写第' + (index + 1) + '组荣誉或奖项', icon: 'none' })
|
return
|
}
|
if (item.date.length == 0) {
|
uni.showToast({ title: '请选择第' + (index + 1) + '组获得时间', icon: 'none' })
|
return
|
}
|
if (item.proof.length == 0) {
|
uni.showToast({ title: '请上传第' + (index + 1) + '组印证资料', icon: 'none' })
|
return
|
}
|
if (item.uploadState != 'uploaded' || item.proofFileId.length == 0) {
|
uni.showToast({ title: item.error != '' ? item.error : '第' + (index + 1) + '组资料尚未上传完成', icon: 'none' })
|
return
|
}
|
index++
|
}
|
isSubmitting.value = true
|
const honors: any[] = []
|
let formIndex = 0
|
while (formIndex < honorForms.value.length) {
|
const item = honorForms.value[formIndex]
|
honors.push({ name: item.name, obtainedDate: item.date, proofFileIds: [item.proofFileId] })
|
formIndex++
|
}
|
submitCardUpgrade({ talentId: Number(talentId.value), targetCardLevel: targetCardLevel.value, honors: honors }, createIdempotencyKey('card-upgrade'), (data: any) => {
|
isSubmitted.value = true
|
submittedStatus.value = data != null && data.status != null ? data.status as string : 'reviewing'
|
uni.removeStorageSync(draftKey())
|
}, (error) => {
|
isSubmitting.value = false
|
saveDraft()
|
uni.showToast({ title: error.message.length > 0 ? error.message : '提交失败,请稍后重试', icon: 'none' })
|
})
|
}
|
</script>
|
|
<style>
|
.page { display: flex; width: 100%; height: 100vh; background-color: #F5F7FA; flex-direction: column; }
|
.custom-nav { position: relative; z-index: 10; flex-shrink: 0; padding-bottom: 8rpx; background-color: #FFF0D7; }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 36rpx; height: 36rpx; }
|
.back-button-active { opacity: .55; }
|
.nav-title { position: absolute; left: 50%; top: 50%; color: #151922; font-size: 38rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
|
.capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
|
.form-scroll { min-height: 0; flex: 1; background-color: #F5F7FA; }
|
.form-list { padding-top: 28rpx; }
|
.page-state { display: flex; min-height: 0; flex: 1; color: #717A87; font-size: 26rpx; flex-direction: row; justify-content: center; align-items: center; }
|
.page-state.error-state { flex-direction: column; }
|
.loading-spinner { width: 34rpx; height: 34rpx; margin-right: 14rpx; box-sizing: border-box; border: 4rpx solid #FFD9C9; border-top-color: #FF6C2B; border-radius: 50%; animation: upgrade-loading 720ms linear infinite; }
|
.retry-text { margin-top: 22rpx; color: #F06D31; }
|
@keyframes upgrade-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
.form-group { box-sizing: border-box; padding: 0 34rpx 40rpx; }
|
.target-group { padding-bottom: 34rpx; }
|
.target-unavailable { display: block; margin-top: 18rpx; color: #9CA4AE; font-size: 26rpx; }
|
.reject-hint { margin: 0 34rpx 34rpx; box-sizing: border-box; padding: 20rpx 24rpx; border-radius: 14rpx; background-color: #FFF0EC; }
|
.reject-hint text { color: #D96040; font-size: 24rpx; line-height: 36rpx; }
|
.form-group.with-divider { padding-top: 50rpx; border-top-width: 20rpx; border-top-style: solid; border-top-color: #E9E9E9; }
|
.field-title-row { display: flex; min-height: 48rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
.field-label { color: #727B89; font-size: 27rpx; line-height: 40rpx; font-weight: 500; }
|
.delete-button { display: flex; height: 52rpx; padding: 0 13rpx; border-radius: 26rpx; background-color: #FFF0EC; flex-direction: row; align-items: center; }
|
.delete-button image { width: 28rpx; height: 28rpx; margin-right: 7rpx; }
|
.delete-button text { color: #FF6030; font-size: 22rpx; }
|
.delete-button-active { opacity: .62; }
|
.input-box { display: flex; height: 110rpx; margin-top: 15rpx; box-sizing: border-box; padding: 0 26rpx; border-radius: 25rpx; background-color: #FFFFFF; align-items: center; }
|
.input-box input { width: 100%; height: 78rpx; color: #262A31; font-size: 28rpx; }
|
.input-placeholder { color: #C5CBD4; font-size: 28rpx; }
|
.field-spacing { display: block; margin-top: 52rpx; }
|
.date-picker { margin-top: 15rpx; }
|
.picker-box { display: flex; height: 110rpx; box-sizing: border-box; padding: 0 26rpx; border-radius: 25rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; justify-content: space-between; }
|
.picker-placeholder { color: #C5CBD4; font-size: 28rpx; }
|
.picker-value { color: #262A31; font-size: 28rpx; }
|
.picker-arrow { color: #8C96A3; font-size: 42rpx; line-height: 48rpx; }
|
.proof-box { display: flex; width: 190rpx; height: 190rpx; margin-top: 18rpx; position: relative; overflow: hidden; border-radius: 25rpx; background-color: #FFFFFF; justify-content: center; align-items: center; }
|
.proof-box-active { opacity: .7; }
|
.proof-plus { width: 48rpx; height: 48rpx; }
|
.proof-image { width: 190rpx; height: 190rpx; }
|
.upload-status { position: absolute; z-index: 2; left: 12rpx; bottom: 12rpx; padding: 5rpx 9rpx; border-radius: 8rpx; background-color: rgba(0,0,0,.58); color: #FFFFFF; font-size: 19rpx; }
|
.retry-status { background-color: #FF6B34; }
|
.scroll-bottom { height: 20rpx; }
|
.bottom-actions { flex-shrink: 0; box-sizing: border-box; padding: 22rpx 36rpx; padding-bottom: calc(22rpx + env(safe-area-inset-bottom)); background-color: #F5F7FA; }
|
.submit-button, .return-button { display: flex; height: 102rpx; border-radius: 52rpx; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); justify-content: center; align-items: center; }
|
.submit-button text, .return-button text { color: #FFFFFF; font-size: 29rpx; font-weight: 700; }
|
.add-button { display: flex; height: 96rpx; margin-top: 20rpx; box-sizing: border-box; border-width: 2rpx; border-style: solid; border-color: #FF701E; border-radius: 50rpx; justify-content: center; align-items: center; }
|
.add-button text { color: #FF701E; font-size: 25rpx; font-weight: 500; }
|
.primary-active, .secondary-active { opacity: .72; }
|
.success-content { min-height: 0; flex: 1; box-sizing: border-box; padding: 34rpx 68rpx calc(40rpx + env(safe-area-inset-bottom)); background-color: #F5F7FA; }
|
.success-card { display: flex; height: 388rpx; border-radius: 25rpx; background-color: #FFFFFF; flex-direction: column; justify-content: center; align-items: center; }
|
.success-card image { width: 122rpx; height: 122rpx; }
|
.success-card text { margin-top: 58rpx; color: #181D26; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
|
.return-button { height: 84rpx; margin-top: 40rpx; }
|
</style>
|