Files
medical-mall/pages/user/profile.uvue

1837 lines
56 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">{{ getPreferredLanguageText() }}</text>
</view>
<text class="info-arrow"></text>
</view>
</view>
<button class="save-button" :disabled="isSaving" :loading="isSaving" @click="saveProfile(true)">
保存修改
</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.id" class="picker-item">
{{ language.label }}
</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 { 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'
type AkLanguageRow = {
id: string
code: string
name: string
native_name: string
is_active: boolean
is_default: boolean
sort_order: number
created_at?: string
}
type LanguageOption = {
id: string
code: string
name: string
nativeName: string
label: string
isDefault: boolean
sortOrder: number
createdAt: string
}
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 originalProfile = ref<UserProfile | null>(null)
const genderOptions: Array<string> = ['male', 'female', 'other']
const languageOptions = ref<Array<LanguageOption>>([])
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: '',
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 normalizeString = (value: string | null | undefined): string | null => {
if (value == null) {
return null
}
const trimmed = value.trim()
return trimmed == '' ? null : trimmed
}
const normalizeNumber = (value: number | null | undefined): number | null => {
if (value == null) {
return null
}
const numericValue = Number(value)
return numericValue == numericValue ? numericValue : null
}
const normalizeDateString = (value: string | null | undefined): string | null => {
const normalized = normalizeString(value)
if (normalized == null) {
return null
}
const parts = normalized.split('T')
return parts.length > 0 ? parts[0] : normalized
}
const deepCloneProfile = (source: UserProfile): UserProfile => {
return {
id: source.id ?? '',
username: source.username ?? '',
email: source.email ?? '',
gender: source.gender ?? '',
birthday: source.birthday ?? '',
height_cm: source.height_cm ?? 0,
weight_kg: source.weight_kg ?? 0,
bio: source.bio ?? '',
avatar_url: source.avatar_url ?? '',
preferred_language: source.preferred_language ?? '',
health_goal: source.health_goal ?? '',
service_address: source.service_address ?? '',
emergency_contact: source.emergency_contact ?? '',
chronic_notes: source.chronic_notes ?? '',
care_preference: source.care_preference ?? '',
role: source.role ?? '',
school_id: source.school_id ?? '',
grade_id: source.grade_id ?? '',
class_id: source.class_id ?? '',
created_at: source.created_at ?? '',
updated_at: source.updated_at ?? ''
} as UserProfile
}
const getLanguageOptionById = (languageId: string | null | undefined): LanguageOption | null => {
const normalizedId = normalizeString(languageId)
if (normalizedId == null) {
return null
}
for (let i = 0; i < languageOptions.value.length; i++) {
const option = languageOptions.value[i]
if (option.id == normalizedId) {
return option
}
}
return null
}
const getDefaultLanguageOption = (): LanguageOption | null => {
for (let i = 0; i < languageOptions.value.length; i++) {
if (languageOptions.value[i].isDefault == true) {
return languageOptions.value[i]
}
}
for (let i = 0; i < languageOptions.value.length; i++) {
if (languageOptions.value[i].code == 'zh-CN') {
return languageOptions.value[i]
}
}
return languageOptions.value.length > 0 ? languageOptions.value[0] : null
}
const getLanguageLabel = (languageId: string | null): string => {
const language = getLanguageOptionById(languageId)
return language != null ? language.label : '未设置'
}
const getPreferredLanguageText = (): string => {
const currentLanguageId = normalizeString(profile.value.preferred_language)
if (currentLanguageId != null) {
return getLanguageLabel(currentLanguageId)
}
const defaultLanguage = getDefaultLanguageOption()
return defaultLanguage != null ? defaultLanguage.label : '未设置'
}
const loadLanguageOptions = async (): Promise<void> => {
const result = await supa
.from('ak_languages')
.select('id, code, name, native_name, is_active, is_default, sort_order, created_at', {} as UTSJSONObject)
.eq('is_active', true)
.order('sort_order', { ascending: true })
.execute()
if (result.error != null) {
console.error('加载语言列表失败:', JSON.stringify(result.error))
languageOptions.value = []
return
}
const rows = result.data
const options: Array<LanguageOption> = []
if (Array.isArray(rows)) {
for (let i = 0; i < rows.length; i++) {
const item = rows[i] as UTSJSONObject
const row: AkLanguageRow = {
id: item.getString('id') ?? '',
code: item.getString('code') ?? '',
name: item.getString('name') ?? '',
native_name: item.getString('native_name') ?? '',
is_active: item.getBoolean('is_active') ?? false,
is_default: item.getBoolean('is_default') ?? false,
sort_order: item.getNumber('sort_order') ?? 0,
created_at: item.getString('created_at') ?? ''
}
if (row.id == '') {
continue
}
const label = row.native_name != '' ? row.native_name : (row.name != '' ? row.name : row.code)
options.push({
id: row.id,
code: row.code,
name: row.name,
nativeName: row.native_name,
label,
isDefault: row.is_default,
sortOrder: row.sort_order,
createdAt: row.created_at ?? ''
} as LanguageOption)
}
}
for (let i = 0; i < options.length; i++) {
for (let j = i + 1; j < options.length; j++) {
let shouldSwap = false
if (options[j].sortOrder < options[i].sortOrder) {
shouldSwap = true
} else if (options[j].sortOrder == options[i].sortOrder && options[j].createdAt != '' && options[i].createdAt != '' && options[j].createdAt < options[i].createdAt) {
shouldSwap = true
}
if (shouldSwap) {
const temp = options[i]
options[i] = options[j]
options[j] = temp
}
}
}
languageOptions.value = options
}
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 => {
if (languageOptions.value.length == 0) {
uni.showToast({
title: '语言列表加载中',
icon: 'none'
})
return
}
const currentLanguageId = normalizeString(profile.value.preferred_language)
const defaultLanguage = getDefaultLanguageOption()
const targetLanguageId = currentLanguageId != null ? currentLanguageId : (defaultLanguage != null ? defaultLanguage.id : '')
let idx = 0
for (let i = 0; i < languageOptions.value.length; i++) {
if (languageOptions.value[i].id == targetLanguageId) {
idx = i
break
}
}
tempLanguageIndex.value = [idx]
showLanguagePicker.value = true
}
const onLanguagePickerViewChange = (e: UniPickerViewChangeEvent): void => {
const idx = e.detail.value[0]
tempLanguageIndex.value = [(idx >= 0 && idx < languageOptions.value.length) ? idx : 0]
}
const confirmLanguagePicker = (): void => {
if (languageOptions.value.length > 0) {
profile.value.preferred_language = languageOptions.value[tempLanguageIndex.value[0]].id
}
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') ?? '',
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
originalProfile.value = deepCloneProfile(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)
}
originalProfile.value = deepCloneProfile(profile.value)
}
isLoading.value = false
}
const buildProfileUpdatePayload = (): UTSJSONObject => {
const payload = new UTSJSONObject()
const oldProfile = originalProfile.value
const newProfile = profile.value
if (oldProfile == null) {
return payload
}
if (normalizeString(oldProfile.username) != normalizeString(newProfile.username)) {
payload.set('username', normalizeString(newProfile.username))
}
if (normalizeString(oldProfile.avatar_url) != normalizeString(newProfile.avatar_url)) {
payload.set('avatar_url', normalizeString(newProfile.avatar_url))
}
if (normalizeString(oldProfile.gender) != normalizeString(newProfile.gender)) {
payload.set('gender', normalizeString(newProfile.gender))
}
if (normalizeDateString(oldProfile.birthday) != normalizeDateString(newProfile.birthday)) {
payload.set('birthday', normalizeDateString(newProfile.birthday))
}
if (normalizeString(oldProfile.service_address) != normalizeString(newProfile.service_address)) {
payload.set('service_address', normalizeString(newProfile.service_address))
}
if (normalizeString(oldProfile.bio) != normalizeString(newProfile.bio)) {
payload.set('bio', normalizeString(newProfile.bio))
}
if (normalizeNumber(oldProfile.height_cm) != normalizeNumber(newProfile.height_cm)) {
payload.set('height_cm', normalizeNumber(newProfile.height_cm))
}
if (normalizeNumber(oldProfile.weight_kg) != normalizeNumber(newProfile.weight_kg)) {
payload.set('weight_kg', normalizeNumber(newProfile.weight_kg))
}
if (normalizeString(oldProfile.preferred_language) != normalizeString(newProfile.preferred_language)) {
payload.set('preferred_language', normalizeString(newProfile.preferred_language))
}
if (normalizeString(oldProfile.health_goal) != normalizeString(newProfile.health_goal)) {
payload.set('health_goal', normalizeString(newProfile.health_goal))
}
if (normalizeString(oldProfile.emergency_contact) != normalizeString(newProfile.emergency_contact)) {
payload.set('emergency_contact', normalizeString(newProfile.emergency_contact))
}
if (normalizeString(oldProfile.chronic_notes) != normalizeString(newProfile.chronic_notes)) {
payload.set('chronic_notes', normalizeString(newProfile.chronic_notes))
}
if (normalizeString(oldProfile.care_preference) != normalizeString(newProfile.care_preference)) {
payload.set('care_preference', normalizeString(newProfile.care_preference))
}
return payload
}
const saveProfile = async (shouldReturn: boolean = false): Promise<void> => {
const userid: string = profileRowId.value != '' ? profileRowId.value : (profile.value.id ?? '')
const updatePayload = buildProfileUpdatePayload()
if (UTSJSONObject.keys(updatePayload).length == 0) {
uni.showToast({
title: '没有修改内容',
icon: 'none'
})
return
}
isSaving.value = true
try {
updatePayload.set('updated_at', new Date().toISOString())
const result = await supa
.from('ak_users')
.update(updatePayload)
.eq('id', userid)
.execute()
if (result.error == null) {
originalProfile.value = deepCloneProfile(profile.value)
setUserProfile(profile.value)
uni.showToast({
title: '保存成功',
icon: 'success'
})
if (shouldReturn) {
setTimeout((): void => {
uni.navigateBack({
delta: 1
})
}, 500)
}
} else {
console.log('saveProfile 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()
loadLanguageOptions()
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>