522 lines
15 KiB
Vue
522 lines
15 KiB
Vue
<template>
|
||
<scroll-view class="panel-scroll" scroll-y>
|
||
<view class="card">
|
||
<text class="section-title">基本信息</text>
|
||
<view class="field">
|
||
<text class="label">手机号码</text>
|
||
<input
|
||
class="input"
|
||
v-model="local.myPhone"
|
||
type="number"
|
||
maxlength="11"
|
||
placeholder="请输入手机号码"
|
||
placeholder-class="ph"
|
||
/>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">名字</text>
|
||
<input class="input" v-model="local.name" placeholder="请输入名字" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">称呼</text>
|
||
<input class="input" v-model="local.salutation" placeholder="例如:小王" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">性别</text>
|
||
<radio-group class="gender-group" @change="onGenderRadioChange">
|
||
<label class="gender-item">
|
||
<radio value="男" :checked="local.gender === '男'" color="#e91e8c" />
|
||
<text class="gender-text">男</text>
|
||
</label>
|
||
<label class="gender-item">
|
||
<radio value="女" :checked="local.gender === '女'" color="#e91e8c" />
|
||
<text class="gender-text">女</text>
|
||
</label>
|
||
</radio-group>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">年龄</text>
|
||
<input
|
||
class="input"
|
||
v-model="local.age"
|
||
type="number"
|
||
placeholder="请输入年龄"
|
||
placeholder-class="ph"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="section-title">职业与居住</text>
|
||
<view class="field">
|
||
<text class="label">行业</text>
|
||
<input class="input" v-model="local.industry" placeholder="所在行业" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">收入水平</text>
|
||
<input class="input" v-model="local.incomeLevel" placeholder="收入水平" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">居住区域</text>
|
||
<input class="input" v-model="local.residentialArea" placeholder="居住区域" placeholder-class="ph" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="section-title">兴趣与关系</text>
|
||
<view class="field">
|
||
<text class="label">性格</text>
|
||
<textarea
|
||
class="area area--sm"
|
||
v-model="local.personality"
|
||
placeholder="性格特点"
|
||
maxlength="500"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">兴趣爱好</text>
|
||
<textarea
|
||
class="area area--sm"
|
||
v-model="local.hobbies"
|
||
placeholder="兴趣爱好"
|
||
maxlength="500"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">喜欢的美食</text>
|
||
<input class="input" v-model="local.favoriteFood" placeholder="喜欢的美食" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">喜欢的电影类型</text>
|
||
<input class="input" v-model="local.favoriteMovieGenres" placeholder="例如:科幻、文艺" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">最近关系的小事</text>
|
||
<textarea
|
||
class="area area--sm"
|
||
v-model="local.recentRelationshipNote"
|
||
placeholder="最近与感情/相处相关的一件小事"
|
||
maxlength="1000"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="section-title">教育与健康</text>
|
||
<view class="field">
|
||
<text class="label">学历</text>
|
||
<input class="input" v-model="local.education" placeholder="学历" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">专业</text>
|
||
<input class="input" v-model="local.major" placeholder="专业" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">身高(厘米)</text>
|
||
<input
|
||
class="input"
|
||
v-model="local.heightCm"
|
||
type="number"
|
||
placeholder="例如 170"
|
||
placeholder-class="ph"
|
||
/>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">体重(千克)</text>
|
||
<input class="input" v-model="local.weightKg" placeholder="例如 65.5" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">健康状况</text>
|
||
<input class="input" v-model="local.healthStatus" placeholder="健康状况" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">对自己的评价</text>
|
||
<textarea
|
||
class="area"
|
||
v-model="local.selfEvaluation"
|
||
placeholder="对自己的评价"
|
||
maxlength="2000"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="section-title">家庭与物质条件</text>
|
||
<view class="field">
|
||
<text class="label">父亲职业</text>
|
||
<input class="input" v-model="local.fatherOccupation" placeholder="父亲职业" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">母亲职业</text>
|
||
<input class="input" v-model="local.motherOccupation" placeholder="母亲职业" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">兄妹几个</text>
|
||
<input class="input" v-model="local.siblingsDesc" placeholder="例如:独生子女 / 有一个哥哥" placeholder-class="ph" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">房子(有无、贷款等)</text>
|
||
<textarea
|
||
class="area area--sm"
|
||
v-model="local.houseSituation"
|
||
placeholder="房产情况"
|
||
maxlength="500"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">车子(有无等)</text>
|
||
<input class="input" v-model="local.carSituation" placeholder="车辆情况" placeholder-class="ph" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="label">我是谁(恋爱相关的自我描述)</text>
|
||
<textarea
|
||
class="area"
|
||
v-model="local.selfIntro"
|
||
placeholder="例如:我的性格、价值观、在关系里看重什么…"
|
||
maxlength="2000"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
<view class="card">
|
||
<text class="label">对另一半的要求</text>
|
||
<textarea
|
||
class="area"
|
||
v-model="local.partnerExpect"
|
||
placeholder="例如:希望对方具备的特质、相处方式、底线与期待…"
|
||
maxlength="2000"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<text class="label">备注</text>
|
||
<textarea
|
||
class="area area--sm"
|
||
v-model="local.remark"
|
||
placeholder="其他备注"
|
||
maxlength="1000"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
</view>
|
||
|
||
<button class="btn-save" type="primary" :loading="saveSubmitting" :disabled="saveSubmitting" @click="save">
|
||
保存
|
||
</button>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script>
|
||
import { post } from '@/common/request.js';
|
||
|
||
const KEY = 'xixi_who_am_i';
|
||
|
||
function trimStr(v) {
|
||
if (v == null) return '';
|
||
return String(v).trim();
|
||
}
|
||
|
||
/** 中国大陆手机号:1 开头第二位 3-9,共 11 位 */
|
||
function isValidChinaMobile(phone) {
|
||
return /^1[3-9]\d{9}$/.test(phone);
|
||
}
|
||
|
||
/**
|
||
* 保存前校验:未填写的项不校验;填写了则必须符合规则
|
||
* @returns {{ ok: true } | { ok: false, message: string }}
|
||
*/
|
||
function validateLoveWhoIFields(local) {
|
||
const phone = trimStr(local.myPhone);
|
||
if (phone !== '') {
|
||
if (!isValidChinaMobile(phone)) {
|
||
return { ok: false, message: '请输入正确的11位手机号' };
|
||
}
|
||
}
|
||
|
||
const ageStr = trimStr(local.age);
|
||
if (ageStr !== '') {
|
||
if (!/^\d+$/.test(ageStr)) {
|
||
return { ok: false, message: '年龄请填写正整数' };
|
||
}
|
||
const age = parseInt(ageStr, 10);
|
||
if (age < 1 || age > 120) {
|
||
return { ok: false, message: '年龄需在 1~120 岁' };
|
||
}
|
||
}
|
||
|
||
const heightStr = trimStr(local.heightCm);
|
||
if (heightStr !== '') {
|
||
if (!/^\d+$/.test(heightStr)) {
|
||
return { ok: false, message: '身高请填写正整数(厘米)' };
|
||
}
|
||
const h = parseInt(heightStr, 10);
|
||
if (h < 50 || h > 250) {
|
||
return { ok: false, message: '身高需在 50~250 厘米' };
|
||
}
|
||
}
|
||
|
||
const weightStr = trimStr(local.weightKg);
|
||
if (weightStr !== '') {
|
||
const normalized = weightStr.replace(/,/g, '');
|
||
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
|
||
return { ok: false, message: '体重请填写数字,最多两位小数(千克)' };
|
||
}
|
||
const w = parseFloat(normalized);
|
||
if (!Number.isFinite(w) || w < 15 || w > 400) {
|
||
return { ok: false, message: '体重需在 15~400 千克' };
|
||
}
|
||
}
|
||
|
||
return { ok: true };
|
||
}
|
||
|
||
/** 提交 /api/loveWhoI/add 的请求体(camelCase,与 Spring 默认一致) */
|
||
function buildLoveWhoIAddPayload(local) {
|
||
const ageStr = trimStr(local.age);
|
||
const heightStr = trimStr(local.heightCm);
|
||
const weightStr = trimStr(local.weightKg);
|
||
const payload = {
|
||
myPhone: trimStr(local.myPhone),
|
||
name: trimStr(local.name),
|
||
salutation: trimStr(local.salutation),
|
||
gender: trimStr(local.gender),
|
||
industry: trimStr(local.industry),
|
||
incomeLevel: trimStr(local.incomeLevel),
|
||
residentialArea: trimStr(local.residentialArea),
|
||
personality: trimStr(local.personality),
|
||
hobbies: trimStr(local.hobbies),
|
||
favoriteFood: trimStr(local.favoriteFood),
|
||
favoriteMovieGenres: trimStr(local.favoriteMovieGenres),
|
||
recentRelationshipNote: trimStr(local.recentRelationshipNote),
|
||
education: trimStr(local.education),
|
||
major: trimStr(local.major),
|
||
healthStatus: trimStr(local.healthStatus),
|
||
selfEvaluation: trimStr(local.selfEvaluation),
|
||
fatherOccupation: trimStr(local.fatherOccupation),
|
||
motherOccupation: trimStr(local.motherOccupation),
|
||
siblingsDesc: trimStr(local.siblingsDesc),
|
||
houseSituation: trimStr(local.houseSituation),
|
||
carSituation: trimStr(local.carSituation),
|
||
remark: trimStr(local.remark),
|
||
};
|
||
if (ageStr !== '') {
|
||
const n = parseInt(ageStr, 10);
|
||
if (Number.isFinite(n)) payload.age = n;
|
||
}
|
||
if (heightStr !== '') {
|
||
const n = parseInt(heightStr, 10);
|
||
if (Number.isFinite(n)) payload.heightCm = n;
|
||
}
|
||
if (weightStr !== '') {
|
||
const w = parseFloat(weightStr);
|
||
if (Number.isFinite(w)) payload.weightKg = w;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function defaultLocal() {
|
||
return {
|
||
loveWhoIId: '',
|
||
myPhone: '',
|
||
name: '',
|
||
salutation: '',
|
||
gender: '',
|
||
age: '',
|
||
industry: '',
|
||
incomeLevel: '',
|
||
residentialArea: '',
|
||
personality: '',
|
||
hobbies: '',
|
||
favoriteFood: '',
|
||
favoriteMovieGenres: '',
|
||
recentRelationshipNote: '',
|
||
education: '',
|
||
major: '',
|
||
heightCm: '',
|
||
weightKg: '',
|
||
healthStatus: '',
|
||
selfEvaluation: '',
|
||
fatherOccupation: '',
|
||
motherOccupation: '',
|
||
siblingsDesc: '',
|
||
houseSituation: '',
|
||
carSituation: '',
|
||
remark: '',
|
||
selfIntro: '',
|
||
partnerExpect: '',
|
||
};
|
||
}
|
||
|
||
export default {
|
||
name: 'XixiWhoAmI',
|
||
data() {
|
||
return {
|
||
local: defaultLocal(),
|
||
saveSubmitting: false,
|
||
};
|
||
},
|
||
mounted() {
|
||
this.load();
|
||
},
|
||
methods: {
|
||
onGenderRadioChange(e) {
|
||
this.local.gender = e.detail.value || '';
|
||
},
|
||
load() {
|
||
const base = defaultLocal();
|
||
try {
|
||
const raw = uni.getStorageSync(KEY);
|
||
if (raw && typeof raw === 'object') {
|
||
this.local = { ...base, ...raw };
|
||
} else {
|
||
this.local = base;
|
||
}
|
||
} catch (e) {
|
||
this.local = base;
|
||
}
|
||
},
|
||
async save() {
|
||
if (this.saveSubmitting) return;
|
||
const check = validateLoveWhoIFields(this.local);
|
||
if (!check.ok) {
|
||
uni.showToast({ title: check.message, icon: 'none' });
|
||
return;
|
||
}
|
||
this.saveSubmitting = true;
|
||
try {
|
||
const requestData = buildLoveWhoIAddPayload(this.local);
|
||
const res = await post('/api/loveWhoI/add', requestData);
|
||
const payload = res.data && typeof res.data === 'object' ? res.data : {};
|
||
const ok = res.statusCode === 200 && payload.success === true;
|
||
|
||
if (ok) {
|
||
const row = payload.data;
|
||
if (row && typeof row === 'object' && row.id) {
|
||
this.local.loveWhoIId = String(row.id);
|
||
}
|
||
try {
|
||
uni.setStorageSync(KEY, { ...this.local });
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
uni.showToast({ title: payload.message || '已保存', icon: 'success' });
|
||
this.$emit('saved');
|
||
} else {
|
||
const msg = payload.message || `服务端保存失败(${res.statusCode || '?'})`;
|
||
try {
|
||
uni.setStorageSync(KEY, { ...this.local });
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
uni.showToast({ title: msg, icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
console.error('[XixiWhoAmI] save failed:', e);
|
||
try {
|
||
uni.setStorageSync(KEY, { ...this.local });
|
||
} catch (err) {
|
||
/* ignore */
|
||
}
|
||
const msg = e?.message || '网络异常,已暂存本地';
|
||
uni.showToast({ title: msg, icon: 'none' });
|
||
} finally {
|
||
this.saveSubmitting = false;
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.panel-scroll {
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
padding: 24rpx 28rpx 32rpx;
|
||
}
|
||
.card {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 28rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.06);
|
||
}
|
||
.section-title {
|
||
display: block;
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #e91e8c;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.field {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.field:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
.label {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.input {
|
||
width: 100%;
|
||
min-height: 72rpx;
|
||
font-size: 28rpx;
|
||
line-height: 1.5;
|
||
color: #333;
|
||
box-sizing: border-box;
|
||
padding: 16rpx;
|
||
background: #fff8fa;
|
||
border-radius: 12rpx;
|
||
}
|
||
.gender-group {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 40rpx;
|
||
padding: 8rpx 4rpx;
|
||
}
|
||
.gender-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
.gender-text {
|
||
margin-left: 12rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
.ph {
|
||
color: #b0b0b0;
|
||
}
|
||
.area {
|
||
width: 100%;
|
||
min-height: 220rpx;
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
box-sizing: border-box;
|
||
padding: 16rpx;
|
||
background: #fff8fa;
|
||
border-radius: 12rpx;
|
||
}
|
||
.area--sm {
|
||
min-height: 140rpx;
|
||
}
|
||
.btn-save {
|
||
margin-top: 12rpx;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, #ff6b9d, #e91e8c);
|
||
border: none;
|
||
}
|
||
</style>
|