1613 lines
50 KiB
Plaintext
1613 lines
50 KiB
Plaintext
<template>
|
||
<view class="page-wrapper">
|
||
<scroll-view scroll-y="true" class="profile-scroll">
|
||
<view class="profile-content-wrap">
|
||
<view v-if="isLoading" class="loading-container">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="hasLoadError" class="error-container">
|
||
<text class="error-text">加载失败</text>
|
||
<button class="retry-button" @click="loadProfile">重试</button>
|
||
</view>
|
||
|
||
<view v-else class="profile-content">
|
||
<view class="profile-card">
|
||
<text class="card-title">基础信息</text>
|
||
|
||
<view class="info-row avatar-row" @click="chooseAvatar">
|
||
<text class="info-label">头像</text>
|
||
<view class="info-value-wrap avatar-value-wrap">
|
||
<image class="avatar-image" :src="userAvatar" mode="aspectFill"></image>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row">
|
||
<text class="info-label">账号名</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getAccountName() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openTextEditor('username')">
|
||
<text class="info-label">昵称</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getNicknameText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="showGenderPickerNow">
|
||
<text class="info-label">性别</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getGenderText(profile.gender ?? 'other') }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="showBirthdayPickernow">
|
||
<text class="info-label">出生日期</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getBirthdayText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row">
|
||
<text class="info-label">注册日期</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ registeredDate }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row last-row" @click="openTextEditor('bio')">
|
||
<text class="info-label">个性签名</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getBioText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="profile-card">
|
||
<text class="card-title">身体信息</text>
|
||
|
||
<view class="info-row" @click="openTextEditor('height_cm')">
|
||
<text class="info-label">身高</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getHeightText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openTextEditor('weight_kg')">
|
||
<text class="info-label">体重</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getWeightText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row last-row">
|
||
<text class="info-label">BMI</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getBmiText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="profile-card">
|
||
<text class="card-title">康养档案</text>
|
||
|
||
<view class="info-row" @click="openHealthGoalPicker">
|
||
<text class="info-label">健康目标</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getTextOrPlaceholder(profile.health_goal, '未设置') }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openAddressPicker">
|
||
<text class="info-label">常住服务地址</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getServiceAddressText() }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openTextEditor('emergency_contact')">
|
||
<text class="info-label">紧急联系人</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getTextOrPlaceholder(profile.emergency_contact, '未填写') }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openTextEditor('chronic_notes')">
|
||
<text class="info-label">慢病/过敏备注</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getTextOrPlaceholder(profile.chronic_notes, '无') }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row" @click="openCarePreferencePicker">
|
||
<text class="info-label">护理偏好</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getTextOrPlaceholder(profile.care_preference, '未设置') }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="info-row last-row" @click="openLanguagePicker">
|
||
<text class="info-label">语言偏好</text>
|
||
<view class="info-value-wrap">
|
||
<text class="info-value">{{ getLanguageText(profile.preferred_language) }}</text>
|
||
</view>
|
||
<text class="info-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<button class="save-button" :disabled="isSaving" :loading="isSaving" @click="saveProfile">
|
||
保存修改
|
||
</button>
|
||
</view>
|
||
|
||
<view v-if="showGenderPicker" class="picker-modal">
|
||
<picker-view class="picker-view" :value="tempGenderIndex" :indicator-style="'height: 50px;'" @change="onGenderPickerViewChange">
|
||
<picker-view-column style="width:750rpx;">
|
||
<view v-for="(g, idx) in genderOptions" :key="g" class="picker-item">
|
||
{{ getGenderText(g) }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="showGenderPicker = false">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmGenderPicker">确定</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showBirthdayPicker" class="picker-modal">
|
||
<picker-view class="picker-view" :value="tempBirthdayIndex" :indicator-style="'height: 50px;'" @change="onBirthdayPickerViewChange">
|
||
<picker-view-column style="width:250rpx;">
|
||
<view v-for="(year, idx) in birthdayYearOptions" :key="year" class="picker-item birthday-picker-item">
|
||
{{ year }}年
|
||
</view>
|
||
</picker-view-column>
|
||
<picker-view-column style="width:250rpx;">
|
||
<view v-for="(month, idx) in birthdayMonthOptions" :key="month" class="picker-item birthday-picker-item">
|
||
{{ month }}月
|
||
</view>
|
||
</picker-view-column>
|
||
<picker-view-column style="width:250rpx;">
|
||
<view v-for="(day, idx) in getBirthdayDayOptions()" :key="day" class="picker-item birthday-picker-item">
|
||
{{ day }}日
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="showBirthdayPicker = false">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmBirthdayPicker">确定</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showEditorModal" class="picker-modal">
|
||
<view class="editor-sheet">
|
||
<text class="editor-title">{{ editorTitle }}</text>
|
||
<textarea
|
||
v-if="editorIsTextarea"
|
||
class="editor-textarea"
|
||
:value="editorDraft"
|
||
:placeholder="editorPlaceholder"
|
||
placeholder-style="color: #b8b8b8;"
|
||
@input="onEditorInput"
|
||
></textarea>
|
||
<input
|
||
v-else
|
||
class="editor-input"
|
||
:type="editorInputType"
|
||
:value="editorDraft"
|
||
:placeholder="editorPlaceholder"
|
||
placeholder-style="color: #b8b8b8;"
|
||
@input="onEditorInput"
|
||
/>
|
||
<view class="picker-actions editor-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="closeEditorModal">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmEditorModal">确定</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showLanguagePicker" class="picker-modal">
|
||
<picker-view class="picker-view" :value="tempLanguageIndex" :indicator-style="'height: 50px;'" @change="onLanguagePickerViewChange">
|
||
<picker-view-column style="width:750rpx;">
|
||
<view v-for="(language, idx) in languageOptions" :key="language" class="picker-item">
|
||
{{ getLanguageText(language) }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="showLanguagePicker = false">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmLanguagePicker">确定</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showHealthGoalPicker" class="picker-modal">
|
||
<picker-view class="picker-view" :value="tempHealthGoalIndex" :indicator-style="'height: 50px;'" @change="onHealthGoalPickerViewChange">
|
||
<picker-view-column style="width:750rpx;">
|
||
<view v-for="(goal, idx) in healthGoalOptions" :key="goal" class="picker-item">
|
||
{{ goal }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="showHealthGoalPicker = false">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmHealthGoalPicker">确定</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showCarePreferencePicker" class="picker-modal">
|
||
<picker-view class="picker-view" :value="tempCarePreferenceIndex" :indicator-style="'height: 50px;'" @change="onCarePreferencePickerViewChange">
|
||
<picker-view-column style="width:750rpx;">
|
||
<view v-for="(preference, idx) in carePreferenceOptions" :key="preference" class="picker-item">
|
||
{{ preference }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<view class="picker-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="showCarePreferencePicker = false">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmCarePreferencePicker">确定</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showAddressPicker" class="picker-modal">
|
||
<view class="address-sheet">
|
||
<text class="editor-title">常住服务地址</text>
|
||
<view v-if="recentAddressSuggestions.length > 0" class="address-suggestion-block">
|
||
<text class="address-suggestion-title">最近使用</text>
|
||
<view class="address-tag-list">
|
||
<view v-for="(item, idx) in recentAddressSuggestions" :key="item" class="address-tag" @click="useAddressSuggestion(item)">
|
||
<text class="address-tag-text">{{ item }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="address-suggestion-block">
|
||
<text class="address-suggestion-title">常用示例</text>
|
||
<view class="address-tag-list">
|
||
<view v-for="(item, idx) in commonAddressSuggestions" :key="item" class="address-tag address-tag-light" @click="useAddressSuggestion(item)">
|
||
<text class="address-tag-text">{{ item }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<picker-view class="address-picker-view" :value="tempAddressRegionIndex" :indicator-style="'height: 50px;'" @change="onAddressPickerViewChange">
|
||
<picker-view-column class="address-picker-column">
|
||
<view v-for="(province, idx) in getProvinceOptions()" :key="province" class="picker-item address-picker-item">
|
||
{{ province }}
|
||
</view>
|
||
</picker-view-column>
|
||
<picker-view-column class="address-picker-column">
|
||
<view v-for="(city, idx) in getCurrentCityOptions()" :key="city" class="picker-item address-picker-item">
|
||
{{ city }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
<input
|
||
class="editor-input address-detail-input"
|
||
type="text"
|
||
:value="tempAddressDetail"
|
||
placeholder="请输入详细地址,如街道、门牌号"
|
||
placeholder-style="color: #b8b8b8;"
|
||
@input="onAddressDetailInput"
|
||
/>
|
||
<view class="picker-actions editor-actions">
|
||
<button class="picker-action-button picker-action-cancel" @click="closeAddressPicker">取消</button>
|
||
<button class="picker-action-button picker-action-confirm" @click="confirmAddressPicker">确定</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { AkSupaSelectOptions } from '@/components/supadb/aksupa.uts'
|
||
import { setUserProfile } from '@/utils/store.uts'
|
||
import { PROFILE_REGION_OPTIONS, PROFILE_COMMON_ADDRESS_SUGGESTIONS } from '@/utils/profileRegionData.uts'
|
||
import type { UserProfile } from '@/types/mall-types.uts'
|
||
|
||
const isLoading = ref<boolean>(false)
|
||
const isSaving = ref<boolean>(false)
|
||
const userAvatar = ref<string>('/static/logo.png')
|
||
const hasLoadError = ref<boolean>(false)
|
||
const registeredDate = ref<string>('暂未记录')
|
||
const profileRowId = ref<string>('')
|
||
const genderOptions: Array<string> = ['male', 'female', 'other']
|
||
const languageOptions: Array<string> = ['zh-CN', 'en-US']
|
||
const customOptionLabel = '自定义其他'
|
||
const healthGoalOptions: Array<string> = ['改善睡眠', '康复护理', '日常保健', '慢病管理', '营养调理', customOptionLabel]
|
||
const carePreferenceOptions: Array<string> = ['上门护理', '陪诊陪护', '康复训练', '定期回访', '电话提醒', customOptionLabel]
|
||
const regionOptions = PROFILE_REGION_OPTIONS
|
||
const commonAddressSuggestions = PROFILE_COMMON_ADDRESS_SUGGESTIONS
|
||
const recentAddressStorageKey = 'medical_profile_recent_addresses'
|
||
const tempGenderIndex = ref<Array<number>>([0])
|
||
const tempLanguageIndex = ref<Array<number>>([0])
|
||
const tempHealthGoalIndex = ref<Array<number>>([0])
|
||
const tempCarePreferenceIndex = ref<Array<number>>([0])
|
||
const tempAddressRegionIndex = ref<Array<number>>([0, 0])
|
||
const showGenderPicker = ref<boolean>(false)
|
||
const showBirthdayPicker = ref<boolean>(false)
|
||
const showLanguagePicker = ref<boolean>(false)
|
||
const showHealthGoalPicker = ref<boolean>(false)
|
||
const showCarePreferencePicker = ref<boolean>(false)
|
||
const showAddressPicker = ref<boolean>(false)
|
||
const showEditorModal = ref<boolean>(false)
|
||
const birthdayYearOptions: Array<number> = []
|
||
for (let year = 1970; year <= new Date().getFullYear(); year++) {
|
||
birthdayYearOptions.push(year)
|
||
}
|
||
const birthdayMonthOptions: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||
const tempBirthdayIndex = ref<Array<number>>([30, 0, 0])
|
||
const tempAddressDetail = ref<string>('')
|
||
const recentAddressSuggestions = ref<Array<string>>([])
|
||
const editorField = ref<string>('')
|
||
const editorTitle = ref<string>('')
|
||
const editorPlaceholder = ref<string>('')
|
||
const editorDraft = ref<string>('')
|
||
const editorInputType = ref<string>('text')
|
||
const editorIsTextarea = ref<boolean>(false)
|
||
|
||
const getProvinceOptions = (): Array<string> => {
|
||
const provinces: Array<string> = []
|
||
for (let i = 0; i < regionOptions.length; i++) {
|
||
provinces.push(regionOptions[i].name)
|
||
}
|
||
return provinces
|
||
}
|
||
|
||
const loadRecentAddressSuggestions = (): void => {
|
||
const rawValue = uni.getStorageSync(recentAddressStorageKey) as string | null
|
||
if (rawValue == null || rawValue == '') {
|
||
recentAddressSuggestions.value = []
|
||
return
|
||
}
|
||
|
||
const list = rawValue.split('||')
|
||
const normalized: Array<string> = []
|
||
for (let i = 0; i < list.length; i++) {
|
||
const item = list[i].trim()
|
||
if (item != '') {
|
||
normalized.push(item)
|
||
}
|
||
}
|
||
recentAddressSuggestions.value = normalized
|
||
}
|
||
|
||
const saveRecentAddressSuggestion = (addressText: string): void => {
|
||
const normalizedAddress = addressText.trim()
|
||
if (normalizedAddress == '') {
|
||
return
|
||
}
|
||
|
||
const updated: Array<string> = [normalizedAddress]
|
||
for (let i = 0; i < recentAddressSuggestions.value.length; i++) {
|
||
const current = recentAddressSuggestions.value[i]
|
||
if (current != normalizedAddress) {
|
||
updated.push(current)
|
||
}
|
||
if (updated.length >= 5) {
|
||
break
|
||
}
|
||
}
|
||
recentAddressSuggestions.value = updated
|
||
uni.setStorageSync(recentAddressStorageKey, updated.join('||'))
|
||
}
|
||
|
||
const profile = ref<UserProfile>({
|
||
id: '',
|
||
username: '',
|
||
email: '',
|
||
gender: 'other',
|
||
birthday: '',
|
||
height_cm: 0,
|
||
weight_kg: 0,
|
||
bio: '',
|
||
avatar_url: '/static/logo.png',
|
||
preferred_language: 'zh-CN',
|
||
health_goal: '',
|
||
service_address: '',
|
||
emergency_contact: '',
|
||
chronic_notes: '',
|
||
care_preference: ''
|
||
} as UserProfile)
|
||
|
||
const goBack = (): void => {
|
||
uni.navigateBack({
|
||
delta: 1
|
||
})
|
||
}
|
||
|
||
const formatDate = (value: string | null): string => {
|
||
if (value == null || value == '') {
|
||
return '暂未记录'
|
||
}
|
||
|
||
const dateText = value.split('T')[0]
|
||
if (dateText != null && dateText != '') {
|
||
return dateText
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
const getAccountName = (): string => {
|
||
const accountId = profile.value.id
|
||
if (accountId == null || accountId == '') {
|
||
return '暂未记录'
|
||
}
|
||
if (accountId.length <= 18) {
|
||
return accountId
|
||
}
|
||
return accountId.substring(0, 8) + '...' + accountId.substring(accountId.length - 6)
|
||
}
|
||
|
||
const getGenderText = (genderCode: string): string => {
|
||
if (genderCode == 'male') {
|
||
return '男'
|
||
} else if (genderCode == 'female') {
|
||
return '女'
|
||
} else {
|
||
return '保密'
|
||
}
|
||
}
|
||
|
||
const getBirthdayText = (): string => {
|
||
const birthday = profile.value.birthday
|
||
if (birthday != null && birthday != '') {
|
||
return birthday
|
||
}
|
||
return '请填写您的生日'
|
||
}
|
||
|
||
const getNicknameText = (): string => {
|
||
const username = profile.value.username
|
||
if (username != null && username != '') {
|
||
return username
|
||
}
|
||
return '请填写昵称'
|
||
}
|
||
|
||
const getBioText = (): string => {
|
||
const bio = profile.value.bio
|
||
if (bio != null && bio != '') {
|
||
return bio
|
||
}
|
||
return '记录你的康养生活'
|
||
}
|
||
|
||
const getHeightText = (): string => {
|
||
const height = profile.value.height_cm
|
||
if (height != null && height > 0) {
|
||
return '' + height + ' cm'
|
||
}
|
||
return '未填写'
|
||
}
|
||
|
||
const getWeightText = (): string => {
|
||
const weight = profile.value.weight_kg
|
||
if (weight != null && weight > 0) {
|
||
return '' + weight + ' kg'
|
||
}
|
||
return '未填写'
|
||
}
|
||
|
||
const getBirthdayDayOptions = (): Array<number> => {
|
||
const year = birthdayYearOptions[tempBirthdayIndex.value[0]]
|
||
const month = birthdayMonthOptions[tempBirthdayIndex.value[1]]
|
||
let maxDay = 31
|
||
if (month == 4 || month == 6 || month == 9 || month == 11) {
|
||
maxDay = 30
|
||
} else if (month == 2) {
|
||
const leapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||
maxDay = leapYear ? 29 : 28
|
||
}
|
||
const days: Array<number> = []
|
||
for (let day = 1; day <= maxDay; day++) {
|
||
days.push(day)
|
||
}
|
||
return days
|
||
}
|
||
|
||
const getBmiText = (): string => {
|
||
const height = profile.value.height_cm
|
||
const weight = profile.value.weight_kg
|
||
if (height == null || weight == null || height <= 0 || weight <= 0) {
|
||
return '待完善'
|
||
}
|
||
const heightMeter = height / 100.0
|
||
const bmi = weight / (heightMeter * heightMeter)
|
||
const rounded = Math.round(bmi * 10.0) / 10.0
|
||
return '' + rounded
|
||
}
|
||
|
||
const getLanguageText = (languageCode: string | null): string => {
|
||
if (languageCode == 'en-US') {
|
||
return 'English'
|
||
}
|
||
return '中文'
|
||
}
|
||
|
||
const getTextOrPlaceholder = (fieldValue: string | null | undefined, placeholder: string): string => {
|
||
if (fieldValue != null && fieldValue != '') {
|
||
return fieldValue
|
||
}
|
||
return placeholder
|
||
}
|
||
|
||
const extractAddressDetail = (addressText: string, province: string, city: string): string => {
|
||
let detail = addressText.trim()
|
||
const provinceIndex = detail.indexOf(province)
|
||
if (provinceIndex >= 0) {
|
||
detail = detail.substring(provinceIndex + province.length).trim()
|
||
}
|
||
|
||
if (city != '' && city != province) {
|
||
while (detail.indexOf(city) == 0) {
|
||
detail = detail.substring(city.length).trim()
|
||
}
|
||
} else {
|
||
while (detail.indexOf(province) == 0) {
|
||
detail = detail.substring(province.length).trim()
|
||
}
|
||
}
|
||
|
||
return detail
|
||
}
|
||
|
||
const normalizeServiceAddress = (addressText: string | null | undefined): string => {
|
||
if (addressText == null || addressText == '') {
|
||
return ''
|
||
}
|
||
|
||
const normalizedText = addressText.trim()
|
||
const provinceOptions = getProvinceOptions()
|
||
for (let i = 0; i < provinceOptions.length; i++) {
|
||
const province = provinceOptions[i]
|
||
if (normalizedText.indexOf(province) >= 0) {
|
||
const cityOptions = getCityOptions(province)
|
||
let matchedCity = ''
|
||
for (let j = 0; j < cityOptions.length; j++) {
|
||
const city = cityOptions[j]
|
||
if (normalizedText.indexOf(city) >= 0) {
|
||
matchedCity = city
|
||
break
|
||
}
|
||
}
|
||
|
||
const defaultCity = cityOptions.length > 0 ? cityOptions[0] : ''
|
||
const city = matchedCity != '' ? matchedCity : defaultCity
|
||
const detail = extractAddressDetail(normalizedText, province, matchedCity)
|
||
return buildRegionAddress(province, city, detail)
|
||
}
|
||
}
|
||
|
||
return normalizedText
|
||
}
|
||
|
||
const getServiceAddressText = (): string => {
|
||
const normalizedAddress = normalizeServiceAddress(profile.value.service_address)
|
||
return getTextOrPlaceholder(normalizedAddress, '请选择服务地址')
|
||
}
|
||
|
||
const getProvinceIndexByName = (province: string): number => {
|
||
const provinceOptions = getProvinceOptions()
|
||
for (let i = 0; i < provinceOptions.length; i++) {
|
||
if (provinceOptions[i] == province) {
|
||
return i
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
const getCityOptions = (province: string): Array<string> => {
|
||
const cities: Array<string> = []
|
||
const provinceIndex = getProvinceIndexByName(province)
|
||
const selectedProvince = regionOptions[provinceIndex]
|
||
for (let i = 0; i < selectedProvince.cities.length; i++) {
|
||
cities.push(selectedProvince.cities[i].name)
|
||
}
|
||
return cities
|
||
}
|
||
|
||
const getCurrentCityOptions = (): Array<string> => {
|
||
const provinceOptions = getProvinceOptions()
|
||
return getCityOptions(provinceOptions[tempAddressRegionIndex.value[0]])
|
||
}
|
||
|
||
const buildRegionAddress = (province: string, city: string, detail: string): string => {
|
||
const cleanDetail = detail.trim()
|
||
let regionText = province
|
||
|
||
if (city != '' && city != province) {
|
||
regionText = regionText + city
|
||
}
|
||
|
||
if (cleanDetail != '') {
|
||
return regionText + ' ' + cleanDetail
|
||
}
|
||
|
||
return regionText
|
||
}
|
||
|
||
const syncAddressPickerWithText = (addressText: string): void => {
|
||
const normalizedText = addressText.trim()
|
||
const provinceOptions = getProvinceOptions()
|
||
let provinceIndex = 0
|
||
for (let i = 0; i < provinceOptions.length; i++) {
|
||
if (normalizedText.indexOf(provinceOptions[i]) >= 0) {
|
||
provinceIndex = i
|
||
break
|
||
}
|
||
}
|
||
|
||
const province = provinceOptions[provinceIndex]
|
||
const cityOptions = getCityOptions(province)
|
||
let cityIndex = 0
|
||
let matchedCity = ''
|
||
for (let i = 0; i < cityOptions.length; i++) {
|
||
if (normalizedText.indexOf(cityOptions[i]) >= 0) {
|
||
cityIndex = i
|
||
matchedCity = cityOptions[i]
|
||
break
|
||
}
|
||
}
|
||
|
||
tempAddressRegionIndex.value = [provinceIndex, cityIndex]
|
||
tempAddressDetail.value = extractAddressDetail(normalizedText, province, matchedCity)
|
||
}
|
||
|
||
const useAddressSuggestion = (addressText: string): void => {
|
||
syncAddressPickerWithText(addressText)
|
||
}
|
||
|
||
const isValidEmergencyContact = (value: string): boolean => {
|
||
if (value == '') {
|
||
return true
|
||
}
|
||
|
||
const normalized = value.replace(',', ' ').replace(',', ' ').replace(';', ' ').replace(';', ' ').trim()
|
||
if (normalized.length < 2) {
|
||
return false
|
||
}
|
||
|
||
let digitText = ''
|
||
for (let i = 0; i < normalized.length; i++) {
|
||
const char = normalized.charAt(i)
|
||
if (char >= '0' && char <= '9') {
|
||
digitText = digitText + char
|
||
}
|
||
}
|
||
|
||
if (digitText == '') {
|
||
return normalized.length >= 2 && normalized.length <= 20
|
||
}
|
||
|
||
if (digitText.length != 11) {
|
||
return false
|
||
}
|
||
|
||
return digitText.charAt(0) == '1'
|
||
}
|
||
|
||
const openTextEditor = (fieldName: string): void => {
|
||
editorField.value = fieldName
|
||
editorInputType.value = 'text'
|
||
editorIsTextarea.value = false
|
||
|
||
if (fieldName == 'username') {
|
||
editorTitle.value = '编辑昵称'
|
||
editorPlaceholder.value = '请填写昵称'
|
||
editorDraft.value = profile.value.username ?? ''
|
||
} else if (fieldName == 'bio') {
|
||
editorTitle.value = '编辑个性签名'
|
||
editorPlaceholder.value = '记录你的康养生活'
|
||
editorDraft.value = profile.value.bio ?? ''
|
||
editorIsTextarea.value = true
|
||
} else if (fieldName == 'height_cm') {
|
||
editorTitle.value = '编辑身高'
|
||
editorPlaceholder.value = '请输入身高(cm)'
|
||
editorDraft.value = profile.value.height_cm != null && profile.value.height_cm > 0 ? '' + profile.value.height_cm : ''
|
||
editorInputType.value = 'number'
|
||
} else if (fieldName == 'weight_kg') {
|
||
editorTitle.value = '编辑体重'
|
||
editorPlaceholder.value = '请输入体重(kg)'
|
||
editorDraft.value = profile.value.weight_kg != null && profile.value.weight_kg > 0 ? '' + profile.value.weight_kg : ''
|
||
editorInputType.value = 'number'
|
||
} else if (fieldName == 'health_goal') {
|
||
editorTitle.value = '自定义健康目标'
|
||
editorPlaceholder.value = '请输入健康目标'
|
||
editorDraft.value = profile.value.health_goal == customOptionLabel ? '' : profile.value.health_goal ?? ''
|
||
} else if (fieldName == 'care_preference') {
|
||
editorTitle.value = '自定义护理偏好'
|
||
editorPlaceholder.value = '请输入护理偏好'
|
||
editorDraft.value = profile.value.care_preference == customOptionLabel ? '' : profile.value.care_preference ?? ''
|
||
} else if (fieldName == 'emergency_contact') {
|
||
editorTitle.value = '编辑紧急联系人'
|
||
editorPlaceholder.value = '请输入姓名或联系方式'
|
||
editorDraft.value = profile.value.emergency_contact ?? ''
|
||
} else if (fieldName == 'chronic_notes') {
|
||
editorTitle.value = '编辑慢病/过敏备注'
|
||
editorPlaceholder.value = '请输入慢病或过敏备注'
|
||
editorDraft.value = profile.value.chronic_notes ?? ''
|
||
editorIsTextarea.value = true
|
||
} else {
|
||
editorTitle.value = '编辑信息'
|
||
editorPlaceholder.value = '请输入内容'
|
||
editorDraft.value = ''
|
||
}
|
||
|
||
showEditorModal.value = true
|
||
}
|
||
|
||
const closeEditorModal = (): void => {
|
||
showEditorModal.value = false
|
||
}
|
||
|
||
const onEditorInput = (e: UniInputEvent): void => {
|
||
editorDraft.value = e.detail.value
|
||
}
|
||
|
||
const confirmEditorModal = (): void => {
|
||
const draft = editorDraft.value.trim()
|
||
if (editorField.value == 'emergency_contact' && !isValidEmergencyContact(draft)) {
|
||
uni.showToast({
|
||
title: '联系人格式不正确',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
if (editorField.value == 'username') {
|
||
profile.value.username = draft
|
||
} else if (editorField.value == 'bio') {
|
||
profile.value.bio = draft
|
||
} else if (editorField.value == 'height_cm') {
|
||
profile.value.height_cm = draft == '' ? 0 : parseInt(draft)
|
||
} else if (editorField.value == 'weight_kg') {
|
||
profile.value.weight_kg = draft == '' ? 0 : parseInt(draft)
|
||
} else if (editorField.value == 'health_goal') {
|
||
profile.value.health_goal = draft
|
||
} else if (editorField.value == 'care_preference') {
|
||
profile.value.care_preference = draft
|
||
} else if (editorField.value == 'emergency_contact') {
|
||
profile.value.emergency_contact = draft
|
||
} else if (editorField.value == 'chronic_notes') {
|
||
profile.value.chronic_notes = draft
|
||
}
|
||
showEditorModal.value = false
|
||
}
|
||
|
||
const openHealthGoalPicker = (): void => {
|
||
const currentValue = profile.value.health_goal
|
||
const idx = currentValue != null ? healthGoalOptions.indexOf(currentValue) : -1
|
||
tempHealthGoalIndex.value = [idx >= 0 ? idx : 0]
|
||
showHealthGoalPicker.value = true
|
||
}
|
||
|
||
const onHealthGoalPickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const idx = e.detail.value[0]
|
||
tempHealthGoalIndex.value = [(idx >= 0 && idx < healthGoalOptions.length) ? idx : 0]
|
||
}
|
||
|
||
const confirmHealthGoalPicker = (): void => {
|
||
const selectedValue = healthGoalOptions[tempHealthGoalIndex.value[0]]
|
||
showHealthGoalPicker.value = false
|
||
if (selectedValue == customOptionLabel) {
|
||
openTextEditor('health_goal')
|
||
return
|
||
}
|
||
profile.value.health_goal = selectedValue
|
||
}
|
||
|
||
const openCarePreferencePicker = (): void => {
|
||
const currentValue = profile.value.care_preference
|
||
const idx = currentValue != null ? carePreferenceOptions.indexOf(currentValue) : -1
|
||
tempCarePreferenceIndex.value = [idx >= 0 ? idx : 0]
|
||
showCarePreferencePicker.value = true
|
||
}
|
||
|
||
const onCarePreferencePickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const idx = e.detail.value[0]
|
||
tempCarePreferenceIndex.value = [(idx >= 0 && idx < carePreferenceOptions.length) ? idx : 0]
|
||
}
|
||
|
||
const confirmCarePreferencePicker = (): void => {
|
||
const selectedValue = carePreferenceOptions[tempCarePreferenceIndex.value[0]]
|
||
showCarePreferencePicker.value = false
|
||
if (selectedValue == customOptionLabel) {
|
||
openTextEditor('care_preference')
|
||
return
|
||
}
|
||
profile.value.care_preference = selectedValue
|
||
}
|
||
|
||
const openAddressPicker = (): void => {
|
||
const addressText = profile.value.service_address ?? ''
|
||
syncAddressPickerWithText(addressText)
|
||
showAddressPicker.value = true
|
||
}
|
||
|
||
const onAddressPickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const values = e.detail.value
|
||
const provinceOptions = getProvinceOptions()
|
||
let provinceIndex = values[0]
|
||
if (provinceIndex < 0 || provinceIndex >= provinceOptions.length) {
|
||
provinceIndex = 0
|
||
}
|
||
|
||
const cityOptions = getCityOptions(provinceOptions[provinceIndex])
|
||
let cityIndex = values[1]
|
||
if (cityIndex < 0 || cityIndex >= cityOptions.length) {
|
||
cityIndex = 0
|
||
}
|
||
|
||
tempAddressRegionIndex.value = [provinceIndex, cityIndex]
|
||
}
|
||
|
||
const onAddressDetailInput = (e: UniInputEvent): void => {
|
||
tempAddressDetail.value = e.detail.value
|
||
}
|
||
|
||
const closeAddressPicker = (): void => {
|
||
showAddressPicker.value = false
|
||
}
|
||
|
||
const confirmAddressPicker = (): void => {
|
||
const provinceOptions = getProvinceOptions()
|
||
const province = provinceOptions[tempAddressRegionIndex.value[0]]
|
||
const cityOptions = getCurrentCityOptions()
|
||
const city = cityOptions[tempAddressRegionIndex.value[1]]
|
||
profile.value.service_address = buildRegionAddress(province, city, tempAddressDetail.value)
|
||
saveRecentAddressSuggestion(profile.value.service_address ?? '')
|
||
showAddressPicker.value = false
|
||
}
|
||
|
||
const openLanguagePicker = (): void => {
|
||
const languageValue = profile.value.preferred_language
|
||
const idx = languageValue != null ? languageOptions.indexOf(languageValue) : -1
|
||
tempLanguageIndex.value = [idx >= 0 ? idx : 0]
|
||
showLanguagePicker.value = true
|
||
}
|
||
|
||
const onLanguagePickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const idx = e.detail.value[0]
|
||
tempLanguageIndex.value = [(idx >= 0 && idx < languageOptions.length) ? idx : 0]
|
||
}
|
||
|
||
const confirmLanguagePicker = (): void => {
|
||
profile.value.preferred_language = languageOptions[tempLanguageIndex.value[0]]
|
||
showLanguagePicker.value = false
|
||
}
|
||
|
||
const loadProfile = async (): Promise<void> => {
|
||
isLoading.value = true
|
||
hasLoadError.value = false
|
||
registeredDate.value = '暂未记录'
|
||
|
||
const user = supa.user
|
||
if (user == null) {
|
||
hasLoadError.value = true
|
||
isLoading.value = false
|
||
return
|
||
}
|
||
|
||
const userEmail = user.getString('email')
|
||
if (userEmail == null || userEmail == '') {
|
||
hasLoadError.value = true
|
||
isLoading.value = false
|
||
return
|
||
}
|
||
|
||
const userCreatedAt = user.getString('created_at')
|
||
if (userCreatedAt != null && userCreatedAt != '') {
|
||
registeredDate.value = formatDate(userCreatedAt)
|
||
}
|
||
|
||
const result = await supa
|
||
.from('ak_users')
|
||
.select('*', {} as UTSJSONObject)
|
||
.or('id.eq.' + (user.id as string) + ',auth_id.eq.' + (user.id as string))
|
||
.execute()
|
||
const data = result.data
|
||
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
const prodata = data[0] as UTSJSONObject
|
||
profileRowId.value = prodata.getString('id') ?? (user.id as string)
|
||
const p: UserProfile = {
|
||
id: profileRowId.value,
|
||
username: prodata.getString('username') ?? '',
|
||
email: prodata.getString('email') ?? '',
|
||
gender: prodata.getString('gender') ?? 'other',
|
||
birthday: prodata.getString('birthday') ?? '',
|
||
height_cm: prodata.getNumber('height_cm') ?? 0,
|
||
weight_kg: prodata.getNumber('weight_kg') ?? 0,
|
||
bio: prodata.getString('bio') ?? '',
|
||
avatar_url: prodata.getString('avatar_url') ?? '/static/logo.png',
|
||
preferred_language: prodata.getString('preferred_language') ?? 'zh-CN',
|
||
health_goal: prodata.getString('health_goal') ?? '',
|
||
service_address: prodata.getString('service_address') ?? '',
|
||
emergency_contact: prodata.getString('emergency_contact') ?? '',
|
||
chronic_notes: prodata.getString('chronic_notes') ?? '',
|
||
care_preference: prodata.getString('care_preference') ?? ''
|
||
} as UserProfile
|
||
p.service_address = normalizeServiceAddress(p.service_address)
|
||
profile.value = p
|
||
const createdAt = prodata.getString('created_at')
|
||
if (createdAt != null && createdAt != '') {
|
||
registeredDate.value = formatDate(createdAt)
|
||
}
|
||
|
||
if (p.avatar_url != null && p.avatar_url != '') {
|
||
userAvatar.value = p.avatar_url!
|
||
}
|
||
|
||
setUserProfile(p)
|
||
} else {
|
||
profileRowId.value = user.getString('id') ?? ''
|
||
profile.value.id = profileRowId.value
|
||
profile.value.username = user.getString('username') ?? ''
|
||
profile.value.email = user.getString('email') ?? ''
|
||
if (profile.value.avatar_url != null && profile.value.avatar_url != '') {
|
||
userAvatar.value = profile.value.avatar_url!
|
||
}
|
||
|
||
if (profile.value.username == '') {
|
||
const emailStr = profile.value.email
|
||
if (emailStr != null && emailStr != '') {
|
||
const parts = emailStr.split('@')
|
||
if (parts.length > 0) {
|
||
profile.value.username = parts[0]
|
||
}
|
||
}
|
||
}
|
||
profile.value.service_address = normalizeServiceAddress(profile.value.service_address)
|
||
|
||
const newProfile = new UTSJSONObject({
|
||
id: profileRowId.value,
|
||
auth_id: user.getString('id') ?? '',
|
||
username: profile.value.username,
|
||
email: profile.value.email,
|
||
gender: profile.value.gender,
|
||
health_goal: profile.value.health_goal,
|
||
service_address: profile.value.service_address,
|
||
emergency_contact: profile.value.emergency_contact,
|
||
chronic_notes: profile.value.chronic_notes,
|
||
care_preference: profile.value.care_preference
|
||
})
|
||
|
||
const insertResult = await supa.from('ak_users').insert(newProfile).execute()
|
||
if (insertResult.error == null) {
|
||
const newProfileData: UserProfile = {
|
||
id: profileRowId.value,
|
||
username: profile.value.username,
|
||
email: profile.value.email,
|
||
gender: profile.value.gender,
|
||
health_goal: profile.value.health_goal,
|
||
service_address: profile.value.service_address,
|
||
emergency_contact: profile.value.emergency_contact,
|
||
chronic_notes: profile.value.chronic_notes,
|
||
care_preference: profile.value.care_preference
|
||
} as UserProfile
|
||
setUserProfile(newProfileData)
|
||
}
|
||
}
|
||
|
||
isLoading.value = false
|
||
}
|
||
|
||
const saveProfile = async (): Promise<void> => {
|
||
isSaving.value = true
|
||
|
||
try {
|
||
const userid: string = profileRowId.value != '' ? profileRowId.value : (profile.value.id ?? '')
|
||
const birthdayValue = profile.value.birthday != null ? profile.value.birthday.trim() : ''
|
||
console.log('saveProfile context:', JSON.stringify({
|
||
profileRowId: profileRowId.value,
|
||
profileId: profile.value.id,
|
||
birthday: birthdayValue != '' ? birthdayValue : null,
|
||
hasHealthGoal: (profile.value.health_goal ?? '') != '',
|
||
hasServiceAddress: (profile.value.service_address ?? '') != ''
|
||
}))
|
||
const baseUpdateData = new UTSJSONObject()
|
||
baseUpdateData.set('username', profile.value.username)
|
||
baseUpdateData.set('gender', profile.value.gender)
|
||
baseUpdateData.set('birthday', birthdayValue != '' ? birthdayValue : null)
|
||
baseUpdateData.set('height_cm', profile.value.height_cm)
|
||
baseUpdateData.set('weight_kg', profile.value.weight_kg)
|
||
baseUpdateData.set('bio', profile.value.bio)
|
||
baseUpdateData.set('avatar_url', profile.value.avatar_url)
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('username', profile.value.username)
|
||
updateData.set('gender', profile.value.gender)
|
||
updateData.set('birthday', birthdayValue != '' ? birthdayValue : null)
|
||
updateData.set('height_cm', profile.value.height_cm)
|
||
updateData.set('weight_kg', profile.value.weight_kg)
|
||
updateData.set('bio', profile.value.bio)
|
||
updateData.set('avatar_url', profile.value.avatar_url)
|
||
updateData.set('health_goal', profile.value.health_goal)
|
||
updateData.set('service_address', profile.value.service_address)
|
||
updateData.set('emergency_contact', profile.value.emergency_contact)
|
||
updateData.set('chronic_notes', profile.value.chronic_notes)
|
||
updateData.set('care_preference', profile.value.care_preference)
|
||
|
||
let result = await supa
|
||
.from('ak_users')
|
||
.update(updateData)
|
||
.eq('id', userid)
|
||
.execute()
|
||
|
||
if (result.error == null) {
|
||
setUserProfile(profile.value)
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success'
|
||
})
|
||
} else {
|
||
console.log('saveProfile update ak_users error:', JSON.stringify(result.error))
|
||
result = await supa
|
||
.from('ak_users')
|
||
.update(baseUpdateData)
|
||
.eq('id', userid)
|
||
.execute()
|
||
|
||
if (result.error == null) {
|
||
setUserProfile(profile.value)
|
||
uni.showToast({
|
||
title: '基础资料已保存,康养字段待后端升级',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
console.log('saveProfile fallback update ak_users error:', JSON.stringify(result.error))
|
||
uni.showToast({
|
||
title: '保存失败,请稍后重试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('saveProfile exception:', e)
|
||
uni.showToast({
|
||
title: '保存失败,请稍后重试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
isSaving.value = false
|
||
}
|
||
|
||
const getUuid = (): string => {
|
||
return `${Date.now()}_${Math.floor(Math.random() * 1e8)}`
|
||
}
|
||
|
||
const chooseAvatar = (): void => {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res: ChooseImageSuccess) => {
|
||
const upfilepath = res.tempFilePaths[0]
|
||
const userId = profile.value.id ?? ''
|
||
let ext = 'png'
|
||
|
||
const tempFiles = res.tempFiles
|
||
if (Array.isArray(tempFiles) && tempFiles.length > 0) {
|
||
const fileObj: ChooseImageTempFile = tempFiles[0]
|
||
const fileName = fileObj.name
|
||
if (fileName != null && fileName != '') {
|
||
const idx = fileName.lastIndexOf('.')
|
||
if (idx >= 0) {
|
||
ext = fileName.substring(idx + 1)
|
||
}
|
||
}
|
||
}
|
||
|
||
const uuid = getUuid()
|
||
const remotePath = `profiles/${userId}_${uuid}.${ext}`
|
||
|
||
supa.storage.from('zhipao').upload(remotePath, upfilepath, {}).then((uploadResult) => {
|
||
if (uploadResult.status == 200 || uploadResult.status == 201) {
|
||
const data = uploadResult.data
|
||
if (data != null) {
|
||
const dataObj = data as UTSJSONObject
|
||
let avatarUrl = dataObj.getString('Key')
|
||
if (avatarUrl != null && avatarUrl != '') {
|
||
avatarUrl = 'https://ak3.oulog.com/storage/v1/object/public/' + avatarUrl
|
||
userAvatar.value = avatarUrl
|
||
profile.value.avatar_url = avatarUrl
|
||
saveProfile()
|
||
}
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const showGenderPickerNow = (): void => {
|
||
const genderValue = profile.value.gender
|
||
const idx = genderValue != null ? genderOptions.indexOf(genderValue) : -1
|
||
tempGenderIndex.value = [idx >= 0 ? idx : 0]
|
||
showGenderPicker.value = true
|
||
}
|
||
|
||
const onGenderPickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const idx = e.detail.value[0]
|
||
tempGenderIndex.value = [(idx >= 0 && idx < genderOptions.length) ? idx : 0]
|
||
}
|
||
|
||
const confirmGenderPicker = (): void => {
|
||
profile.value.gender = genderOptions[tempGenderIndex.value[0]]
|
||
showGenderPicker.value = false
|
||
}
|
||
|
||
const onBirthdayPickerViewChange = (e: UniPickerViewChangeEvent): void => {
|
||
const values = e.detail.value
|
||
let yearIndex = values[0]
|
||
if (yearIndex < 0 || yearIndex >= birthdayYearOptions.length) {
|
||
yearIndex = 0
|
||
}
|
||
let monthIndex = values[1]
|
||
if (monthIndex < 0 || monthIndex >= birthdayMonthOptions.length) {
|
||
monthIndex = 0
|
||
}
|
||
const nextIndex = [yearIndex, monthIndex, 0]
|
||
tempBirthdayIndex.value = nextIndex
|
||
const dayOptions = getBirthdayDayOptions()
|
||
let dayIndex = values[2]
|
||
if (dayIndex < 0 || dayIndex >= dayOptions.length) {
|
||
dayIndex = 0
|
||
}
|
||
tempBirthdayIndex.value = [yearIndex, monthIndex, dayIndex]
|
||
}
|
||
|
||
const showBirthdayPickernow = (): void => {
|
||
const birthday = profile.value.birthday
|
||
if (birthday != null && birthday != '') {
|
||
const parts = birthday.split('-')
|
||
if (parts.length == 3) {
|
||
const yearValue = parseInt(parts[0])
|
||
const monthValue = parseInt(parts[1])
|
||
const dayValue = parseInt(parts[2])
|
||
let yearIndex = birthdayYearOptions.indexOf(yearValue)
|
||
if (yearIndex < 0) {
|
||
yearIndex = 0
|
||
}
|
||
let monthIndex = birthdayMonthOptions.indexOf(monthValue)
|
||
if (monthIndex < 0) {
|
||
monthIndex = 0
|
||
}
|
||
tempBirthdayIndex.value = [yearIndex, monthIndex, 0]
|
||
const dayOptions = getBirthdayDayOptions()
|
||
let dayIndex = dayOptions.indexOf(dayValue)
|
||
if (dayIndex < 0) {
|
||
dayIndex = 0
|
||
}
|
||
tempBirthdayIndex.value = [yearIndex, monthIndex, dayIndex]
|
||
}
|
||
} else {
|
||
tempBirthdayIndex.value = [birthdayYearOptions.indexOf(2000), 0, 0]
|
||
}
|
||
showBirthdayPicker.value = true
|
||
}
|
||
|
||
const confirmBirthdayPicker = (): void => {
|
||
showBirthdayPicker.value = false
|
||
const y = birthdayYearOptions[tempBirthdayIndex.value[0]]
|
||
const m = birthdayMonthOptions[tempBirthdayIndex.value[1]]
|
||
const dayOptions = getBirthdayDayOptions()
|
||
const d = dayOptions[tempBirthdayIndex.value[2]]
|
||
const mm = m < 10 ? '0' + m : '' + m
|
||
const dd = d < 10 ? '0' + d : '' + d
|
||
profile.value.birthday = `${y}-${mm}-${dd}`
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadRecentAddressSuggestions()
|
||
loadProfile()
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.page-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f6f6f6;
|
||
height: 100%;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.custom-nav {
|
||
height: 96rpx;
|
||
background-color: #ffffff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24rpx;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f0f0f0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nav-left,
|
||
.nav-right {
|
||
width: 120rpx;
|
||
height: 96rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-right {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.nav-icon {
|
||
font-size: 36rpx;
|
||
color: #222;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.nav-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
color: #222;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.profile-scroll {
|
||
flex: 1;
|
||
height: 0;
|
||
}
|
||
|
||
.profile-content-wrap {
|
||
min-height: 100%;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.profile-content {
|
||
padding-top: 20rpx;
|
||
}
|
||
|
||
.profile-card {
|
||
margin: 20rpx;
|
||
padding: 8rpx 0;
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.card-title {
|
||
padding: 18rpx 28rpx 8rpx 28rpx;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
min-height: 102rpx;
|
||
padding: 0 28rpx;
|
||
box-sizing: border-box;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f4f4f4;
|
||
}
|
||
|
||
.avatar-row {
|
||
min-height: 130rpx;
|
||
}
|
||
|
||
.last-row {
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.info-label {
|
||
width: 180rpx;
|
||
font-size: 28rpx;
|
||
color: #222;
|
||
}
|
||
|
||
.info-value-wrap {
|
||
flex-shrink: 0;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-width: 0;
|
||
}
|
||
|
||
.avatar-value-wrap {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.info-value {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
text-align: right;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.info-arrow {
|
||
width: 28rpx;
|
||
margin-left: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #c7c7c7;
|
||
text-align: right;
|
||
}
|
||
|
||
.avatar-image {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
background-color: #ededed;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 80%;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.error-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding-top: 240rpx;
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 28rpx;
|
||
color: #f44336;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.retry-button {
|
||
width: 240rpx;
|
||
height: 84rpx;
|
||
font-size: 28rpx;
|
||
background-color: #2f80ed;
|
||
color: white;
|
||
border-radius: 42rpx;
|
||
}
|
||
|
||
.save-button {
|
||
margin: 28rpx 20rpx 0 20rpx;
|
||
width: auto;
|
||
height: 90rpx;
|
||
font-size: 32rpx;
|
||
border-radius: 45rpx;
|
||
background-color: #21b883;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
}
|
||
|
||
.save-button:disabled {
|
||
background: #ccc;
|
||
}
|
||
|
||
.picker-modal {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #fff;
|
||
z-index: 1000;
|
||
padding-bottom: 30rpx;
|
||
border-top-left-radius: 20rpx;
|
||
border-top-right-radius: 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.picker-view {
|
||
width: 750rpx;
|
||
height: 320px;
|
||
background: #fff;
|
||
}
|
||
|
||
.picker-item {
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 750rpx;
|
||
}
|
||
|
||
.birthday-picker-item {
|
||
width: 250rpx;
|
||
}
|
||
|
||
.picker-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 750rpx;
|
||
padding: 20rpx 40rpx 0 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.picker-action-button {
|
||
width: 300rpx;
|
||
height: 84rpx;
|
||
margin: 0;
|
||
border-radius: 16rpx;
|
||
font-size: 28rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.picker-action-cancel {
|
||
background: #f2f3f5;
|
||
color: #666;
|
||
}
|
||
|
||
.picker-action-confirm {
|
||
background: #21b883;
|
||
color: #fff;
|
||
}
|
||
|
||
.editor-sheet,
|
||
.option-sheet {
|
||
width: 750rpx;
|
||
background: #fff;
|
||
border-top-left-radius: 20rpx;
|
||
border-top-right-radius: 20rpx;
|
||
padding: 30rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.editor-title {
|
||
font-size: 30rpx;
|
||
color: #222;
|
||
text-align: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.editor-input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
padding: 0 24rpx;
|
||
background-color: #f7f7f7;
|
||
border-radius: 16rpx;
|
||
box-sizing: border-box;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.editor-textarea {
|
||
width: 100%;
|
||
height: 180rpx;
|
||
padding: 24rpx;
|
||
background-color: #f7f7f7;
|
||
border-radius: 16rpx;
|
||
box-sizing: border-box;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.editor-actions {
|
||
width: 100%;
|
||
padding: 30rpx 0 0 0;
|
||
}
|
||
|
||
.address-sheet {
|
||
width: 750rpx;
|
||
background: #fff;
|
||
border-top-left-radius: 20rpx;
|
||
border-top-right-radius: 20rpx;
|
||
padding: 30rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.address-picker-view {
|
||
width: 100%;
|
||
height: 320px;
|
||
background: #fff;
|
||
}
|
||
|
||
.address-suggestion-block {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.address-suggestion-title {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.address-tag-list {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.address-tag {
|
||
max-width: 100%;
|
||
padding: 12rpx 18rpx;
|
||
margin-right: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
background-color: #ecf8f2;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.address-tag-light {
|
||
background-color: #f5f6f8;
|
||
}
|
||
|
||
.address-tag-text {
|
||
font-size: 24rpx;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.address-picker-column {
|
||
width: 345rpx;
|
||
}
|
||
|
||
.address-picker-item {
|
||
width: 345rpx;
|
||
}
|
||
|
||
.address-detail-input {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.option-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 96rpx;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f0f0f0;
|
||
}
|
||
|
||
.option-last-row {
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
</style>
|