release: freeze demo v1.1.0 2026-10-19
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<PageLayout title="基础资料">
|
||||
<div class="pb">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
<template v-else>
|
||||
<div class="card">
|
||||
<UiSectionHeader title="基本信息" />
|
||||
<p class="pb__hint">由实名认证自动生成</p>
|
||||
<div class="pb__readonly">
|
||||
<div class="pb__row">
|
||||
<span class="pb__row-label">性别</span>
|
||||
<span class="pb__row-value">{{ genderLabel || '未认证' }}</span>
|
||||
</div>
|
||||
<div class="pb__row">
|
||||
<span class="pb__row-label">年龄</span>
|
||||
<span class="pb__row-value">{{ profile?.age ? `${profile.age}岁` : '未认证' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="个人资料" />
|
||||
<p class="pb__hint">请如实填写</p>
|
||||
<van-field
|
||||
v-model="regionLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="居住区域"
|
||||
placeholder="请选择"
|
||||
:border="false"
|
||||
@click="openPicker('region')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.heightCm"
|
||||
type="digit"
|
||||
label="身高(cm)"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
<van-field
|
||||
v-model="maritalLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="婚姻状况"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('marital')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="childrenLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="子女情况"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('children')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="educationLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="学历"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('education')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="occupationLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="职业类型"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('occupation')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="pb__actions">
|
||||
<van-button type="primary" block round :loading="saving" :disabled="loading" @click="onNext">
|
||||
下一步
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="picker.show" position="bottom" round>
|
||||
<van-picker
|
||||
:columns="picker.columns"
|
||||
@cancel="picker.show = false"
|
||||
@confirm="onPickerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const profile = ref<any>(null)
|
||||
|
||||
const form = reactive({
|
||||
regionName: '',
|
||||
heightCm: '',
|
||||
maritalStatus: '',
|
||||
childrenStatus: '',
|
||||
educationCode: '',
|
||||
occupationCode: '',
|
||||
})
|
||||
|
||||
const REGION_OPTIONS = [
|
||||
{ text: '丹阳', value: '丹阳' },
|
||||
{ text: '镇江市区', value: '镇江市区' },
|
||||
{ text: '镇江其他辖区', value: '镇江其他辖区' },
|
||||
{ text: '南京', value: '南京' },
|
||||
]
|
||||
const REGION_CODES: Record<string, string> = { 丹阳: '321181', 镇江市区: '321100', 南京: '320100' }
|
||||
const MARITAL_OPTIONS = [
|
||||
{ text: '未婚', value: 'never_married' },
|
||||
{ text: '离异', value: 'divorced' },
|
||||
{ text: '丧偶', value: 'widowed' },
|
||||
]
|
||||
const CHILDREN_OPTIONS = [
|
||||
{ text: '无子女', value: 'none' },
|
||||
{ text: '有子女', value: 'yes' },
|
||||
]
|
||||
const EDUCATION_OPTIONS = [
|
||||
{ text: '高中/中专', value: 'senior_high' },
|
||||
{ text: '大专', value: 'college' },
|
||||
{ text: '本科', value: 'bachelor' },
|
||||
{ text: '硕士', value: 'master' },
|
||||
{ text: '博士', value: 'doctor' },
|
||||
]
|
||||
const OCCUPATION_OPTIONS = [
|
||||
{ text: '机关事业单位', value: 'government' },
|
||||
{ text: '教育', value: 'education' },
|
||||
{ text: '医疗', value: 'medical' },
|
||||
{ text: '企业职员', value: 'enterprise' },
|
||||
{ text: '技术人员', value: 'technical' },
|
||||
{ text: '个体经营', value: 'self_employed' },
|
||||
{ text: '自由职业', value: 'freelance' },
|
||||
{ text: '服务行业', value: 'service' },
|
||||
{ text: '农业', value: 'agriculture' },
|
||||
{ text: '其他', value: 'other' },
|
||||
]
|
||||
|
||||
function labelOf(options: Array<{ text: string; value: string }>, value: string): string {
|
||||
return options.find((o) => o.value === value)?.text || ''
|
||||
}
|
||||
|
||||
const regionLabel = computed(() => labelOf(REGION_OPTIONS, form.regionName))
|
||||
const maritalLabel = computed(() => labelOf(MARITAL_OPTIONS, form.maritalStatus))
|
||||
const childrenLabel = computed(() => labelOf(CHILDREN_OPTIONS, form.childrenStatus))
|
||||
const educationLabel = computed(() => labelOf(EDUCATION_OPTIONS, form.educationCode))
|
||||
const occupationLabel = computed(() => labelOf(OCCUPATION_OPTIONS, form.occupationCode))
|
||||
const genderLabel = computed(() => {
|
||||
const g = profile.value?.gender
|
||||
return g === 'male' ? '男' : g === 'female' ? '女' : ''
|
||||
})
|
||||
|
||||
const picker = reactive<{ show: boolean; key: string; columns: any[] }>({ show: false, key: '', columns: [] })
|
||||
const PICKER_COLUMNS: Record<string, any[]> = {
|
||||
region: REGION_OPTIONS,
|
||||
marital: MARITAL_OPTIONS,
|
||||
children: CHILDREN_OPTIONS,
|
||||
education: EDUCATION_OPTIONS,
|
||||
occupation: OCCUPATION_OPTIONS,
|
||||
}
|
||||
|
||||
function openPicker(key: string) {
|
||||
picker.key = key
|
||||
picker.columns = PICKER_COLUMNS[key]
|
||||
picker.show = true
|
||||
}
|
||||
|
||||
function onPickerConfirm({ selectedOptions }: any) {
|
||||
const value = selectedOptions?.[0]?.value
|
||||
const key = picker.key
|
||||
picker.show = false
|
||||
if (value === undefined) return
|
||||
if (key === 'region') form.regionName = value
|
||||
else if (key === 'marital') form.maritalStatus = value
|
||||
else if (key === 'children') form.childrenStatus = value
|
||||
else if (key === 'education') form.educationCode = value
|
||||
else if (key === 'occupation') form.occupationCode = value
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getProfile()
|
||||
profile.value = p
|
||||
form.regionName = p?.regionName || ''
|
||||
form.heightCm = p?.heightCm ? String(p.heightCm) : ''
|
||||
form.maritalStatus = p?.maritalStatus || ''
|
||||
form.childrenStatus = p?.childrenStatus || ''
|
||||
form.educationCode = p?.educationCode || ''
|
||||
form.occupationCode = p?.occupationCode || ''
|
||||
} catch {
|
||||
/* 首次填写无资料 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
if (!form.regionName) return appStore.toast('请选择居住区域')
|
||||
if (!form.heightCm) return appStore.toast('请填写身高')
|
||||
if (!form.maritalStatus) return appStore.toast('请选择婚姻状况')
|
||||
if (!form.childrenStatus) return appStore.toast('请选择子女情况')
|
||||
if (!form.educationCode) return appStore.toast('请选择学历')
|
||||
if (!form.occupationCode) return appStore.toast('请选择职业类型')
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updateBasicProfile({
|
||||
regionName: form.regionName,
|
||||
regionCode: REGION_CODES[form.regionName] || null,
|
||||
heightCm: Number(form.heightCm),
|
||||
maritalStatus: form.maritalStatus,
|
||||
childrenStatus: form.childrenStatus,
|
||||
educationCode: form.educationCode,
|
||||
occupationCode: form.occupationCode,
|
||||
})
|
||||
router.push('/profile/lifestyle')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pb__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pb {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 0 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card :deep(.ui-section-header) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.card :deep(.van-field) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.pb__readonly {
|
||||
padding: 4px 16px 4px;
|
||||
}
|
||||
.pb__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--mini-border-weak, #f2f3f5);
|
||||
}
|
||||
.pb__row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.pb__row-label {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pb__row-value {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text-strong);
|
||||
}
|
||||
.pb__actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<PageLayout title="资料确认">
|
||||
<div class="pc">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
<UiErrorState v-else-if="error" @retry="loadAll" />
|
||||
|
||||
<template v-else-if="profile">
|
||||
<!-- 审核状态 -->
|
||||
<div class="card">
|
||||
<div class="pc__status-row">
|
||||
<span class="pc__status-label">当前审核状态</span>
|
||||
<UiTag :type="reviewTag">{{ reviewLabel }}</UiTag>
|
||||
</div>
|
||||
<div v-if="reviewComment" class="pc__comment">
|
||||
<span class="pc__comment-title">审核反馈:</span>
|
||||
<span class="pc__comment-text">{{ reviewComment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整度 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="资料完整度" />
|
||||
<p class="pc__hint">{{ completion }}%</p>
|
||||
<div class="pc__progress">
|
||||
<div class="pc__progress-track">
|
||||
<div class="pc__progress-fill" :style="{ width: completion + '%' }"></div>
|
||||
</div>
|
||||
<div class="pc__progress-hint">
|
||||
{{ completion >= 100 ? '资料已完整,可提交审核' : '部分资料尚未填写,提交前请确认' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资料概览 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="资料概览" />
|
||||
<p class="pc__hint">只读展示,点选对应项可修改</p>
|
||||
<van-cell-group :border="false">
|
||||
<van-cell title="昵称" :value="profile.displayName || '未设置'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="年龄" :value="profile.age ? profile.age + '岁' : '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="身高" :value="profile.heightCm ? profile.heightCm + 'cm' : '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="婚姻状况" :value="maritalLabel || '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="子女情况" :value="childrenLabel || '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="学历" :value="educationLabel || '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="居住区域" :value="profile.regionName || '-'" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="生活照" :value="`${photoCount}张`" is-link @click="router.push('/profile/photos')" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<!-- 自我介绍 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="自我介绍" more-text="修改" @more="router.push('/profile/intro')" />
|
||||
<div class="pc__intro">{{ profile.selfIntro || '暂未填写' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 择偶条件 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="择偶条件" more-text="修改" @more="router.push('/profile/preference')" />
|
||||
<div v-if="preferences">
|
||||
<div class="pc__row">
|
||||
<span class="pc__row-label">年龄范围</span>
|
||||
<span class="pc__row-value">{{ preferences.minAge || '-' }} ~ {{ preferences.maxAge || '-' }}岁</span>
|
||||
</div>
|
||||
<div class="pc__row">
|
||||
<span class="pc__row-label">期望婚姻状况</span>
|
||||
<span class="pc__row-value">
|
||||
{{ (preferences.maritalStatuses || []).map((c: string) => maritalText(c)).join('、') || '不限' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pc__row">
|
||||
<span class="pc__row-label">最低学历</span>
|
||||
<span class="pc__row-value">{{ educationText(preferences.minEducationCode) || '不限' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<UiEmpty v-else description="尚未填写择偶条件" />
|
||||
</div>
|
||||
|
||||
<!-- 提交须知 -->
|
||||
<div class="pc__notice">
|
||||
<UiSectionHeader title="提交前请确认" />
|
||||
<div class="pc__notice-item">· 提交后资料将进入人工审核</div>
|
||||
<div class="pc__notice-item">· 审核期间你可继续修改,但需重新提交</div>
|
||||
<div class="pc__notice-item">· 审核通过后即可开始匹配</div>
|
||||
<div class="pc__notice-item">· 资料须真实有效,禁止虚假信息</div>
|
||||
</div>
|
||||
|
||||
<div class="pc__actions">
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
:loading="submitting"
|
||||
:disabled="!resubmitAllowed"
|
||||
@click="onSubmit"
|
||||
>
|
||||
{{ isPending ? '审核中,不可重复提交' : '提交审核' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UiEmpty v-else description="资料加载失败,请重试" />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showConfirmDialog } from 'vant'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiEmpty from '@/components/UiEmpty.vue'
|
||||
import UiErrorState from '@/components/UiErrorState.vue'
|
||||
import UiTag from '@/components/UiTag.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const submitting = ref(false)
|
||||
const profile = ref<any>(null)
|
||||
const preferences = ref<any>(null)
|
||||
const photoCount = ref(0)
|
||||
const completion = ref(0)
|
||||
const reviewStatus = ref('not_submitted')
|
||||
const reviewComment = ref('')
|
||||
|
||||
const MARITAL: Record<string, string> = { never_married: '未婚', divorced: '离异', widowed: '丧偶', single: '未婚' }
|
||||
const CHILDREN: Record<string, string> = { none: '无子女', has: '有子女', yes: '有子女' }
|
||||
const EDUCATION: Record<string, string> = {
|
||||
junior_high: '初中',
|
||||
senior_high: '高中/中专',
|
||||
high_school: '高中/中专',
|
||||
college: '大专',
|
||||
bachelor: '本科',
|
||||
master: '硕士',
|
||||
doctor: '博士',
|
||||
}
|
||||
const REVIEW_TEXT: Record<string, string> = {
|
||||
not_submitted: '未提交',
|
||||
pending: '审核中',
|
||||
pending_review: '审核中',
|
||||
approved: '已通过',
|
||||
need_edit: '需修改',
|
||||
rejected: '未通过',
|
||||
}
|
||||
|
||||
function maritalText(code: string): string {
|
||||
return MARITAL[code] || code
|
||||
}
|
||||
function educationText(code?: string): string {
|
||||
if (!code) return ''
|
||||
return EDUCATION[code] || code
|
||||
}
|
||||
|
||||
const maritalLabel = computed(() => MARITAL[profile.value?.maritalStatus] || '')
|
||||
const childrenLabel = computed(() => CHILDREN[profile.value?.childrenStatus] || '')
|
||||
const educationLabel = computed(() => educationText(profile.value?.educationCode))
|
||||
const isPending = computed(() => reviewStatus.value === 'pending' || reviewStatus.value === 'pending_review')
|
||||
const reviewLabel = computed(() => REVIEW_TEXT[reviewStatus.value] || '未提交')
|
||||
const reviewTag = computed<'primary' | 'success' | 'warning' | 'danger' | 'info'>(() => {
|
||||
if (reviewStatus.value === 'approved') return 'success'
|
||||
if (reviewStatus.value === 'need_edit' || isPending.value) return 'warning'
|
||||
if (reviewStatus.value === 'rejected') return 'danger'
|
||||
return 'info'
|
||||
})
|
||||
const resubmitAllowed = computed(
|
||||
() => reviewStatus.value === 'not_submitted' || reviewStatus.value === 'need_edit' || reviewStatus.value === 'rejected',
|
||||
)
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const [p, prefs, review, photosRes, comp] = await Promise.all([
|
||||
profileService.getProfile().catch(() => null),
|
||||
profileService.getPreferences().catch(() => null),
|
||||
profileService.getReviewStatus().catch(() => null),
|
||||
profileService.getPhotos().catch(() => ({ photos: [] })),
|
||||
profileService.getCompletion().catch(() => null),
|
||||
])
|
||||
profile.value = p
|
||||
preferences.value = prefs
|
||||
const photos = (photosRes as any)?.photos || (photosRes as any)?.items || (photosRes as any)?.records || []
|
||||
photoCount.value = photos.length
|
||||
reviewStatus.value = (review as any)?.status || 'not_submitted'
|
||||
const items = (review as any)?.items || []
|
||||
reviewComment.value = items
|
||||
.filter((i: any) => i.status === 'need_edit' || i.status === 'rejected')
|
||||
.map((i: any) => i.reasonText)
|
||||
.filter(Boolean)
|
||||
.join(';')
|
||||
const c = comp as any
|
||||
const raw = c?.completion ?? c?.profileCompletion ?? (p as any)?.profileCompletion ?? 0
|
||||
completion.value = Math.max(0, Math.min(100, Math.round(Number(raw) || 0)))
|
||||
if (!p) error.value = true
|
||||
} catch {
|
||||
error.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (submitting.value || !resubmitAllowed.value) return
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '确认提交审核?',
|
||||
message: '提交后资料将进入人工审核,审核期间你可继续修改,但需重新提交审核。',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await profileService.submitReview()
|
||||
appStore.toastSuccess('已提交审核')
|
||||
router.replace('/review/pending')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pc__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pc {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 16px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card :deep(.van-cell) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.pc__status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
.pc__status-label {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pc__comment {
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(245, 166, 35, 0.1);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--mini-text);
|
||||
}
|
||||
.pc__comment-title {
|
||||
color: #b9770e;
|
||||
}
|
||||
.pc__progress {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pc__progress-track {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--mini-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.pc__progress-fill {
|
||||
height: 100%;
|
||||
background: var(--mini-accent-pink);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.pc__progress-hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.pc__intro {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text);
|
||||
line-height: 1.8;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.pc__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.pc__row-label {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pc__row-value {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text);
|
||||
text-align: right;
|
||||
}
|
||||
.pc__notice-item {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.pc__actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<PageLayout title="婚恋态度">
|
||||
<div class="pt">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="婚恋态度" />
|
||||
<p class="pt__hint">让我们更了解你的期待</p>
|
||||
<div class="pt__field pt__field--column">
|
||||
<div class="pt__label">当前婚恋目标</div>
|
||||
<textarea
|
||||
v-model="form.relationshipGoal"
|
||||
class="pt__textarea"
|
||||
maxlength="100"
|
||||
placeholder="寻找以婚姻为目标的稳定关系"
|
||||
/>
|
||||
</div>
|
||||
<van-field
|
||||
v-model="marriagePlanLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="希望多久进入婚姻"
|
||||
placeholder="请选择"
|
||||
:border="false"
|
||||
@click="openPicker('marriagePlan')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="activityLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="参加公益相亲活动"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('activity')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="matchmakerLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="接受公益红娘推荐"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('matchmaker')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pt__actions">
|
||||
<van-button type="primary" block round :loading="saving" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="picker.show" position="bottom" round>
|
||||
<van-picker :columns="picker.columns" @cancel="picker.show = false" @confirm="onPickerConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const DEFAULT_GOAL = '寻找以婚姻为目标的稳定关系'
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
relationshipGoal: DEFAULT_GOAL,
|
||||
marriagePlan: '',
|
||||
activityWillingness: '',
|
||||
matchmakerWillingness: '',
|
||||
})
|
||||
|
||||
const MARRIAGE_PLAN_OPTIONS = [
|
||||
{ text: '半年内', value: 'half_year' },
|
||||
{ text: '1年内', value: 'one_year' },
|
||||
{ text: '1~2年', value: 'one_to_two_years' },
|
||||
{ text: '2年以上', value: 'two_years_plus' },
|
||||
{ text: '顺其自然', value: 'let_it_be' },
|
||||
]
|
||||
const ACTIVITY_OPTIONS = [
|
||||
{ text: '愿意', value: 'willing' },
|
||||
{ text: '暂不考虑', value: 'not_now' },
|
||||
]
|
||||
const MATCHMAKER_OPTIONS = [
|
||||
{ text: '接受', value: 'accept' },
|
||||
{ text: '暂不接受', value: 'not_now' },
|
||||
]
|
||||
|
||||
function labelOf(options: Array<{ text: string; value: string }>, value: string): string {
|
||||
return options.find((o) => o.value === value)?.text || ''
|
||||
}
|
||||
|
||||
const marriagePlanLabel = computed(() => labelOf(MARRIAGE_PLAN_OPTIONS, form.marriagePlan))
|
||||
const activityLabel = computed(() => labelOf(ACTIVITY_OPTIONS, form.activityWillingness))
|
||||
const matchmakerLabel = computed(() => labelOf(MATCHMAKER_OPTIONS, form.matchmakerWillingness))
|
||||
|
||||
const picker = reactive<{ show: boolean; key: string; columns: any[] }>({ show: false, key: '', columns: [] })
|
||||
const PICKER_COLUMNS: Record<string, any[]> = {
|
||||
marriagePlan: MARRIAGE_PLAN_OPTIONS,
|
||||
activity: ACTIVITY_OPTIONS,
|
||||
matchmaker: MATCHMAKER_OPTIONS,
|
||||
}
|
||||
|
||||
function openPicker(key: string) {
|
||||
picker.key = key
|
||||
picker.columns = PICKER_COLUMNS[key]
|
||||
picker.show = true
|
||||
}
|
||||
|
||||
function onPickerConfirm({ selectedOptions }: any) {
|
||||
const value = selectedOptions?.[0]?.value
|
||||
const key = picker.key
|
||||
picker.show = false
|
||||
if (value === undefined) return
|
||||
if (key === 'marriagePlan') form.marriagePlan = value
|
||||
else if (key === 'activity') form.activityWillingness = value
|
||||
else if (key === 'matchmaker') form.matchmakerWillingness = value
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getProfile()
|
||||
form.relationshipGoal = p?.relationshipGoal || DEFAULT_GOAL
|
||||
form.marriagePlan = p?.marriagePlan || ''
|
||||
form.activityWillingness = p?.activityWillingness || ''
|
||||
form.matchmakerWillingness = p?.matchmakerWillingness || ''
|
||||
} catch {
|
||||
/* 首次填写 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
if (!form.marriagePlan) return appStore.toast('请选择希望多久进入婚姻')
|
||||
if (!form.activityWillingness) return appStore.toast('请选择是否参加公益活动')
|
||||
if (!form.matchmakerWillingness) return appStore.toast('请选择是否接受红娘推荐')
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updateProfile({
|
||||
relationshipGoal: form.relationshipGoal,
|
||||
marriagePlan: form.marriagePlan,
|
||||
activityWillingness: form.activityWillingness,
|
||||
matchmakerWillingness: form.matchmakerWillingness,
|
||||
})
|
||||
router.push('/profile/intro')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pt__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pt {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.card :deep(.ui-section-header) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.card :deep(.van-field) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.pt__field--column {
|
||||
padding: 8px 16px 4px;
|
||||
}
|
||||
.pt__label {
|
||||
font-size: 14px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pt__textarea {
|
||||
width: 100%;
|
||||
min-height: 84px;
|
||||
border: 1px solid var(--mini-border-weak, #f2f3f5);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: var(--mini-text);
|
||||
background: var(--mini-bg);
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.pt__actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<PageLayout title="兴趣与生活方式">
|
||||
<div class="pi">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="兴趣" />
|
||||
<p class="pi__hint">最多选择 {{ MAX_TAGS }} 项({{ selectedCount }}/{{ MAX_TAGS }})</p>
|
||||
<div class="pi__tags">
|
||||
<span
|
||||
v-for="t in interests"
|
||||
:key="t.code"
|
||||
class="pi__tag"
|
||||
:class="{ 'pi__tag--selected': t.selected }"
|
||||
@click="toggleInterest(t)"
|
||||
>
|
||||
{{ t.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="生活方式" />
|
||||
<p class="pi__hint">可选,多选</p>
|
||||
<div class="pi__tags">
|
||||
<span
|
||||
v-for="t in lifestyles"
|
||||
:key="t.code"
|
||||
class="pi__tag"
|
||||
:class="{ 'pi__tag--selected': t.selected }"
|
||||
@click="t.selected = !t.selected"
|
||||
>
|
||||
{{ t.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pi__actions">
|
||||
<van-button type="primary" block round :loading="saving" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const MAX_TAGS = 8
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const interests = ref([
|
||||
{ code: 'travel', name: '旅行', selected: false },
|
||||
{ code: 'reading', name: '阅读', selected: false },
|
||||
{ code: 'sports', name: '运动', selected: false },
|
||||
{ code: 'fitness', name: '健身', selected: false },
|
||||
{ code: 'photography', name: '摄影', selected: false },
|
||||
{ code: 'movie', name: '电影', selected: false },
|
||||
{ code: 'music', name: '音乐', selected: false },
|
||||
{ code: 'food', name: '美食', selected: false },
|
||||
{ code: 'cooking', name: '烹饪', selected: false },
|
||||
{ code: 'outdoor', name: '户外', selected: false },
|
||||
{ code: 'pets', name: '宠物', selected: false },
|
||||
{ code: 'games', name: '游戏', selected: false },
|
||||
{ code: 'culture', name: '传统文化', selected: false },
|
||||
{ code: 'charity', name: '公益活动', selected: false },
|
||||
{ code: 'other', name: '其他', selected: false },
|
||||
])
|
||||
const lifestyles = ref([
|
||||
{ code: 'quiet', name: '喜欢安静', selected: false },
|
||||
{ code: 'social', name: '喜欢社交', selected: false },
|
||||
{ code: 'outdoor', name: '喜欢户外', selected: false },
|
||||
{ code: 'homebody', name: '居家型', selected: false },
|
||||
{ code: 'regular_work', name: '工作规律', selected: false },
|
||||
{ code: 'frequent_travel', name: '经常出差', selected: false },
|
||||
{ code: 'family_oriented', name: '注重家庭', selected: false },
|
||||
{ code: 'other', name: '其他', selected: false },
|
||||
])
|
||||
|
||||
const selectedCount = computed(() => interests.value.filter((o) => o.selected).length)
|
||||
|
||||
function toggleInterest(t: { code: string; name: string; selected: boolean }) {
|
||||
if (!t.selected && selectedCount.value >= MAX_TAGS) {
|
||||
appStore.toast(`最多选择 ${MAX_TAGS} 个兴趣`)
|
||||
return
|
||||
}
|
||||
t.selected = !t.selected
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getProfile()
|
||||
const savedInterests: string[] = p?.interests || []
|
||||
const savedLifestyles: string[] = p?.lifestyles || []
|
||||
interests.value = interests.value.map((o) => ({ ...o, selected: savedInterests.includes(o.code) }))
|
||||
lifestyles.value = lifestyles.value.map((o) => ({ ...o, selected: savedLifestyles.includes(o.code) }))
|
||||
} catch {
|
||||
/* 首次填写 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updateProfile({
|
||||
interests: interests.value.filter((o) => o.selected).map((o) => o.code),
|
||||
lifestyles: lifestyles.value.filter((o) => o.selected).map((o) => o.code),
|
||||
})
|
||||
router.push('/profile/intent')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pi__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pi {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 16px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pi__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pi__tag {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text);
|
||||
background: var(--mini-bg);
|
||||
border-radius: 16px;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.pi__tag--selected {
|
||||
color: var(--mini-tabbar-active);
|
||||
background: rgba(244, 0, 90, 0.08);
|
||||
border-color: var(--mini-tabbar-active);
|
||||
}
|
||||
.pi__actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<PageLayout title="个人介绍">
|
||||
<div class="pri">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="简单介绍一下自己" />
|
||||
<p class="pri__hint">{{ MIN_LEN }}~{{ MAX_LEN }} 字</p>
|
||||
<div class="pri__wrap">
|
||||
<textarea
|
||||
v-model="selfIntro"
|
||||
class="pri__textarea"
|
||||
:maxlength="MAX_LEN"
|
||||
placeholder="可以介绍工作、性格、兴趣、生活状态,以及你期待怎样的感情"
|
||||
/>
|
||||
<div class="pri__counter">{{ selfIntro.length }}/{{ MAX_LEN }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="填写须知" />
|
||||
<div class="pri__guidelines">
|
||||
<div v-for="(g, i) in guidelines" :key="i" class="pri__guideline">
|
||||
<span class="pri__dot"></span>
|
||||
<span class="pri__guideline-text">{{ g }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isWizard" class="card">
|
||||
<UiSectionHeader title="其他资料入口" />
|
||||
<p class="pri__hint">点选可跳转对应步骤修改</p>
|
||||
<van-cell-group :border="false">
|
||||
<van-cell title="基础资料" is-link @click="router.push('/profile/basic')" />
|
||||
<van-cell title="职业与生活" is-link @click="router.push('/profile/lifestyle')" />
|
||||
<van-cell title="头像与照片" is-link @click="router.push('/profile/photos')" />
|
||||
<van-cell title="兴趣与生活方式" is-link @click="router.push('/profile/interests')" />
|
||||
<van-cell title="婚恋态度" is-link @click="router.push('/profile/intent')" />
|
||||
<van-cell title="择偶条件" is-link @click="router.push('/profile/preference')" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<div class="pri__actions">
|
||||
<van-button type="primary" block round :loading="saving" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const MIN_LEN = 20
|
||||
const MAX_LEN = 500
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const selfIntro = ref('')
|
||||
const guidelines = [
|
||||
'可以介绍工作、性格、兴趣、生活状态',
|
||||
'以及你期待怎样的感情',
|
||||
'禁止填写手机号、微信号、二维码',
|
||||
'禁止广告、色情低俗、投资引流内容',
|
||||
]
|
||||
|
||||
// 带 step=1 进入时为向导模式(隐藏资料入口),否则作为「我的资料」入口展示
|
||||
const isWizard = computed(() => route.query.step === '1')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getProfile()
|
||||
selfIntro.value = p?.selfIntro || ''
|
||||
} catch {
|
||||
/* 首次填写 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
const text = selfIntro.value.trim()
|
||||
if (text.length < MIN_LEN) return appStore.toast(`至少填写 ${MIN_LEN} 字`)
|
||||
if (text.length > MAX_LEN) return appStore.toast(`最多填写 ${MAX_LEN} 字`)
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updateProfile({ selfIntro: text })
|
||||
router.push('/profile/preference')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pri__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pri {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 16px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pri__wrap {
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pri__textarea {
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
border: 1px solid var(--mini-border-weak, #f2f3f5);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--mini-text);
|
||||
background: var(--mini-bg);
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.pri__counter {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.pri__guidelines {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pri__guideline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.pri__dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--mini-tabbar-active);
|
||||
flex: none;
|
||||
}
|
||||
.pri__guideline-text {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.card :deep(.van-cell) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.pri__actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<PageLayout title="职业与生活">
|
||||
<div class="pl">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="职业与生活" />
|
||||
<p class="pl__hint">完善你的生活状态</p>
|
||||
<van-field
|
||||
v-model="occupationLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="职业类别"
|
||||
placeholder="请选择"
|
||||
:border="false"
|
||||
@click="openPicker('occupation')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="workRegionLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="工作区域"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('workRegion')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="smokingLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否吸烟"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('smoking')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="drinkingLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否饮酒"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('drinking')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="longTermLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否长期居住丹阳"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('longTerm')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="futureLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="未来是否愿意继续在丹阳"
|
||||
placeholder="请选择"
|
||||
@click="openPicker('future')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pl__actions">
|
||||
<van-button type="primary" block round :loading="saving" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="picker.show" position="bottom" round>
|
||||
<van-picker :columns="picker.columns" @cancel="picker.show = false" @confirm="onPickerConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
occupationCode: '',
|
||||
workRegionCode: '',
|
||||
smokingStatus: '',
|
||||
drinkingStatus: '',
|
||||
longTermDanyang: '',
|
||||
futureDanyang: '',
|
||||
})
|
||||
|
||||
const OCCUPATION_OPTIONS = [
|
||||
{ text: '机关事业单位', value: 'government' },
|
||||
{ text: '教育', value: 'education' },
|
||||
{ text: '医疗', value: 'medical' },
|
||||
{ text: '企业职员', value: 'enterprise' },
|
||||
{ text: '技术人员', value: 'technical' },
|
||||
{ text: '个体经营', value: 'self_employed' },
|
||||
{ text: '自由职业', value: 'freelance' },
|
||||
{ text: '服务行业', value: 'service' },
|
||||
{ text: '农业', value: 'agriculture' },
|
||||
{ text: '其他', value: 'other' },
|
||||
]
|
||||
const WORK_REGION_OPTIONS = [
|
||||
{ text: '丹阳', value: '丹阳' },
|
||||
{ text: '镇江市区', value: '镇江市区' },
|
||||
{ text: '镇江其他辖区', value: '镇江其他辖区' },
|
||||
{ text: '南京', value: '南京' },
|
||||
{ text: '不限', value: '不限' },
|
||||
]
|
||||
const SMOKING_OPTIONS = [
|
||||
{ text: '不吸烟', value: 'never' },
|
||||
{ text: '偶尔', value: 'sometimes' },
|
||||
{ text: '经常', value: 'often' },
|
||||
]
|
||||
const DRINKING_OPTIONS = [
|
||||
{ text: '不饮酒', value: 'never' },
|
||||
{ text: '偶尔', value: 'sometimes' },
|
||||
{ text: '经常', value: 'often' },
|
||||
]
|
||||
const YES_NO_OPTIONS = [
|
||||
{ text: '是', value: 'yes' },
|
||||
{ text: '否', value: 'no' },
|
||||
]
|
||||
const FUTURE_OPTIONS = [
|
||||
{ text: '愿意', value: 'willing' },
|
||||
{ text: '不确定', value: 'uncertain' },
|
||||
{ text: '不愿意', value: 'unwilling' },
|
||||
]
|
||||
|
||||
function labelOf(options: Array<{ text: string; value: string }>, value: string): string {
|
||||
return options.find((o) => o.value === value)?.text || ''
|
||||
}
|
||||
|
||||
const occupationLabel = computed(() => labelOf(OCCUPATION_OPTIONS, form.occupationCode))
|
||||
const workRegionLabel = computed(() => labelOf(WORK_REGION_OPTIONS, form.workRegionCode))
|
||||
const smokingLabel = computed(() => labelOf(SMOKING_OPTIONS, form.smokingStatus))
|
||||
const drinkingLabel = computed(() => labelOf(DRINKING_OPTIONS, form.drinkingStatus))
|
||||
const longTermLabel = computed(() => labelOf(YES_NO_OPTIONS, form.longTermDanyang))
|
||||
const futureLabel = computed(() => labelOf(FUTURE_OPTIONS, form.futureDanyang))
|
||||
|
||||
const picker = reactive<{ show: boolean; key: string; columns: any[] }>({ show: false, key: '', columns: [] })
|
||||
const PICKER_COLUMNS: Record<string, any[]> = {
|
||||
occupation: OCCUPATION_OPTIONS,
|
||||
workRegion: WORK_REGION_OPTIONS,
|
||||
smoking: SMOKING_OPTIONS,
|
||||
drinking: DRINKING_OPTIONS,
|
||||
longTerm: YES_NO_OPTIONS,
|
||||
future: FUTURE_OPTIONS,
|
||||
}
|
||||
|
||||
function openPicker(key: string) {
|
||||
picker.key = key
|
||||
picker.columns = PICKER_COLUMNS[key]
|
||||
picker.show = true
|
||||
}
|
||||
|
||||
function onPickerConfirm({ selectedOptions }: any) {
|
||||
const value = selectedOptions?.[0]?.value
|
||||
const key = picker.key
|
||||
picker.show = false
|
||||
if (value === undefined) return
|
||||
if (key === 'occupation') form.occupationCode = value
|
||||
else if (key === 'workRegion') form.workRegionCode = value
|
||||
else if (key === 'smoking') form.smokingStatus = value
|
||||
else if (key === 'drinking') form.drinkingStatus = value
|
||||
else if (key === 'longTerm') form.longTermDanyang = value
|
||||
else if (key === 'future') form.futureDanyang = value
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getProfile()
|
||||
form.occupationCode = p?.occupationCode || ''
|
||||
form.workRegionCode = p?.workRegionCode || ''
|
||||
form.smokingStatus = p?.smokingStatus || ''
|
||||
form.drinkingStatus = p?.drinkingStatus || ''
|
||||
form.longTermDanyang = p?.longTermDanyang || ''
|
||||
form.futureDanyang = p?.futureDanyang || ''
|
||||
} catch {
|
||||
/* 首次填写 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
if (!form.occupationCode) return appStore.toast('请选择职业类别')
|
||||
if (!form.workRegionCode) return appStore.toast('请选择工作区域')
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updateProfile({
|
||||
occupationCode: form.occupationCode,
|
||||
workRegionCode: form.workRegionCode,
|
||||
smokingStatus: form.smokingStatus,
|
||||
drinkingStatus: form.drinkingStatus,
|
||||
longTermDanyang: form.longTermDanyang,
|
||||
futureDanyang: form.futureDanyang,
|
||||
})
|
||||
router.push('/profile/photos')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pl__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pl {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.card :deep(.ui-section-header) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.card :deep(.van-field) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.pl__actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<PageLayout title="头像与照片">
|
||||
<div class="pp">
|
||||
<UiLoading v-if="loading" text="加载照片…" />
|
||||
<UiErrorState v-else-if="error" @retry="loadPhotos" />
|
||||
|
||||
<template v-else>
|
||||
<div class="card">
|
||||
<UiSectionHeader title="主头像" />
|
||||
<p class="pp__hint">必须上传 1 张清晰正面照</p>
|
||||
<div class="pp__grid">
|
||||
<div v-for="p in avatarPhotos" :key="p.photoId" class="pp__item" @click="preview(p)">
|
||||
<img :src="p.url" class="pp__img" alt="" />
|
||||
<div v-if="p.isMain || p.main" class="pp__main-flag">主头像</div>
|
||||
<div class="pp__remove" @click.stop="onRemove(p)">×</div>
|
||||
</div>
|
||||
<div v-if="avatarPhotos.length === 0" class="pp__add" @click="choose('avatar')">
|
||||
<span class="pp__add-icon">+</span>
|
||||
<span class="pp__add-text">上传头像</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="生活照片" />
|
||||
<p class="pp__hint">最多 {{ MAX_LIFESTYLE }} 张(可选)</p>
|
||||
<div class="pp__grid">
|
||||
<div v-for="p in lifestylePhotos" :key="p.photoId" class="pp__item" @click="preview(p)">
|
||||
<img :src="p.url" class="pp__img" alt="" />
|
||||
<div class="pp__remove" @click.stop="onRemove(p)">×</div>
|
||||
</div>
|
||||
<div v-if="lifestylePhotos.length < MAX_LIFESTYLE" class="pp__add" @click="choose('lifestyle')">
|
||||
<span class="pp__add-icon">+</span>
|
||||
<span class="pp__add-text">上传照片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<UiSectionHeader title="上传须知" />
|
||||
<div class="pp__guidelines">
|
||||
<div v-for="(g, i) in guidelines" :key="i" class="pp__guideline">
|
||||
<span class="pp__dot"></span>
|
||||
<span class="pp__guideline-text">{{ g }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="pp__actions">
|
||||
<van-button type="primary" block round :disabled="loading || error" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
|
||||
<input ref="fileInput" type="file" accept="image/*" class="pp__hidden-input" @change="onFileChange" />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showConfirmDialog, showImagePreview } from 'vant'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiErrorState from '@/components/UiErrorState.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const MAX_LIFESTYLE = 5 // 总上限 6(含 1 张头像)减 1
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const uploadTarget = ref<'avatar' | 'lifestyle'>('avatar')
|
||||
const avatarPhotos = ref<any[]>([])
|
||||
const lifestylePhotos = ref<any[]>([])
|
||||
const guidelines = [
|
||||
'请使用本人近期真实照片',
|
||||
'禁止网图、二维码、广告、联系方式图片',
|
||||
'建议真人生活照,避免过度美颜',
|
||||
]
|
||||
|
||||
async function loadPhotos() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const result: any = await profileService.getPhotos()
|
||||
const photos = result?.photos || result?.items || result?.records || []
|
||||
avatarPhotos.value = photos.filter((p: any) => p.photoType === 'avatar')
|
||||
lifestylePhotos.value = photos.filter((p: any) => p.photoType === 'lifestyle')
|
||||
} catch {
|
||||
error.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function choose(target: 'avatar' | 'lifestyle') {
|
||||
if (target === 'lifestyle' && lifestylePhotos.value.length >= MAX_LIFESTYLE) {
|
||||
appStore.toast(`最多上传 ${MAX_LIFESTYLE} 张生活照`)
|
||||
return
|
||||
}
|
||||
uploadTarget.value = target
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function onFileChange(evt: Event) {
|
||||
const input = evt.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
const photoType = uploadTarget.value
|
||||
try {
|
||||
const token: any = await profileService.requestUploadToken({
|
||||
fileName: `photo_${Date.now()}.jpg`,
|
||||
contentType: file.type || 'image/jpeg',
|
||||
})
|
||||
await fetch(token.uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: { 'Content-Type': file.type || 'image/jpeg' },
|
||||
})
|
||||
const confirmRes: any = await profileService.confirmPhoto({ fileKey: token.fileKey, photoType })
|
||||
if ((photoType === 'avatar' || avatarPhotos.value.length === 0) && confirmRes?.photoId) {
|
||||
await profileService.setMainPhoto(confirmRes.photoId)
|
||||
}
|
||||
appStore.toastSuccess('上传成功')
|
||||
await loadPhotos()
|
||||
} catch {
|
||||
appStore.toastError('上传失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemove(photo: any) {
|
||||
try {
|
||||
await showConfirmDialog({ title: '删除照片', message: '确定删除这张照片?删除后不可恢复。' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await profileService.deletePhoto(photo.photoId)
|
||||
appStore.toastSuccess('已删除')
|
||||
await loadPhotos()
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function preview(photo: any) {
|
||||
const urls = [...avatarPhotos.value, ...lifestylePhotos.value].map((p) => p.url).filter(Boolean)
|
||||
if (!urls.length) return
|
||||
showImagePreview({ images: urls, startPosition: urls.indexOf(photo.url) })
|
||||
}
|
||||
|
||||
function onNext() {
|
||||
if (avatarPhotos.value.length === 0) {
|
||||
appStore.toast('请至少上传 1 张主头像')
|
||||
return
|
||||
}
|
||||
router.push('/profile/interests')
|
||||
}
|
||||
|
||||
onMounted(loadPhotos)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pp__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pp {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 16px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pp__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pp__item {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--mini-bg);
|
||||
}
|
||||
.pp__img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.pp__main-flag {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: var(--mini-tabbar-active);
|
||||
padding: 1px 6px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
.pp__remove {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.pp__add {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
border: 1px dashed var(--mini-border, #dcdfe6);
|
||||
border-radius: 10px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pp__add-icon {
|
||||
position: absolute;
|
||||
top: 28%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
.pp__add-text {
|
||||
position: absolute;
|
||||
top: 58%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
.pp__guidelines {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pp__guideline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.pp__dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--mini-tabbar-active);
|
||||
flex: none;
|
||||
}
|
||||
.pp__guideline-text {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pp__actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.pp__hidden-input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<PageLayout title="择偶条件">
|
||||
<div class="pf">
|
||||
<UiLoading v-if="loading" text="加载中…" />
|
||||
|
||||
<!-- 年龄 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="年龄范围" />
|
||||
<p class="pf__hint">可设置必须符合 / 最好符合</p>
|
||||
<div class="pf__range">
|
||||
<van-field v-model="pref.ageMin" type="digit" placeholder="最小" class="pf__range-field" />
|
||||
<span class="pf__dash">~</span>
|
||||
<van-field v-model="pref.ageMax" type="digit" placeholder="最大" class="pf__range-field" />
|
||||
<span class="pf__unit">岁</span>
|
||||
</div>
|
||||
<div class="pf__importance">
|
||||
<span class="pf__importance-label">重要程度</span>
|
||||
<span
|
||||
v-for="o in IMPORTANCE_OPTIONS"
|
||||
:key="o.value"
|
||||
class="pf__imp"
|
||||
:class="{ 'pf__imp--active': pref.agePriority === o.value }"
|
||||
@click="setImportance('agePriority', o.value)"
|
||||
>{{ o.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 身高 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="身高范围" />
|
||||
<p class="pf__hint">可设置必须符合 / 最好符合</p>
|
||||
<div class="pf__range">
|
||||
<van-field v-model="pref.heightMin" type="digit" placeholder="最小" class="pf__range-field" />
|
||||
<span class="pf__dash">~</span>
|
||||
<van-field v-model="pref.heightMax" type="digit" placeholder="最大" class="pf__range-field" />
|
||||
<span class="pf__unit">cm</span>
|
||||
</div>
|
||||
<div class="pf__importance">
|
||||
<span class="pf__importance-label">重要程度</span>
|
||||
<span
|
||||
v-for="o in IMPORTANCE_OPTIONS"
|
||||
:key="o.value"
|
||||
class="pf__imp"
|
||||
:class="{ 'pf__imp--active': pref.heightPriority === o.value }"
|
||||
@click="setImportance('heightPriority', o.value)"
|
||||
>{{ o.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 婚姻状况 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="期望婚姻状况" />
|
||||
<p class="pf__hint">多选</p>
|
||||
<div class="pf__tags">
|
||||
<span
|
||||
v-for="o in MARITAL_OPTIONS"
|
||||
:key="o.value"
|
||||
class="pf__tag"
|
||||
:class="{ 'pf__tag--selected': pref.maritalStatusAccepted.includes(o.value) }"
|
||||
@click="toggleMarital(o.value)"
|
||||
>
|
||||
{{ o.text }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pf__importance">
|
||||
<span class="pf__importance-label">重要程度</span>
|
||||
<span
|
||||
v-for="o in IMPORTANCE_OPTIONS"
|
||||
:key="o.value"
|
||||
class="pf__imp"
|
||||
:class="{ 'pf__imp--active': pref.maritalPriority === o.value }"
|
||||
@click="setImportance('maritalPriority', o.value)"
|
||||
>{{ o.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学历 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="期望学历" />
|
||||
<van-field
|
||||
v-model="educationLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="最低学历"
|
||||
placeholder="不限"
|
||||
:border="false"
|
||||
@click="openPicker('education')"
|
||||
/>
|
||||
<div class="pf__importance">
|
||||
<span class="pf__importance-label">重要程度</span>
|
||||
<span
|
||||
v-for="o in IMPORTANCE_OPTIONS"
|
||||
:key="o.value"
|
||||
class="pf__imp"
|
||||
:class="{ 'pf__imp--active': pref.educationPriority === o.value }"
|
||||
@click="setImportance('educationPriority', o.value)"
|
||||
>{{ o.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更多条件 -->
|
||||
<div class="card">
|
||||
<UiSectionHeader title="更多择偶条件" />
|
||||
<p class="pf__hint">可选,均允许不限</p>
|
||||
<van-field
|
||||
v-model="childrenLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否接受有子女"
|
||||
placeholder="不限"
|
||||
:border="false"
|
||||
@click="openPicker('children')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="smokingLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否吸烟"
|
||||
placeholder="不限"
|
||||
@click="openPicker('smoking')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="drinkingLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="是否饮酒"
|
||||
placeholder="不限"
|
||||
@click="openPicker('drinking')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="marriagePlanLabel"
|
||||
is-link
|
||||
readonly
|
||||
label="结婚时间计划"
|
||||
placeholder="不限"
|
||||
@click="openPicker('marriagePlan')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pf__actions">
|
||||
<van-button type="primary" block round :loading="saving" @click="onNext">下一步</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="picker.show" position="bottom" round>
|
||||
<van-picker :columns="picker.columns" @cancel="picker.show = false" @confirm="onPickerConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showConfirmDialog } from 'vant'
|
||||
import { profileService } from '@/api/services'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import PageLayout from '@/layouts/PageLayout.vue'
|
||||
import UiLoading from '@/components/UiLoading.vue'
|
||||
import UiSectionHeader from '@/components/UiSectionHeader.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const IMPORTANCE_OPTIONS = [
|
||||
{ text: '必须符合', value: 'required' },
|
||||
{ text: '最好符合', value: 'preferred' },
|
||||
]
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const pref = reactive({
|
||||
ageMin: '',
|
||||
ageMax: '',
|
||||
agePriority: 'preferred',
|
||||
heightMin: '',
|
||||
heightMax: '',
|
||||
heightPriority: 'preferred',
|
||||
maritalStatusAccepted: [] as string[],
|
||||
maritalPriority: 'preferred',
|
||||
educationCode: '',
|
||||
educationPriority: 'preferred',
|
||||
childrenAcceptance: 'any',
|
||||
smokingPreference: '',
|
||||
drinkingPreference: '',
|
||||
marriagePlanPreference: '',
|
||||
})
|
||||
|
||||
const MARITAL_OPTIONS = [
|
||||
{ text: '未婚', value: 'never_married' },
|
||||
{ text: '离异', value: 'divorced' },
|
||||
{ text: '丧偶', value: 'widowed' },
|
||||
]
|
||||
const EDUCATION_OPTIONS = [
|
||||
{ text: '不限', value: '' },
|
||||
{ text: '高中/中专以上', value: 'senior_high' },
|
||||
{ text: '大专以上', value: 'college' },
|
||||
{ text: '本科以上', value: 'bachelor' },
|
||||
{ text: '研究生以上', value: 'master' },
|
||||
]
|
||||
const CHILDREN_OPTIONS = [
|
||||
{ text: '不限', value: 'any' },
|
||||
{ text: '可接受', value: 'accept' },
|
||||
{ text: '不接受', value: 'reject' },
|
||||
]
|
||||
const SMOKING_OPTIONS = [
|
||||
{ text: '不限', value: '' },
|
||||
{ text: '不吸烟', value: 'never' },
|
||||
{ text: '偶尔', value: 'sometimes' },
|
||||
{ text: '经常', value: 'often' },
|
||||
]
|
||||
const DRINKING_OPTIONS = [
|
||||
{ text: '不限', value: '' },
|
||||
{ text: '不饮酒', value: 'never' },
|
||||
{ text: '偶尔', value: 'sometimes' },
|
||||
{ text: '经常', value: 'often' },
|
||||
]
|
||||
const MARRIAGE_PLAN_OPTIONS = [
|
||||
{ text: '不限', value: '' },
|
||||
{ text: '半年内', value: 'half_year' },
|
||||
{ text: '1年内', value: 'one_year' },
|
||||
{ text: '1~2年', value: 'one_to_two_years' },
|
||||
{ text: '顺其自然', value: 'let_it_be' },
|
||||
]
|
||||
|
||||
function labelOf(options: Array<{ text: string; value: string }>, value: string): string {
|
||||
return options.find((o) => o.value === value)?.text || ''
|
||||
}
|
||||
|
||||
const educationLabel = computed(() => labelOf(EDUCATION_OPTIONS, pref.educationCode) || '不限')
|
||||
const childrenLabel = computed(() => labelOf(CHILDREN_OPTIONS, pref.childrenAcceptance) || '不限')
|
||||
const smokingLabel = computed(() => labelOf(SMOKING_OPTIONS, pref.smokingPreference) || '不限')
|
||||
const drinkingLabel = computed(() => labelOf(DRINKING_OPTIONS, pref.drinkingPreference) || '不限')
|
||||
const marriagePlanLabel = computed(() => labelOf(MARRIAGE_PLAN_OPTIONS, pref.marriagePlanPreference) || '不限')
|
||||
|
||||
const picker = reactive<{ show: boolean; key: string; columns: any[] }>({ show: false, key: '', columns: [] })
|
||||
const PICKER_COLUMNS: Record<string, any[]> = {
|
||||
education: EDUCATION_OPTIONS,
|
||||
children: CHILDREN_OPTIONS,
|
||||
smoking: SMOKING_OPTIONS,
|
||||
drinking: DRINKING_OPTIONS,
|
||||
marriagePlan: MARRIAGE_PLAN_OPTIONS,
|
||||
}
|
||||
|
||||
function openPicker(key: string) {
|
||||
picker.key = key
|
||||
picker.columns = PICKER_COLUMNS[key]
|
||||
picker.show = true
|
||||
}
|
||||
|
||||
function onPickerConfirm({ selectedOptions }: any) {
|
||||
const value = selectedOptions?.[0]?.value
|
||||
const key = picker.key
|
||||
picker.show = false
|
||||
if (value === undefined) return
|
||||
if (key === 'education') pref.educationCode = value
|
||||
else if (key === 'children') pref.childrenAcceptance = value || 'any'
|
||||
else if (key === 'smoking') pref.smokingPreference = value
|
||||
else if (key === 'drinking') pref.drinkingPreference = value
|
||||
else if (key === 'marriagePlan') pref.marriagePlanPreference = value
|
||||
}
|
||||
|
||||
function toggleMarital(code: string) {
|
||||
const list = [...pref.maritalStatusAccepted]
|
||||
const idx = list.indexOf(code)
|
||||
if (idx >= 0) list.splice(idx, 1)
|
||||
else list.push(code)
|
||||
pref.maritalStatusAccepted = list
|
||||
}
|
||||
|
||||
/** Required 不可自动放宽:必须符合 → 最好符合 需二次确认 */
|
||||
async function setImportance(field: string, value: string) {
|
||||
if ((pref as any)[field] === 'required' && value === 'preferred') {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '确认放宽此条件?',
|
||||
message: '"必须符合"的条件不会被系统放宽。改为"最好符合"后,可能匹配到略不满足此条件的对象。',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
;(pref as any)[field] = value
|
||||
}
|
||||
|
||||
function toApiPayload() {
|
||||
const num = (v: string) => (v === '' || v === null ? null : Number(v))
|
||||
return {
|
||||
minAge: num(pref.ageMin),
|
||||
maxAge: num(pref.ageMax),
|
||||
agePriority: pref.agePriority,
|
||||
minHeightCm: num(pref.heightMin),
|
||||
maxHeightCm: num(pref.heightMax),
|
||||
heightPriority: pref.heightPriority,
|
||||
maritalStatuses: pref.maritalStatusAccepted,
|
||||
maritalPriority: pref.maritalPriority,
|
||||
minEducationCode: pref.educationCode || null,
|
||||
educationPriority: pref.educationPriority,
|
||||
childrenAcceptance: pref.childrenAcceptance,
|
||||
smokingPreference: pref.smokingPreference || null,
|
||||
drinkingPreference: pref.drinkingPreference || null,
|
||||
marriagePlanPreference: pref.marriagePlanPreference || null,
|
||||
}
|
||||
}
|
||||
|
||||
function fromApiPayload(p: any) {
|
||||
if (!p) return
|
||||
const str = (v: any) => (v === undefined || v === null ? '' : String(v))
|
||||
pref.ageMin = str(p.minAge)
|
||||
pref.ageMax = str(p.maxAge)
|
||||
pref.agePriority = p.agePriority || 'preferred'
|
||||
pref.heightMin = str(p.minHeightCm)
|
||||
pref.heightMax = str(p.maxHeightCm)
|
||||
pref.heightPriority = p.heightPriority || 'preferred'
|
||||
pref.maritalStatusAccepted = p.maritalStatuses || []
|
||||
pref.maritalPriority = p.maritalPriority || 'preferred'
|
||||
pref.educationCode = p.minEducationCode || ''
|
||||
pref.educationPriority = p.educationPriority || 'preferred'
|
||||
pref.childrenAcceptance = p.childrenAcceptance || 'any'
|
||||
pref.smokingPreference = p.smokingPreference || ''
|
||||
pref.drinkingPreference = p.drinkingPreference || ''
|
||||
pref.marriagePlanPreference = p.marriagePlanPreference || ''
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const p: any = await profileService.getPreferences()
|
||||
fromApiPayload(p)
|
||||
} catch {
|
||||
/* 首次填写 */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (saving.value) return
|
||||
if (!pref.ageMin || !pref.ageMax) return appStore.toast('请填写年龄范围')
|
||||
saving.value = true
|
||||
try {
|
||||
await profileService.updatePreferences(toApiPayload())
|
||||
router.push('/profile/confirm')
|
||||
} catch (err: any) {
|
||||
appStore.toastError(err?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pf__hint {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pf {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--mini-surface);
|
||||
border-radius: var(--mini-radius);
|
||||
box-shadow: var(--mini-shadow);
|
||||
padding: 4px 16px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pf__range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pf__range-field {
|
||||
flex: 1;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.pf__dash {
|
||||
color: var(--mini-text-muted);
|
||||
}
|
||||
.pf__unit {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text-secondary);
|
||||
}
|
||||
.pf__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
.pf__tag {
|
||||
font-size: 13px;
|
||||
color: var(--mini-text);
|
||||
background: var(--mini-bg);
|
||||
border-radius: 16px;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.pf__tag--selected {
|
||||
color: var(--mini-tabbar-active);
|
||||
background: rgba(244, 0, 90, 0.08);
|
||||
border-color: var(--mini-tabbar-active);
|
||||
}
|
||||
.pf__importance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.pf__importance-label {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-muted);
|
||||
}
|
||||
.pf__imp {
|
||||
font-size: 12px;
|
||||
color: var(--mini-text-secondary);
|
||||
background: var(--mini-bg);
|
||||
border-radius: 12px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.pf__imp--active {
|
||||
color: #fff;
|
||||
background: var(--mini-tabbar-active);
|
||||
}
|
||||
.card :deep(.van-field) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.pf__actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user