471 lines
10 KiB
Vue
471 lines
10 KiB
Vue
<template>
|
|
<view class="page-container">
|
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
|
<view class="bg-decoration"></view>
|
|
|
|
<view class="header-area">
|
|
<view class="page-title">地址管理</view>
|
|
<view class="page-subtitle">Address Management</view>
|
|
</view>
|
|
|
|
<view class="action-bar">
|
|
<button class="add-btn" @click="toAdd">
|
|
<text class="plus-icon">+</text>
|
|
<text>新增收货地址</text>
|
|
</button>
|
|
</view>
|
|
|
|
<scroll-view scroll-y class="content-scroll">
|
|
<view v-if="error" class="error-tip">{{ error }}</view>
|
|
|
|
<!-- 空状态 -->
|
|
<view v-if="list.length === 0 && !loading" class="empty-state">
|
|
<text class="empty-icon">📍</text>
|
|
<text class="empty-text">暂无收货地址</text>
|
|
</view>
|
|
|
|
<!-- 地址列表 -->
|
|
<view class="addr-list">
|
|
<view
|
|
v-for="(item, index) in list"
|
|
:key="item.id"
|
|
class="addr-card"
|
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
|
>
|
|
<view class="addr-content" @click="toEdit(item)">
|
|
<view class="addr-header">
|
|
<view class="user-info">
|
|
<text class="name">{{ item.name || item.realname }}</text>
|
|
<text class="phone">{{ item.phone || item.mobile }}</text>
|
|
</view>
|
|
<view v-if="item.is_default" class="default-tag">默认</view>
|
|
</view>
|
|
|
|
<view class="addr-detail">
|
|
<text class="region">{{ item.province }} {{ item.city }} {{ item.district }}</text>
|
|
<text class="detail-text">{{ item.address || item.detail }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 分割线 -->
|
|
<view class="card-divider"></view>
|
|
|
|
<!-- 操作栏 -->
|
|
<view class="addr-actions">
|
|
<view class="action-left" @tap.stop="onSetDefault(item)">
|
|
<view class="radio-circle" :class="{ checked: item.is_default }"></view>
|
|
<text class="action-text">{{ item.is_default ? '默认地址' : '设为默认' }}</text>
|
|
</view>
|
|
|
|
<view class="action-right">
|
|
<view class="action-btn" @tap.stop="toEdit(item)">
|
|
<text class="btn-icon">✎</text>
|
|
<text>编辑</text>
|
|
</view>
|
|
<view class="action-btn delete" @tap.stop="onDelete(item)">
|
|
<text class="btn-icon">🗑</text>
|
|
<text>删除</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="bottom-spacer"></view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
|
import { listAddresses, deleteAddress, setDefaultAddress } from '../../api/appUser'
|
|
|
|
const list = ref([])
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
|
|
async function fetchList() {
|
|
const user_id = uni.getStorageSync('user_id')
|
|
const token = uni.getStorageSync('token')
|
|
const phoneBound = !!uni.getStorageSync('phone_bound')
|
|
|
|
// 简单的登录检查,实际逻辑可能需要更严谨
|
|
if (!user_id || !token) {
|
|
// 这里不再强制弹窗,由页面逻辑决定是否跳转
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const data = await listAddresses(user_id)
|
|
list.value = Array.isArray(data) ? data : (data && (data.list || data.items)) || []
|
|
} catch (e) {
|
|
// 静默失败或显示轻提示
|
|
console.error(e)
|
|
// error.value = '获取地址列表失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function toAdd() {
|
|
uni.removeStorageSync('edit_address')
|
|
uni.navigateTo({ url: '/pages-user/address/edit' })
|
|
}
|
|
|
|
function toEdit(item) {
|
|
uni.setStorageSync('edit_address', item)
|
|
uni.navigateTo({ url: `/pages-user/address/edit?id=${item.id}` })
|
|
}
|
|
|
|
function onDelete(item) {
|
|
const user_id = uni.getStorageSync('user_id')
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: '确定删除该地址吗?',
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
try {
|
|
await deleteAddress(user_id, item.id)
|
|
uni.showToast({ title: '已删除', icon: 'none' })
|
|
fetchList()
|
|
} catch (e) {
|
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async function onSetDefault(item) {
|
|
console.log('onSetDefault called', item.id, 'is_default:', item.is_default)
|
|
if (item.is_default) {
|
|
console.log('Already default, skipping')
|
|
return
|
|
}
|
|
try {
|
|
const user_id = uni.getStorageSync('user_id')
|
|
console.log('Calling setDefaultAddress API', user_id, item.id)
|
|
await setDefaultAddress(user_id, item.id)
|
|
uni.showToast({ title: '设置成功', icon: 'none' })
|
|
fetchList()
|
|
} catch (e) {
|
|
console.error('setDefaultAddress error', e)
|
|
uni.showToast({ title: '设置失败', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
onLoad(() => {
|
|
fetchList()
|
|
})
|
|
|
|
onShow(() => {
|
|
fetchList()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.page-container {
|
|
min-height: 100vh;
|
|
background: $bg-page;
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* 背景装饰 - 与优惠券页面统一 */
|
|
.bg-decoration {
|
|
position: absolute;
|
|
top: 0; left: 0; width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -100rpx; right: -100rpx;
|
|
width: 600rpx; height: 600rpx;
|
|
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
|
filter: blur(60rpx);
|
|
border-radius: 50%;
|
|
opacity: 0.8;
|
|
animation: float 10s ease-in-out infinite;
|
|
}
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 200rpx; left: -200rpx;
|
|
width: 500rpx; height: 500rpx;
|
|
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
|
filter: blur(50rpx);
|
|
border-radius: 50%;
|
|
opacity: 0.6;
|
|
animation: float 15s ease-in-out infinite reverse;
|
|
}
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translate(0, 0); }
|
|
50% { transform: translate(30rpx, 50rpx); }
|
|
}
|
|
|
|
.header-area {
|
|
padding: $spacing-xl $spacing-lg;
|
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 48rpx;
|
|
font-weight: 900;
|
|
color: $text-main;
|
|
margin-bottom: 8rpx;
|
|
letter-spacing: 1rpx;
|
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.page-subtitle {
|
|
font-size: 24rpx;
|
|
color: $text-tertiary;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.action-bar {
|
|
@extend .glass-card;
|
|
margin: 0 $spacing-lg $spacing-md;
|
|
padding: 20rpx;
|
|
z-index: 10;
|
|
}
|
|
|
|
.add-btn {
|
|
background: $gradient-brand;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 100rpx;
|
|
height: 88rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
box-shadow: $shadow-warm;
|
|
transition: all 0.3s;
|
|
|
|
&:active {
|
|
transform: scale(0.98);
|
|
opacity: 0.9;
|
|
}
|
|
}
|
|
|
|
.plus-icon {
|
|
font-size: 40rpx;
|
|
margin-right: 12rpx;
|
|
margin-top: -4rpx;
|
|
font-weight: 300;
|
|
}
|
|
|
|
.content-scroll {
|
|
flex: 1;
|
|
height: 0;
|
|
padding: 0 $spacing-lg;
|
|
box-sizing: border-box;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 空状态 */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 100rpx 0;
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 20rpx;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.empty-text {
|
|
color: $text-tertiary;
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
/* 地址列表 */
|
|
.addr-list {
|
|
padding-bottom: 40rpx;
|
|
}
|
|
|
|
.addr-card {
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
margin-bottom: 24rpx;
|
|
box-shadow: $shadow-sm;
|
|
overflow: hidden;
|
|
animation: fadeInUp 0.5s ease-out backwards;
|
|
/* Removed border from glass style */
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(20rpx); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.addr-content {
|
|
padding: 30rpx;
|
|
}
|
|
|
|
.addr-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.name {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: $text-main;
|
|
}
|
|
|
|
.phone {
|
|
font-size: 28rpx;
|
|
color: $text-sub;
|
|
font-family: monospace; /* 数字等宽 */
|
|
}
|
|
|
|
.default-tag {
|
|
font-size: 20rpx;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 8rpx 0 8rpx 0;
|
|
font-weight: 600;
|
|
box-shadow: 0 2rpx 6rpx rgba($brand-primary, 0.2);
|
|
}
|
|
|
|
.addr-detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.region {
|
|
font-size: 26rpx;
|
|
color: $text-sub;
|
|
}
|
|
|
|
.detail-text {
|
|
font-size: 28rpx;
|
|
color: $text-main;
|
|
line-height: 1.5;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* 分割线 */
|
|
.card-divider {
|
|
height: 1rpx;
|
|
background: #f0f0f0;
|
|
margin: 0 30rpx;
|
|
}
|
|
|
|
/* 操作栏 */
|
|
.addr-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
background: rgba(249, 249, 249, 0.5);
|
|
}
|
|
|
|
.action-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.radio-circle {
|
|
width: 32rpx;
|
|
height: 32rpx;
|
|
border-radius: 50%;
|
|
border: 2rpx solid #ddd;
|
|
position: relative;
|
|
transition: all 0.2s;
|
|
|
|
&.checked {
|
|
border-color: $brand-primary;
|
|
background: $brand-primary;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 12rpx; height: 6rpx;
|
|
border-left: 3rpx solid #fff;
|
|
border-bottom: 3rpx solid #fff;
|
|
transform: translate(-50%, -60%) rotate(-45deg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.action-text {
|
|
font-size: 24rpx;
|
|
color: $text-sub;
|
|
}
|
|
|
|
.action-right {
|
|
display: flex;
|
|
gap: 30rpx;
|
|
}
|
|
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6rpx;
|
|
font-size: 26rpx;
|
|
color: $text-sub;
|
|
padding: 10rpx;
|
|
|
|
.btn-icon {
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
&:active {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
&.delete {
|
|
color: $color-error; // 使用全局变量或具体色值
|
|
}
|
|
}
|
|
|
|
.error-tip {
|
|
color: #ff4d4f;
|
|
background: rgba(255, 77, 79, 0.1);
|
|
padding: 20rpx;
|
|
border-radius: 12rpx;
|
|
text-align: center;
|
|
font-size: 26rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.bottom-spacer {
|
|
height: 40rpx;
|
|
}
|
|
</style> |