728 lines
18 KiB
Vue
728 lines
18 KiB
Vue
<template>
|
||
<div class="address-page">
|
||
<!-- 导航栏 -->
|
||
<nav class="navbar">
|
||
<div class="nav-left">
|
||
<el-button
|
||
type="text"
|
||
@click="$router.go(-1)"
|
||
class="back-btn"
|
||
>
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
返回
|
||
</el-button>
|
||
</div>
|
||
<div class="nav-center">
|
||
<h1 class="nav-title">地址</h1>
|
||
</div>
|
||
<div class="nav-right">
|
||
<el-button
|
||
type="primary"
|
||
@click="showAddDialog = true"
|
||
class="add-btn"
|
||
>
|
||
+新增地址
|
||
</el-button>
|
||
</div>
|
||
</nav>
|
||
|
||
<div v-loading="loading" class="address-content">
|
||
<!-- 地址列表为空 -->
|
||
<div v-if="addresses.length === 0 && !loading" class="empty-address">
|
||
<el-icon class="empty-icon"><Location /></el-icon>
|
||
<p>暂无收货地址</p>
|
||
<p class="empty-tip">点击右上角添加收货地址</p>
|
||
</div>
|
||
|
||
<!-- 地址列表 -->
|
||
<div v-else class="address-list">
|
||
<div
|
||
v-for="(address,index) in addresses"
|
||
:key="address.id"
|
||
class="address-item"
|
||
:class="{ 'default-address': address.isDefault }"
|
||
>
|
||
<div class="address-info">
|
||
<div class="address-header">
|
||
<el-icon><Location /></el-icon>
|
||
<div class="address-location">
|
||
<div class="region-info">{{ address.province_name }} {{ address.city_name }} {{ address.district_name }}</div>
|
||
<div class="detail-info">{{ address.detailAddress }}</div>
|
||
</div>
|
||
<el-tag v-if="address.isDefault" type="warning" size="small" class="default-tag">
|
||
默认
|
||
</el-tag>
|
||
</div>
|
||
<div class="address-detail">
|
||
<span class="recipient-name">{{ address.recipientName }}</span>
|
||
<span class="recipient-phone">{{ address.recipientPhone }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="address-actions">
|
||
<el-button
|
||
type="text"
|
||
@click="editAddress(address)"
|
||
class="edit-btn"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
type="text"
|
||
@click="deleteAddress(address.id)"
|
||
class="delete-btn"
|
||
>
|
||
删除
|
||
</el-button>
|
||
<el-button
|
||
v-if="!address.isDefault"
|
||
type="text"
|
||
@click="setDefaultAddress(index)"
|
||
class="default-btn"
|
||
>
|
||
设为默认
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑地址对话框 -->
|
||
<el-dialog
|
||
v-model="showAddDialog"
|
||
:title="editingAddress ? '编辑地址' : '新增地址'"
|
||
width="90%"
|
||
class="address-dialog"
|
||
>
|
||
<el-form
|
||
ref="addressFormRef"
|
||
:model="addressForm"
|
||
:rules="addressRules"
|
||
label-width="80px"
|
||
class="address-form"
|
||
>
|
||
<el-form-item label="收货人" prop="recipientName">
|
||
<el-input
|
||
v-model="addressForm.recipientName"
|
||
placeholder="请输入收货人姓名"
|
||
maxlength="20"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="手机号" prop="recipientPhone">
|
||
<el-input
|
||
v-model="addressForm.recipientPhone"
|
||
placeholder="请输入手机号"
|
||
maxlength="11"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="省市区" prop="region">
|
||
<el-cascader
|
||
v-model="addressForm.region"
|
||
:options="regionOptions"
|
||
placeholder="请选择省市区"
|
||
style="width: 100%"
|
||
:props="{ expandTrigger: 'hover', value:'code' }"
|
||
:show-all-levels="true"
|
||
:collapse-tags="true"
|
||
:max-collapse-tags="1"
|
||
:teleported="false"
|
||
popper-class="mobile-cascader-popper"
|
||
@change="handleRegionChange"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="详细地址" prop="detailAddress">
|
||
<el-input
|
||
v-model="addressForm.detailAddress"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入详细地址"
|
||
maxlength="100"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-checkbox v-model="addressForm.isDefault">
|
||
设为默认地址
|
||
</el-checkbox>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="cancelEdit">取消</el-button>
|
||
<el-button type="primary" @click="saveAddress" :loading="saving">
|
||
{{ editingAddress ? '保存' : '添加' }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
ArrowLeft,
|
||
Location
|
||
} from '@element-plus/icons-vue'
|
||
import api from '@/utils/api'
|
||
|
||
const router = useRouter()
|
||
|
||
// 响应式数据
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const addresses = ref([])
|
||
const showAddDialog = ref(false)
|
||
const editingAddress = ref(null)
|
||
const addressFormRef = ref(null)
|
||
const addressList = ref([])
|
||
|
||
// 地址表单数据
|
||
const addressForm = reactive({
|
||
recipientName: '',
|
||
recipientPhone: '',
|
||
region: [],
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detailAddress: '',
|
||
isDefault: false
|
||
})
|
||
|
||
// 省市区选项数据(从API获取)
|
||
const regionOptions = ref([])
|
||
|
||
// 表单验证规则
|
||
const addressRules = {
|
||
recipientName: [
|
||
{ required: true, message: '请输入收货人姓名', trigger: 'blur' },
|
||
{ min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
|
||
],
|
||
recipientPhone: [
|
||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||
],
|
||
region: [
|
||
{ required: true, message: '请选择省市区', trigger: 'change' }
|
||
],
|
||
detailAddress: [
|
||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||
{ min: 5, max: 100, message: '详细地址长度在 5 到 100 个字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// 方法
|
||
const loadRegionOptions = async () => {
|
||
try {
|
||
// 获取所有省份
|
||
const provincesResponse = await api.get('/regions/provinces')
|
||
console.log('获取省份数据:', provincesResponse)
|
||
if (!provincesResponse.data.success) {
|
||
throw new Error(provincesResponse.data.message || '获取省份数据失败')
|
||
}
|
||
|
||
regionOptions.value= provincesResponse.data.data || []
|
||
console.log('获取省份数据:', regionOptions.value);
|
||
|
||
} catch (error) {
|
||
console.error('获取省市区数据失败:', error)
|
||
ElMessage.error(error.message || '获取省市区数据失败')
|
||
|
||
}
|
||
}
|
||
|
||
const loadAddresses = async () => {
|
||
try {
|
||
loading.value = true
|
||
const response = await api.get('/addresses')
|
||
console.log('获取地址列表响应:', response)
|
||
if (response.data.success) {
|
||
// 根据接口文档转换数据格式
|
||
addressList.value = response.data.data || []
|
||
addresses.value = addressList.value.map(addr => ({
|
||
id: addr.id,
|
||
recipientName: addr.receiver_name,
|
||
recipientPhone: addr.receiver_phone,
|
||
province: addr.province_name,
|
||
city: addr.city_name,
|
||
district: addr.district_name,
|
||
detailAddress: addr.detailed_address,
|
||
isDefault: addr.is_default,
|
||
labelName: addr.label_name,
|
||
labelColor: addr.label_color,
|
||
city_code:addr.city_code,
|
||
province_code:addr.province_code,
|
||
district_code:addr.district_code,
|
||
...addr
|
||
}))
|
||
} else {
|
||
// 保留原有的测试数据作为回退
|
||
addresses.value = [{
|
||
id: 1,
|
||
recipientName: '张三',
|
||
recipientPhone: '11111111111',
|
||
province: '浙江省',
|
||
city: '宁波市',
|
||
district: '鄞州区',
|
||
detailAddress: '宁波外经合作大厦',
|
||
isDefault: true
|
||
},{
|
||
id: 2,
|
||
recipientName: '李四',
|
||
recipientPhone: '22222222222',
|
||
province: '浙江省',
|
||
city: '宁波市',
|
||
district: '鄞州区',
|
||
detailAddress: '四明东路',
|
||
isDefault: false
|
||
}]
|
||
throw new Error(response.data.message || '获取地址列表失败')
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(error.message || '获取地址列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleRegionChange = (value) => {
|
||
console.log(value);
|
||
|
||
if (value && value.length === 3) {
|
||
addressForm.province = value[0]
|
||
addressForm.city = value[1]
|
||
addressForm.district = value[2]
|
||
}
|
||
}
|
||
|
||
|
||
const editAddress = (address) => {
|
||
editingAddress.value = address
|
||
addressForm.recipientName = address.recipientName
|
||
addressForm.recipientPhone = address.recipientPhone
|
||
addressForm.region = [address.province, address.city, address.district]
|
||
addressForm.province = address.province
|
||
addressForm.city = address.city
|
||
addressForm.district = address.district
|
||
addressForm.detailAddress = address.detailAddress
|
||
addressForm.isDefault = address.isDefault
|
||
showAddDialog.value = true
|
||
}
|
||
|
||
const saveAddress = async () => {
|
||
if (!addressFormRef.value) return
|
||
|
||
try {
|
||
await addressFormRef.value.validate()
|
||
saving.value = true
|
||
|
||
// 根据接口文档构建请求数据
|
||
const addressData = {
|
||
recipient_name: addressForm.recipientName,
|
||
phone: addressForm.recipientPhone,
|
||
province_code: addressForm.province,
|
||
city_code: addressForm.city,
|
||
district_code: addressForm.district,
|
||
detailed_address: addressForm.detailAddress,
|
||
is_default: addressForm.isDefault
|
||
}
|
||
|
||
let response
|
||
if (editingAddress.value) {
|
||
// 编辑地址
|
||
response = await api.put(`/addresses/${editingAddress.value.id}`, addressData)
|
||
} else {
|
||
// 新增地址
|
||
console.log('表单原始数据:', addressForm)
|
||
response = await api.post('/addresses', addressData)
|
||
}
|
||
|
||
if (response.data.success) {
|
||
ElMessage.success(editingAddress.value ? '地址更新成功' : '地址添加成功')
|
||
showAddDialog.value = false
|
||
resetForm()
|
||
await loadAddresses()
|
||
} else {
|
||
throw new Error(response.data.message || '保存地址失败')
|
||
}
|
||
} catch (error) {
|
||
if (error.message) {
|
||
ElMessage.error(error.message)
|
||
}
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const deleteAddress = async (addressId) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'确定要删除这个地址吗?',
|
||
'确认删除',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
|
||
const response = await api.delete(`/addresses/${addressId}`)
|
||
console.log("删除id",addressId)
|
||
if (response.data.success) {
|
||
ElMessage.success('地址删除成功')
|
||
await loadAddresses()
|
||
} else {
|
||
throw new Error(response.data.message || '删除地址失败')
|
||
}
|
||
} catch (error) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error(error.message || '删除地址失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
const setDefaultAddress = async (addressId) => {
|
||
console.log(addressId,addresses.value[addressId]);
|
||
let addressDataTest = addresses.value[addressId]
|
||
addressDataTest.isDefault = true
|
||
const addressData = {
|
||
recipient_name: addressDataTest.receiver_name,
|
||
phone: addressDataTest.receiver_phone,
|
||
province_code: addressDataTest.province,
|
||
city_code: addressDataTest.city,
|
||
district_code: addressDataTest.district,
|
||
detailed_address: addressDataTest.detailAddress,
|
||
is_default: addressDataTest.isDefault
|
||
}
|
||
console.log('addressDataTest',addressDataTest);
|
||
console.log('addressData',addressData);
|
||
try {
|
||
const response = await api.put(`/addresses/${addressDataTest.id}`, addressData)
|
||
if (response.data.success) {
|
||
ElMessage.success('默认地址设置成功')
|
||
await loadAddresses()
|
||
} else {
|
||
throw new Error(response.data.message || '设置默认地址失败')
|
||
}
|
||
} catch (error) {console.log(error)}
|
||
return
|
||
// 从addresses列表中找到对应的地址信息
|
||
const getDefaultAddress = () => {
|
||
for(add in addresses.value)
|
||
{
|
||
if(addresses.value[add].isDefault)
|
||
{
|
||
return addresses.value[add]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
|
||
|
||
console.log('getDefaultAddress', getDefaultAddress)
|
||
// const addressData = {
|
||
// recipient_name: addressList.value[0].receiver_name,
|
||
// phone: addressList.value[0].receiver_phone,
|
||
// province_code: addressList.value[0].province,
|
||
// city_code: addressList.value[0].city,
|
||
// district_code: addressList.value[0].district,
|
||
// detailed_address: targetAddress.detailAddress,
|
||
// is_default: true
|
||
// }
|
||
console.log('addresses', addresses.value)
|
||
console.log('targetaddress', targetAddress)
|
||
console.log('默认地址替换数据', addressData)
|
||
try {
|
||
const response = await api.put(`/addresses/${addressId}`, addressData)
|
||
if (response.data.success) {
|
||
ElMessage.success('默认地址设置成功')
|
||
await loadAddresses()
|
||
} else {
|
||
throw new Error(response.data.message || '设置默认地址失败')
|
||
}
|
||
} catch (error) {console.log(error)}
|
||
}
|
||
|
||
const cancelEdit = () => {
|
||
showAddDialog.value = false
|
||
resetForm()
|
||
}
|
||
|
||
const resetForm = () => {
|
||
editingAddress.value = null
|
||
addressForm.recipientName = ''
|
||
addressForm.recipientPhone = ''
|
||
addressForm.region = []
|
||
addressForm.province = ''
|
||
addressForm.city = ''
|
||
addressForm.district = ''
|
||
addressForm.detailAddress = ''
|
||
addressForm.isDefault = false
|
||
|
||
if (addressFormRef.value) {
|
||
addressFormRef.value.resetFields()
|
||
}
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
loadRegionOptions()
|
||
loadAddresses()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.address-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
|
||
}
|
||
|
||
.navbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 16px;
|
||
height: 60px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.nav-left, .nav-right {
|
||
flex: 1;
|
||
}
|
||
|
||
.nav-right {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.nav-center {
|
||
flex: 2;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: white;
|
||
}
|
||
|
||
.back-btn {
|
||
color: white;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.add-btn {
|
||
font-size: 14px;
|
||
background-color: none;
|
||
border-color: none;
|
||
}
|
||
|
||
.address-content {
|
||
padding: 16px;
|
||
}
|
||
|
||
.empty-address {
|
||
text-align: center;
|
||
padding: 80px 20px;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 64px;
|
||
color: #ddd;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-tip {
|
||
font-size: 14px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.address-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.address-item {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.address-item:hover {
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.default-address {
|
||
border: 2px solid #ffc640;
|
||
}
|
||
|
||
.address-info {
|
||
margin-bottom: 12px;
|
||
flex: 1;
|
||
}
|
||
|
||
.address-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.address-location {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
flex: 1;
|
||
}
|
||
|
||
.region-info {
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.detail-info {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.recipient-name {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.recipient-phone {
|
||
font-size: 16px;
|
||
color: #666;
|
||
}
|
||
|
||
.default-tag {
|
||
font-size: 12px;
|
||
margin-left: auto;
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.address-detail {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
color: #666;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
margin-left: 0;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.address-actions {
|
||
display: flex;
|
||
gap: 16px;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.edit-btn {
|
||
color: #409eff;
|
||
}
|
||
|
||
.delete-btn {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.default-btn {
|
||
color: #67c23a;
|
||
}
|
||
|
||
.address-dialog {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.address-form {
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.address-item {
|
||
padding: 12px;
|
||
}
|
||
|
||
.address-actions {
|
||
gap: 12px;
|
||
}
|
||
|
||
.address-dialog {
|
||
width: 95% !important;
|
||
}
|
||
}
|
||
|
||
/* 移动端级联选择器样式优化 */
|
||
:deep(.mobile-cascader-popper) {
|
||
max-width: calc(100vw - 32px) !important;
|
||
left: 16px !important;
|
||
right: 16px !important;
|
||
transform: none !important;
|
||
}
|
||
|
||
:deep(.mobile-cascader-popper .el-cascader-panel) {
|
||
max-width: 100% !important;
|
||
overflow-x: auto;
|
||
display: flex !important;
|
||
}
|
||
|
||
:deep(.mobile-cascader-popper .el-cascader-menu) {
|
||
min-width: 120px !important;
|
||
max-width: 150px !important;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
:deep(.mobile-cascader-popper) {
|
||
max-width: calc(100vw - 20px) !important;
|
||
left: 10px !important;
|
||
right: 10px !important;
|
||
}
|
||
|
||
:deep(.mobile-cascader-popper .el-cascader-menu) {
|
||
min-width: 100px !important;
|
||
max-width: 120px !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.mobile-cascader-popper .el-cascader-menu__item) {
|
||
padding: 8px 10px;
|
||
line-height: 1.2;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.address-form :deep(.el-cascader) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.address-form :deep(.el-cascader .el-input__inner) {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
</style> |