613 lines
16 KiB
Plaintext
613 lines
16 KiB
Plaintext
<template>
|
||
<view class="page-container">
|
||
<scroll-view class="address-edit-scroll" scroll-y="true">
|
||
<view class="address-edit-content">
|
||
<!-- 基础信息组 -->
|
||
<view class="form-group">
|
||
<view class="form-item">
|
||
<text class="label">收货人</text>
|
||
<input class="input" v-model="formData.name" placeholder="请填写收货人姓名" placeholder-class="placeholder" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="label">手机号码</text>
|
||
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="请填写手机号码" placeholder-class="placeholder" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="label">所在地区</text>
|
||
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" placeholder-class="placeholder" />
|
||
<text class="arrow-icon">›</text>
|
||
</view>
|
||
<view class="form-item detail-item">
|
||
<text class="label">详细地址</text>
|
||
<textarea class="textarea" v-model="formData.detail" placeholder="街道、楼牌号等" placeholder-class="placeholder" maxlength="100"></textarea>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 标签与默认设置组 -->
|
||
<view class="form-group">
|
||
<view class="form-item label-section">
|
||
<text class="label">地址标签</text>
|
||
<view class="tags-container">
|
||
<view
|
||
v-for="tag in tags"
|
||
:key="tag"
|
||
class="tag-item"
|
||
:class="{ active: formData.label === tag }"
|
||
@click="selectTag(tag)"
|
||
>
|
||
<text class="tag-text" :class="{ 'tag-text-active': formData.label === tag }">{{ tag }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="form-item switch-item">
|
||
<view class="switch-label-group">
|
||
<text class="label">设为默认地址</text>
|
||
<text class="sub-label">下单时优先使用该地址</text>
|
||
</view>
|
||
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 智能识别组 -->
|
||
<view class="form-group smart-group">
|
||
<view class="smart-header">
|
||
<text class="smart-title">智能填写</text>
|
||
<text class="smart-clear" v-if="smartInput" @click="smartInput = ''">清空</text>
|
||
</view>
|
||
<textarea class="smart-textarea" v-model="smartInput" placeholder="粘贴整段地址,自动识别姓名、电话、地址" @input="parseSmartInput" maxlength="200"></textarea>
|
||
<view class="smart-footer">
|
||
<text class="smart-tip">示例:张三,13800138000,北京市朝阳区...</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部操作按钮 -->
|
||
<view class="footer-actions">
|
||
<button class="save-btn" @click="saveAddress">保存地址</button>
|
||
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除此地址</button>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { supabaseService, AddAddressParams, UpdateAddressParams } from '@/utils/supabaseService.uts'
|
||
|
||
type Address = {
|
||
id: string
|
||
name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail: string
|
||
isDefault: boolean
|
||
label?: string
|
||
}
|
||
|
||
const isEdit = ref(false)
|
||
const addressId = ref('')
|
||
const regionString = ref('')
|
||
const tags = ['家', '公司', '学校']
|
||
const smartInput = ref('')
|
||
|
||
type AddressForm = {
|
||
name: string
|
||
phone: string
|
||
detail: string
|
||
isDefault: boolean
|
||
label: string
|
||
}
|
||
|
||
const formData = reactive({
|
||
name: '',
|
||
phone: '',
|
||
detail: '',
|
||
isDefault: false,
|
||
label: ''
|
||
} as AddressForm)
|
||
|
||
const loadAddress = async (id: string) => {
|
||
try {
|
||
// 从Supabase加载地址详情
|
||
const address = await supabaseService.getAddressById(id)
|
||
if (address != null) {
|
||
formData.name = address.recipient_name
|
||
formData.phone = address.phone
|
||
formData.detail = address.detail_address
|
||
formData.isDefault = address.is_default
|
||
formData.label = address.label ?? ''
|
||
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
|
||
} else {
|
||
// 如果Supabase没有找到,尝试从本地存储加载
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
if (storedAddresses != null) {
|
||
const addresses = JSON.parse(storedAddresses as string) as Address[]
|
||
const localAddress = addresses.find(item => item.id === id)
|
||
if (localAddress != null) {
|
||
formData.name = localAddress.name
|
||
formData.phone = localAddress.phone
|
||
formData.detail = localAddress.detail
|
||
formData.isDefault = localAddress.isDefault
|
||
formData.label = localAddress.label ?? ''
|
||
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载地址详情失败:', error)
|
||
// 失败时从本地存储加载
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
if (storedAddresses != null) {
|
||
try {
|
||
const addresses = JSON.parse(storedAddresses as string) as Address[]
|
||
const address = addresses.find(item => item.id === id)
|
||
if (address != null) {
|
||
formData.name = address.name
|
||
formData.phone = address.phone
|
||
formData.detail = address.detail
|
||
formData.isDefault = address.isDefault
|
||
formData.label = address.label ?? ''
|
||
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
|
||
}
|
||
} catch (e) {
|
||
console.error('解析本地地址数据失败', e)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
onLoad((options) => {
|
||
if (options['id'] != null) {
|
||
isEdit.value = true
|
||
addressId.value = options['id'] as string
|
||
loadAddress(addressId.value)
|
||
}
|
||
})
|
||
|
||
const selectTag = (tag: string) => {
|
||
if (formData.label === tag) {
|
||
formData.label = ''
|
||
} else {
|
||
formData.label = tag
|
||
}
|
||
}
|
||
|
||
const onSwitchChange = (e: UniSwitchChangeEvent) => {
|
||
formData.isDefault = e.detail.value
|
||
}
|
||
|
||
const saveAddress = async () => {
|
||
if (formData.name == '') {
|
||
uni.showToast({ title: '请填写收货人', icon: 'none' })
|
||
return
|
||
}
|
||
if (formData.phone == '') {
|
||
uni.showToast({ title: '请填写手机号码', icon: 'none' })
|
||
return
|
||
}
|
||
if (regionString.value == '') {
|
||
uni.showToast({ title: '请填写所在地区', icon: 'none' })
|
||
return
|
||
}
|
||
if (formData.detail == '') {
|
||
uni.showToast({ title: '请填写详细地址', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 简单解析地区(这里简化处理,实际应使用选择器)
|
||
const regions = regionString.value.split(' ')
|
||
const province = regions[0] ?? ''
|
||
const city = regions[1] ?? ''
|
||
const district = regions.slice(2).join(' ')
|
||
|
||
// 构建地址对象
|
||
const addressData = {
|
||
recipient_name: formData.name,
|
||
phone: formData.phone,
|
||
province: province,
|
||
city: city,
|
||
district: district,
|
||
detail_address: formData.detail,
|
||
postal_code: '', // 如果需要可以添加邮政编码字段
|
||
is_default: formData.isDefault,
|
||
label: formData.label
|
||
} as AddAddressParams
|
||
|
||
let success = false
|
||
|
||
if (isEdit.value) {
|
||
// 更新地址
|
||
const updateData = {
|
||
recipient_name: formData.name,
|
||
phone: formData.phone,
|
||
province: province,
|
||
city: city,
|
||
district: district,
|
||
detail_address: formData.detail,
|
||
postal_code: '',
|
||
is_default: formData.isDefault,
|
||
label: formData.label
|
||
} as UpdateAddressParams
|
||
success = await supabaseService.updateAddress(addressId.value, updateData)
|
||
} else {
|
||
// 添加新地址
|
||
success = await supabaseService.addAddress(addressData)
|
||
}
|
||
|
||
if (success) {
|
||
// 同时更新本地存储作为缓存
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
let addresses: Address[] = []
|
||
if (storedAddresses != null) {
|
||
try {
|
||
addresses = JSON.parse(storedAddresses as string) as Address[]
|
||
} catch (e) {
|
||
addresses = []
|
||
}
|
||
}
|
||
|
||
// 如果设为默认,取消其他默认
|
||
if (formData.isDefault) {
|
||
addresses.forEach(item => {
|
||
item.isDefault = false
|
||
})
|
||
}
|
||
|
||
if (isEdit.value) {
|
||
const index = addresses.findIndex(item => item.id === addressId.value)
|
||
if (index !== -1) {
|
||
addresses[index] = {
|
||
...addresses[index],
|
||
name: formData.name,
|
||
phone: formData.phone,
|
||
province: province,
|
||
city: city,
|
||
district: district,
|
||
detail: formData.detail,
|
||
isDefault: formData.isDefault,
|
||
label: formData.label
|
||
}
|
||
}
|
||
} else {
|
||
const newAddress: Address = {
|
||
id: `addr_${Date.now()}`, // 临时ID,实际由Supabase生成
|
||
name: formData.name,
|
||
phone: formData.phone,
|
||
province: province,
|
||
city: city,
|
||
district: district,
|
||
detail: formData.detail,
|
||
isDefault: formData.isDefault,
|
||
label: formData.label
|
||
}
|
||
addresses.push(newAddress)
|
||
}
|
||
|
||
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
} else {
|
||
console.error('保存地址失败')
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
const parseSmartInput = () => {
|
||
const input = smartInput.value.trim()
|
||
if (input == '') return
|
||
|
||
// 提取手机号
|
||
const phoneRegex = /(1[3-9]\d{9})/
|
||
const phoneMatch = input.match(phoneRegex)
|
||
if (phoneMatch != null) {
|
||
formData.phone = phoneMatch[0] ?? ''
|
||
}
|
||
|
||
// 提取姓名(取第一个2-4位中文)
|
||
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
|
||
const nameMatch = input.match(nameRegex)
|
||
if (nameMatch != null) {
|
||
formData.name = nameMatch[0] ?? ''
|
||
}
|
||
|
||
// 去掉姓名和电话后剩余作为地址
|
||
let addrText = input
|
||
if (formData.name != '') addrText = addrText.replace(formData.name, '')
|
||
if (formData.phone != '') addrText = addrText.replace(formData.phone, '')
|
||
addrText = addrText.replace(/[,,;;\s]+/g, ' ').trim()
|
||
|
||
// 解析省市区
|
||
const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/
|
||
const m = addrText.match(pattern1)
|
||
if (m != null) {
|
||
const province = m[1] ?? ''
|
||
const city = m[2] ?? ''
|
||
const district = m[3] ?? ''
|
||
const detail = m[4] ?? ''
|
||
regionString.value = `${province.trim()} ${city.trim()} ${district.trim()}`.trim()
|
||
formData.detail = detail.trim()
|
||
} else {
|
||
formData.detail = addrText
|
||
}
|
||
}
|
||
const deleteAddress = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要删除该地址吗?',
|
||
success: (res: UniShowModalResult) => {
|
||
if (res.confirm) {
|
||
// 调用Supabase服务删除地址
|
||
supabaseService.deleteAddress(addressId.value).then((success) => {
|
||
if (success) {
|
||
// 同时从本地存储中移除
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
if (storedAddresses != null) {
|
||
try {
|
||
let addresses = JSON.parse(storedAddresses as string) as Address[]
|
||
addresses = addresses.filter(item => item.id !== addressId.value)
|
||
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||
} catch (e) {
|
||
console.error('解析本地地址数据失败', e)
|
||
}
|
||
}
|
||
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
} else {
|
||
console.error('删除地址失败')
|
||
uni.showToast({
|
||
title: '删除失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.page-container {
|
||
flex: 1;
|
||
background-color: #f8f8f8;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.address-edit-scroll {
|
||
flex: 1;
|
||
}
|
||
|
||
.address-edit-content {
|
||
padding: 12px;
|
||
padding-bottom: 40px;
|
||
}
|
||
|
||
.form-group {
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
padding: 16px; /* 给整个组增加内边距 */
|
||
margin-bottom: 12px;
|
||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 0 16px;
|
||
min-height: 52px;
|
||
background-color: #f8f8f8;
|
||
border-radius: 26px; /* 增加大圆角,使其从直角变为圆角 */
|
||
margin-bottom: 12px;
|
||
border: none;
|
||
}
|
||
|
||
.form-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.detail-item {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
padding: 16px;
|
||
border-radius: 16px; /* 详细地址区域也增加圆角 */
|
||
}
|
||
|
||
.detail-item .label {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.label {
|
||
width: 80px;
|
||
font-size: 15px;
|
||
color: #666;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.input {
|
||
flex: 1;
|
||
height: 44px; /* 增加高度 */
|
||
line-height: 44px;
|
||
font-size: 16px;
|
||
color: #333;
|
||
padding: 0 4px;
|
||
background-color: transparent; /* 确保输入框背景透明 */
|
||
border: none; /* 强制去除安卓原生边框 */
|
||
}
|
||
|
||
.textarea {
|
||
width: 100%;
|
||
height: 80px;
|
||
font-size: 15px;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
padding: 4px 0;
|
||
background-color: transparent;
|
||
border: none; /* 强制去除安卓原生边框 */
|
||
}
|
||
|
||
.placeholder {
|
||
color: #bbb;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.arrow-icon {
|
||
font-size: 18px;
|
||
color: #ccc;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* 标签选择 */
|
||
.label-section {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.label-section .label {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.tags-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tag-item {
|
||
padding: 8px 20px; /* 增大点击区域 */
|
||
background-color: #f7f7f7;
|
||
border-radius: 20px;
|
||
margin-right: 12px;
|
||
margin-bottom: 8px; /* 增加底部间距 */
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.tag-item.active {
|
||
background-color: #fff1eb;
|
||
border-color: #ff5000;
|
||
}
|
||
|
||
.tag-text {
|
||
font-size: 14px; /* 增大标签文字 */
|
||
color: #666;
|
||
}
|
||
|
||
.tag-text-active {
|
||
color: #ff5000;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 开关项 */
|
||
.switch-item {
|
||
justify-content: space-between;
|
||
min-height: 72px; /* 增加开关项高度 */
|
||
}
|
||
|
||
.switch-label-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sub-label {
|
||
font-size: 13px; /* 增大副标题 */
|
||
color: #999;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
/* 智能填写 */
|
||
.smart-group {
|
||
padding: 16px;
|
||
}
|
||
|
||
.smart-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.smart-title {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.smart-clear {
|
||
font-size: 12px;
|
||
color: #007aff;
|
||
}
|
||
|
||
.smart-textarea {
|
||
width: 100%;
|
||
height: 80px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: #666;
|
||
}
|
||
|
||
.smart-footer {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.smart-tip {
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
/* 底部按钮 */
|
||
.footer-actions {
|
||
margin-top: 32px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.save-btn {
|
||
background-color: #ff5000;
|
||
color: #ffffff;
|
||
height: 48px;
|
||
line-height: 48px;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
border-radius: 24px;
|
||
border: none;
|
||
margin-bottom: 16px;
|
||
box-shadow: 0 8rpx 20rpx rgba(255, 80, 0, 0.2);
|
||
}
|
||
|
||
.delete-btn {
|
||
background-color: #ffffff;
|
||
color: #ee0a24;
|
||
height: 48px;
|
||
line-height: 48px;
|
||
font-size: 16px;
|
||
border-radius: 24px;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
</style>
|
||
|