Files
medical-mall/pages/mall/consumer/address.uvue

924 lines
20 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="address-page">
<!-- 顶部栏 -->
<view class="address-header">
<view class="header-title">
<text class="title-text">收货地址</text>
</view>
</view>
<!-- 地址列表 -->
<scroll-view class="address-list" scroll-y>
<!-- 地址为空 -->
<view v-if="addressList.length === 0" class="empty-address">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
<text class="empty-subtext">点击下方按钮添加地址</text>
</view>
<!-- 地址项 -->
<view v-for="address in addressList" :key="address.id" class="address-item">
<view class="address-info" @click="selectAddress(address)">
<view class="address-header-row">
<text class="address-name">{{ address.recipient_name }}</text>
<text class="address-phone">{{ address.phone }}</text>
<view v-if="address.is_default" class="default-tag">
<text class="tag-text">默认</text>
</view>
</view>
<view class="address-detail">
<text class="detail-text">{{ getFullAddress(address) }}</text>
</view>
</view>
<view class="address-actions">
<view class="action-item" @click="editAddress(address)">
<text class="action-icon">✏️</text>
<text class="action-text">编辑</text>
</view>
<view class="action-item" @click="deleteAddress(address)">
<text class="action-icon">🗑️</text>
<text class="action-text">删除</text>
</view>
<view v-if="!address.is_default" class="action-item" @click="setDefaultAddress(address)">
<text class="action-icon">⭐</text>
<text class="action-text">设为默认</text>
</view>
</view>
</view>
<!-- 从选择页面返回时的提示 -->
<view v-if="fromSelect && addressList.length > 0" class="select-tip">
<text class="tip-text">请选择收货地址</text>
<text class="tip-subtext">或点击下方添加新地址</text>
</view>
</scroll-view>
<!-- 添加地址按钮 -->
<view class="add-address-btn" @click="showNewAddressForm = true">
<text class="btn-icon">+</text>
<text class="btn-text">添加新地址</text>
</view>
<!-- 新建地址表单弹窗 -->
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
<view class="address-form-popup" @click.stop>
<view class="form-header">
<text class="form-title">新建收货地址</text>
<text class="form-close" @click="cancelNewAddress">×</text>
</view>
<scroll-view class="form-content" scroll-y>
<view class="form-item">
<text class="form-label">收货人</text>
<input class="form-input" v-model="newAddress.recipient_name"
placeholder="请输入收货人姓名" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="newAddress.phone"
placeholder="请输入手机号码" type="number" />
</view>
<!-- 地址智能填写 -->
<view class="form-item">
<text class="form-label">智能填写地址</text>
<textarea class="form-textarea smart-address-input"
v-model="smartAddressInput"
placeholder="请输入完整地址,系统将自动识别省市区和详细地址"
@blur="parseSmartAddress"
maxlength="200" />
<text class="smart-tip">例如北京市朝阳区三里屯SOHO A座</text>
</view>
<view class="form-item">
<text class="form-label">所在地区</text>
<view class="region-inputs">
<input class="form-input region-input" v-model="newAddress.province"
placeholder="省" readonly />
<input class="form-input region-input" v-model="newAddress.city"
placeholder="市" readonly />
<input class="form-input region-input" v-model="newAddress.district"
placeholder="区/县" readonly />
</view>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea" v-model="newAddress.detail"
placeholder="街道、小区、楼栋、门牌号等"
maxlength="100" />
</view>
<view class="form-item checkbox-item">
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
<view :class="['checkbox', { checked: newAddress.is_default }]">
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
</view>
<text class="checkbox-label">设为默认地址</text>
</view>
</view>
</scroll-view>
<view class="form-buttons">
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts'
type AddressType = {
id: string
user_id: string
recipient_name: string
phone: string
province: string
city: string
district: string
detail: string
postal_code: string | null
is_default: boolean
created_at: string
}
const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false)
const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<any>({
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
})
const smartAddressInput = ref<string>('')
onLoad((options: any) => {
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const flag = optObj.getString('fromSelect') ?? ''
fromSelect.value = (flag == '1' || flag == 'true')
})
// 生命周期
onMounted(() => {
loadAddresses()
// 监听地址更新事件从checkout页面或其他页面
uni.$on('addressUpdated', (updatedAddressList: any) => {
addressList.value = updatedAddressList
})
})
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
})
// 加载地址列表
const loadAddresses = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
}
try {
const { data, error } = await supa
.from('user_addresses')
.select('*')
.eq('user_id', userId)
.order('is_default', { ascending: false })
.order('created_at', { ascending: false })
if (error !== null) {
console.error('加载地址失败:', error)
return
}
addressList.value = data ?? []
} catch (err) {
console.error('加载地址异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
const userStore = uni.getStorageSync('userInfo')
if (userStore instanceof UTSJSONObject) {
return userStore.getString('id')
}
const obj = JSON.parse(JSON.stringify(userStore ?? {})) as UTSJSONObject
return obj.getString('id')
}
// 获取完整地址
const getFullAddress = (address: AddressType): string => {
return `${address.province}${address.city}${address.district}${address.detail}`
}
// 选择地址
const selectAddress = (address: AddressType) => {
if (fromSelect.value) {
try {
uni.setStorageSync('selectedAddress', JSON.stringify(address))
} catch (e) {}
uni.$emit('addressSelected', address)
uni.navigateBack()
}
}
// 编辑地址
const editAddress = (address: AddressType) => {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${address.id}`
})
}
// 删除地址
const deleteAddress = (address: AddressType) => {
uni.showModal({
title: '删除地址',
content: '确定要删除这个收货地址吗?',
success: async (res) => {
if (res.confirm) {
try {
// 如果是默认地址,删除前检查是否还有其他地址
if (address.is_default && addressList.value.length > 1) {
uni.showModal({
title: '提示',
content: '删除默认地址后,系统会自动设置第一个地址为默认地址',
success: async (confirmRes) => {
if (confirmRes.confirm) {
await performDelete(address)
}
}
})
} else {
await performDelete(address)
}
} catch (err) {
console.error('删除地址异常:', err)
}
}
}
})
}
// 执行删除
const performDelete = async (address: AddressType) => {
try {
const { error } = await supa
.from('user_addresses')
.delete()
.eq('id', address.id)
if (error !== null) {
console.error('删除地址失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
// 从列表中移除
const index = addressList.value.findIndex(item => item.id === address.id)
if (index !== -1) {
addressList.value.splice(index, 1)
}
// 如果是默认地址被删除,设置第一个地址为默认
if (address.is_default && addressList.value.length > 0) {
const newDefault = addressList.value[0]
await setAsDefault(newDefault)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('执行删除异常:', err)
}
}
// 设为默认地址
const setDefaultAddress = async (address: AddressType) => {
try {
const userId = getCurrentUserId()
if (!userId) return
// 1. 取消当前所有默认地址
const { error: updateError } = await supa
.from('user_addresses')
.update({ is_default: false })
.eq('user_id', userId)
.eq('is_default', true)
if (updateError !== null) {
console.error('取消默认地址失败:', updateError)
return
}
// 2. 设置新的默认地址
const { error: setError } = await supa
.from('user_addresses')
.update({ is_default: true })
.eq('id', address.id)
if (setError !== null) {
console.error('设置默认地址失败:', setError)
return
}
// 更新本地数据
addressList.value.forEach(item => {
item.is_default = item.id === address.id
})
uni.showToast({
title: '已设为默认地址',
icon: 'success'
})
} catch (err) {
console.error('设置默认地址异常:', err)
}
}
// 设置地址为默认(内部方法)
const setAsDefault = async (address: AddressType) => {
try {
const { error } = await supa
.from('user_addresses')
.update({ is_default: true })
.eq('id', address.id)
if (error !== null) {
console.error('设置默认地址失败:', error)
return
}
address.is_default = true
} catch (err) {
console.error('设置默认地址异常:', err)
}
}
// 解析智能地址
const parseSmartAddress = () => {
const input = smartAddressInput.value.trim()
if (!input) return
// 重置表单
newAddress.value.recipient_name = ''
newAddress.value.phone = ''
newAddress.value.province = ''
newAddress.value.city = ''
newAddress.value.district = ''
newAddress.value.detail = ''
// 尝试匹配手机号码11位数字
const phoneRegex = /(1[3-9]\d{9})/g
const phoneMatches = input.match(phoneRegex)
if (phoneMatches && phoneMatches.length > 0) {
newAddress.value.phone = phoneMatches[0]
}
// 尝试匹配收件人姓名中文姓名2-4个汉字
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
const nameMatches = input.match(nameRegex)
if (nameMatches && nameMatches.length > 0) {
// 取第一个匹配的中文姓名作为收件人
newAddress.value.recipient_name = nameMatches[0]
}
// 提取地址部分(移除姓名和手机号)
let addressText = input
if (newAddress.value.recipient_name) {
addressText = addressText.replace(newAddress.value.recipient_name, '')
}
if (newAddress.value.phone) {
addressText = addressText.replace(newAddress.value.phone, '')
}
// 清理地址文本(移除多余的空格和标点)
addressText = addressText.replace(/[,;\s]+/g, ' ').trim()
// 地址解析逻辑
const patterns = [
// 匹配格式:省市区详细地址
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
// 匹配格式:省市详细地址
/^(.*?省)?(.*?市)?(.*)$/
]
for (const pattern of patterns) {
const match = addressText.match(pattern)
if (match) {
const [, province, city, district, detail] = match
if (province) newAddress.value.province = province.replace('省', '').trim()
if (city) newAddress.value.city = city.replace('市', '').trim()
if (district) newAddress.value.district = district.trim()
if (detail) newAddress.value.detail = detail.trim()
// 如果详细地址为空,但还有剩余内容,则作为详细地址
if (!newAddress.value.detail && district && detail) {
newAddress.value.detail = detail.trim()
}
break
}
}
// 如果没有匹配到模式,尝试简单分割
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
// 尝试按常见分隔符分割
const parts = addressText.split(/[省市县区]/)
if (parts.length >= 2) {
newAddress.value.province = parts[0] || ''
newAddress.value.city = parts[1] || ''
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
} else {
newAddress.value.detail = addressText
}
}
// 如果地址部分为空,但还有剩余文本,则作为详细地址
if (!newAddress.value.detail && addressText.trim()) {
newAddress.value.detail = addressText.trim()
}
}
// 保存新地址
const saveNewAddress = async () => {
// 验证表单
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
try {
const { data, error } = await supa
.from('user_addresses')
.insert({
user_id: userId,
recipient_name: newAddress.value.recipient_name,
phone: newAddress.value.phone,
province: newAddress.value.province,
city: newAddress.value.city,
district: newAddress.value.district,
detail: newAddress.value.detail,
is_default: newAddress.value.is_default,
created_at: new Date().toISOString()
})
.select()
.single()
if (error !== null) {
console.error('保存地址失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
return
}
// 如果是默认地址,取消其他默认地址
if (newAddress.value.is_default) {
addressList.value.forEach(addr => {
addr.is_default = false
})
// 更新数据库中的其他地址
await supa
.from('user_addresses')
.update({ is_default: false })
.eq('user_id', userId)
.neq('id', data.id)
}
// 添加到列表
addressList.value.unshift(data)
// 发布地址更新事件让checkout页面也能获取到
uni.$emit('addressUpdated', addressList.value)
// 重置表单
resetNewAddressForm()
uni.showToast({
title: '地址保存成功',
icon: 'success'
})
} catch (err) {
console.error('保存地址异常:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 重置新建地址表单
const resetNewAddressForm = () => {
showNewAddressForm.value = false
newAddress.value = {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
}
smartAddressInput.value = ''
}
// 取消新建地址
const cancelNewAddress = () => {
resetNewAddressForm()
}
</script>
<style scoped>
.address-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.address-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
}
.header-title {
text-align: center;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.address-list {
flex: 1;
padding: 10px;
}
.empty-address {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
border-radius: 8px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.address-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.address-info {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #f5f5f5;
}
.address-header-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.address-name {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-right: 15px;
}
.address-phone {
font-size: 14px;
color: #666666;
margin-right: 10px;
}
.default-tag {
background-color: #ff4757;
padding: 2px 8px;
border-radius: 10px;
}
.tag-text {
color: #ffffff;
font-size: 12px;
}
.address-detail {
font-size: 14px;
color: #333333;
line-height: 1.5;
}
.detail-text {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.address-actions {
display: flex;
justify-content: flex-end;
gap: 20px;
}
.action-item {
display: flex;
align-items: center;
}
.action-icon {
font-size: 16px;
margin-right: 5px;
}
.action-text {
font-size: 14px;
color: #666666;
}
.select-tip {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
margin-bottom: 10px;
text-align: center;
}
.tip-text {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 5px;
display: block;
}
.tip-subtext {
font-size: 14px;
color: #999999;
display: block;
}
.add-address-btn {
background-color: #007aff;
margin: 10px;
padding: 15px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
color: #ffffff;
font-size: 24px;
margin-right: 10px;
}
.btn-text {
color: #ffffff;
font-size: 16px;
font-weight: bold;
}
/* 新建地址表单弹窗样式 */
.address-form-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.address-form-popup {
background-color: #ffffff;
width: 90%;
max-width: 500px;
max-height: 80vh;
border-radius: 12px;
display: flex;
flex-direction: column;
}
.form-header {
padding: 20px 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.form-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.form-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.form-content {
flex: 1;
padding: 15px;
max-height: 50vh;
}
.form-item {
margin-bottom: 20px;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 14px;
color: #333333;
margin-bottom: 8px;
display: block;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
box-sizing: border-box;
}
.form-input[readonly] {
background-color: #f9f9f9;
color: #666666;
}
.region-inputs {
display: flex;
gap: 10px;
}
.region-input {
flex: 1;
}
.form-textarea {
width: 100%;
min-height: 80px;
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
box-sizing: border-box;
}
.smart-address-input {
min-height: 60px;
}
.smart-tip {
font-size: 12px;
color: #999999;
margin-top: 5px;
display: block;
}
.checkbox-item {
margin-top: 20px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
}
.checkbox {
width: 20px;
height: 20px;
border: 1px solid #e5e5e5;
border-radius: 4px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox.checked {
background-color: #007aff;
border-color: #007aff;
}
.checkbox-check {
color: #ffffff;
font-size: 14px;
}
.checkbox-label {
font-size: 14px;
color: #333333;
}
.form-buttons {
display: flex;
padding: 15px;
border-top: 1px solid #e5e5e5;
gap: 10px;
}
.form-cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #333333;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
.form-submit-btn {
flex: 1;
background-color: #007aff;
color: #ffffff;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
</style>