Files
medical-mall/pages/mall/consumer/address.uvue
2026-05-14 15:28:09 +08:00

829 lines
18 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" direction="vertical">
<!-- 地址为空 -->
<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" direction="vertical">
<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"></textarea>
<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"></textarea>
</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 { supabaseService, type UserAddress, type AddAddressParams } from '@/utils/supabaseService.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
}
type NewAddressForm = {
recipient_name: string
phone: string
province: string
city: string
district: string
detail: string
is_default: boolean
}
const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false)
const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<NewAddressForm>({
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
})
const smartAddressInput = ref<string>('')
function mapAddress(item: UserAddress): AddressType {
return {
id: item.id,
user_id: item.user_id,
recipient_name: item.recipient_name ?? '',
phone: item.phone ?? '',
province: item.province ?? '',
city: item.city ?? '',
district: item.district ?? '',
detail: item.detail_address ?? '',
postal_code: item.postal_code ?? null,
is_default: item.is_default,
created_at: item.created_at ?? ''
}
}
function getCurrentUserId(): string {
return supabaseService.getCurrentUserId() ?? ''
}
function getFullAddress(address: AddressType): string {
return `${address.province}${address.city}${address.district}${address.detail}`
}
function resetNewAddressForm(): void {
showNewAddressForm.value = false
newAddress.value = {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
}
smartAddressInput.value = ''
}
function cancelNewAddress(): void {
resetNewAddressForm()
}
function selectAddress(address: AddressType): void {
if (fromSelect.value) {
try {
uni.setStorageSync('selectedAddress', JSON.stringify(address))
} catch (e) {}
uni.$emit('addressSelected', address)
uni.navigateBack()
}
}
function editAddress(address: AddressType): void {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${address.id}`
})
}
async function loadAddresses(): Promise<void> {
const userId = getCurrentUserId()
if (userId === '') {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
}
try {
const items = await supabaseService.getAddresses()
const nextList: AddressType[] = []
for (let i = 0; i < items.length; i++) {
nextList.push(mapAddress(items[i]))
}
addressList.value = nextList
} catch (err) {
console.error('加载地址异常:', err)
addressList.value = []
}
}
async function doDeleteAddress(address: AddressType): Promise<void> {
const success = await supabaseService.deleteAddress(address.id)
if (!success) {
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
await loadAddresses()
if (address.is_default && addressList.value.length > 0) {
await supabaseService.setDefaultAddress(addressList.value[0].id)
await loadAddresses()
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
function confirmDeleteAddress(address: AddressType): void {
doDeleteAddress(address)
}
function deleteAddress(address: AddressType): void {
uni.showModal({
title: '删除地址',
content: '确定要删除这个收货地址吗?',
success: (res) => {
if (!res.confirm) return
if (address.is_default && addressList.value.length > 1) {
uni.showModal({
title: '提示',
content: '删除默认地址后,系统会自动设置第一个地址为默认地址',
success: (confirmRes) => {
if (confirmRes.confirm) {
confirmDeleteAddress(address)
}
}
})
return
}
confirmDeleteAddress(address)
}
})
}
async function setDefaultAddress(address: AddressType): Promise<void> {
const success = await supabaseService.setDefaultAddress(address.id)
if (!success) {
uni.showToast({
title: '设置失败',
icon: 'none'
})
return
}
await loadAddresses()
uni.showToast({
title: '已设为默认地址',
icon: 'success'
})
}
function parseSmartAddress(): void {
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 = ''
const phoneRegex = /(1[3-9]\d{9})/g
const phoneMatches = input.match(phoneRegex)
if (phoneMatches != null && phoneMatches.length > 0) {
newAddress.value.phone = phoneMatches[0] ?? ''
}
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
const nameMatches = input.match(nameRegex)
if (nameMatches != null && 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 (let i = 0; i < patterns.length; i++) {
const match = addressText.match(patterns[i])
if (match != null) {
const province = match.length > 1 ? (match[1] ?? '') : ''
const city = match.length > 2 ? (match[2] ?? '') : ''
const district = match.length > 3 ? (match[3] ?? '') : ''
const detail = match.length > 4 ? (match[4] ?? '') : ''
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] ?? ''
const detailCandidate = parts.slice(2).join('').trim()
newAddress.value.detail = detailCandidate !== '' ? detailCandidate : addressText
} else {
newAddress.value.detail = addressText
}
}
if (newAddress.value.detail === '' && addressText.trim() !== '') {
newAddress.value.detail = addressText.trim()
}
}
async function saveNewAddress(): Promise<void> {
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
}
const payload: AddAddressParams = {
recipient_name: newAddress.value.recipient_name,
phone: newAddress.value.phone,
province: newAddress.value.province,
city: newAddress.value.city,
district: newAddress.value.district,
detail_address: newAddress.value.detail,
is_default: newAddress.value.is_default
}
try {
const success = await supabaseService.addAddress(payload)
if (!success) {
uni.showToast({
title: '保存失败',
icon: 'none'
})
return
}
await loadAddresses()
uni.$emit('addressUpdated', addressList.value)
resetNewAddressForm()
uni.showToast({
title: '地址保存成功',
icon: 'success'
})
} catch (err) {
console.error('保存地址异常:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
onLoad((options) => {
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')
})
// 生命周期
function handleAddressUpdated(): void {
loadAddresses()
}
onMounted(() => {
loadAddresses()
// 监听地址更新事件从checkout页面或其他页面
uni.$on('addressUpdated', handleAddressUpdated)
})
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
})
</script>
<style scoped>
.address-page {
display: flex;
flex-direction: column;
height: 100%;
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: 80%;
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: 50%;
}
.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>