refactor: 重构路由配置,移除订单页面路由 style: 统一使用CSS变量替换硬编码颜色值 docs: 添加前端数据需求分析文档 fix: 修复登录页面记住我功能,更新用户信息页面链接 perf: 优化搜索组件动画效果和响应式设计 chore: 更新TypeScript类型定义,添加订单和地址相关类型
447 lines
14 KiB
Vue
447 lines
14 KiB
Vue
<!-- 更新地址弹窗 上下结构 上方有一个输入框可以识别地址更新到下方 下方有可以更新地址信息(省份、城市、区县、详细地址) -->
|
||
<!-- 父组件会传递一个地址对象 子组件需要根据这个地址对象更新弹窗的地址信息 -->
|
||
<template>
|
||
<a-modal :open="visible" title="更新地址" :footer="null" width="500px">
|
||
<div class="modal-content">
|
||
<!-- 地址识别输入框 -->
|
||
<div class="form-group">
|
||
<label for="address-input">地址识别</label>
|
||
<a-input v-model:value="addressInput" placeholder="请输入完整地址,系统会自动识别省份、城市、区县"
|
||
@change="handleAddressRecognition" />
|
||
<div v-if="recognitionError" class="error-message">{{ recognitionError }}</div>
|
||
</div>
|
||
|
||
<!-- 地址信息表单 -->
|
||
<div class="form-group">
|
||
<label for="province">省份 <span class="required">*</span></label>
|
||
<a-select v-model:value="form.province" :options="provinceOptions" placeholder="请选择省份"
|
||
@change="handleProvinceChange" />
|
||
<div v-if="errors.province" class="error-message">{{ errors.province }}</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="city">城市 <span class="required">*</span></label>
|
||
<a-select v-model:value="form.city" :options="cityOptions" placeholder="请选择城市"
|
||
@change="handleCityChange" />
|
||
<div v-if="errors.city" class="error-message">{{ errors.city }}</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="district">区县 <span class="required">*</span></label>
|
||
<a-select v-model:value="form.district" :options="districtOptions" placeholder="请选择区县" />
|
||
<div v-if="errors.district" class="error-message">{{ errors.district }}</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="detailAddress">详细地址 <span class="required">*</span></label>
|
||
<a-input v-model:value="form.detail" placeholder="请输入详细地址(街道、楼栋、门牌号等)" :rows="3" type="textarea" />
|
||
<div v-if="errors.detail" class="error-message">{{ errors.detail }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<a-button @click="handleCancel">取消</a-button>
|
||
<a-button type="primary" @click="handleOk">确认更新</a-button>
|
||
</div>
|
||
</a-modal>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import { ref, watch, computed } from 'vue'
|
||
import type { Address } from '@/Util/Type'
|
||
const props = defineProps({
|
||
// 设置弹窗显示状态控制
|
||
visible: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
// 传递的地址对象
|
||
address: {
|
||
type: Object as () => Address,
|
||
default: () => ({
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
isDefault: false
|
||
}) as Address,
|
||
}
|
||
})
|
||
// 定义事件
|
||
const emit = defineEmits(['update:visible', 'update:updateaddress'])
|
||
// 弹窗显示状态控制
|
||
const visible = ref(props.visible)
|
||
// 表单数据
|
||
const form = ref<Address>({
|
||
id:props.address.id ,
|
||
name: props.address.name,
|
||
phone: props.address.phone,
|
||
province: props.address.province,
|
||
city: props.address.city,
|
||
district: props.address.district,
|
||
detail: props.address.detail,
|
||
isDefault: props.address.isDefault
|
||
})
|
||
|
||
// 地址识别输入框
|
||
const addressInput = ref('')
|
||
const recognitionError = ref('')
|
||
|
||
// 表单验证错误
|
||
const errors = ref({
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
isDefault: ''
|
||
})
|
||
|
||
// 定义类型
|
||
interface Data {
|
||
[key: string]: { label: string; value: string }[] | undefined;
|
||
}
|
||
// 城市选项(根据省份动态生成)
|
||
const cityOptions = ref<any[]>([])
|
||
|
||
// 区县选项(根据城市动态生成)
|
||
const districtOptions = ref<any[]>([])
|
||
|
||
// 省份选项
|
||
const provinceOptions = ref([
|
||
{ label: '北京市', value: '北京市' },
|
||
{ label: '上海市', value: '上海市' },
|
||
{ label: '广东省', value: '广东省' },
|
||
{ label: '浙江省', value: '浙江省' },
|
||
{ label: '江苏省', value: '江苏省' },
|
||
{ label: '四川省', value: '四川省' },
|
||
{ label: '湖北省', value: '湖北省' },
|
||
{ label: '湖南省', value: '湖南省' },
|
||
{ label: '山东省', value: '山东省' },
|
||
{ label: '河南省', value: '河南省' }
|
||
])
|
||
|
||
// 城市数据
|
||
const cityData: Data = {
|
||
'北京市': [{ label: '北京市', value: '北京市' }],
|
||
'上海市': [{ label: '上海市', value: '上海市' }],
|
||
'广东省': [
|
||
{ label: '广州市', value: '广州市' },
|
||
{ label: '深圳市', value: '深圳市' },
|
||
{ label: '东莞市', value: '东莞市' },
|
||
{ label: '佛山市', value: '佛山市' }
|
||
],
|
||
'浙江省': [
|
||
{ label: '杭州市', value: '杭州市' },
|
||
{ label: '宁波市', value: '宁波市' },
|
||
{ label: '温州市', value: '温州市' },
|
||
{ label: '嘉兴市', value: '嘉兴市' }
|
||
],
|
||
'江苏省': [
|
||
{ label: '南京市', value: '南京市' },
|
||
{ label: '苏州市', value: '苏州市' },
|
||
{ label: '无锡市', value: '无锡市' },
|
||
{ label: '常州市', value: '常州市' }
|
||
]
|
||
}
|
||
|
||
// 区县数据
|
||
const districtData: Data = {
|
||
'北京市-北京市': [
|
||
{ label: '东城区', value: '东城区' },
|
||
{ label: '西城区', value: '西城区' },
|
||
{ label: '朝阳区', value: '朝阳区' },
|
||
{ label: '海淀区', value: '海淀区' },
|
||
{ label: '丰台区', value: '丰台区' },
|
||
{ label: '石景山区', value: '石景山区' },
|
||
{ label: '门头沟区', value: '门头沟区' },
|
||
{ label: '房山区', value: '房山区' },
|
||
{ label: '通州区', value: '通州区' },
|
||
{ label: '顺义区', value: '顺义区' }
|
||
],
|
||
'上海市-上海市': [
|
||
{ label: '黄浦区', value: '黄浦区' },
|
||
{ label: '徐汇区', value: '徐汇区' },
|
||
{ label: '长宁区', value: '长宁区' },
|
||
{ label: '静安区', value: '静安区' },
|
||
{ label: '普陀区', value: '普陀区' },
|
||
{ label: '虹口区', value: '虹口区' },
|
||
{ label: '杨浦区', value: '杨浦区' },
|
||
{ label: '浦东新区', value: '浦东新区' }
|
||
],
|
||
'广东省-广州市': [
|
||
{ label: '越秀区', value: '越秀区' },
|
||
{ label: '海珠区', value: '海珠区' },
|
||
{ label: '荔湾区', value: '荔湾区' },
|
||
{ label: '天河区', value: '天河区' },
|
||
{ label: '白云区', value: '白云区' },
|
||
{ label: '黄埔区', value: '黄埔区' },
|
||
{ label: '番禺区', value: '番禺区' },
|
||
{ label: '花都区', value: '花都区' }
|
||
],
|
||
'浙江省-杭州市': [
|
||
{ label: '上城区', value: '上城区' },
|
||
{ label: '下城区', value: '下城区' },
|
||
{ label: '江干区', value: '江干区' },
|
||
{ label: '拱墅区', value: '拱墅区' },
|
||
{ label: '西湖区', value: '西湖区' },
|
||
{ label: '滨江区', value: '滨江区' },
|
||
{ label: '萧山区', value: '萧山区' },
|
||
{ label: '余杭区', value: '余杭区' }
|
||
]
|
||
}
|
||
|
||
// 监听 address 变化 用于更新弹窗的地址信息
|
||
watch(() => props.address, (newVal) => {
|
||
if (newVal) {
|
||
form.value = { ...newVal }
|
||
// 更新城市和区县选项
|
||
handleProvinceChange(newVal.province)
|
||
if (newVal.city) {
|
||
handleCityChange(newVal.city)
|
||
}
|
||
}
|
||
}, { deep: true })
|
||
// 监听 visible 变化 用于更新弹窗的显示状态
|
||
watch(() => props.visible, (newVal) => {
|
||
visible.value = newVal
|
||
})
|
||
// 监听弹窗关闭事件
|
||
watch(() => props.visible, (newVal) => {
|
||
if (!newVal) {
|
||
// 重置表单
|
||
form.value = {
|
||
id:0,
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
isDefault: false
|
||
}
|
||
addressInput.value = ''
|
||
recognitionError.value = ''
|
||
errors.value = {
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
isDefault: ''
|
||
}
|
||
cityOptions.value = []
|
||
districtOptions.value = []
|
||
}
|
||
})
|
||
|
||
// 地址识别处理
|
||
const handleAddressRecognition = () => {
|
||
if (!addressInput.value) {
|
||
recognitionError.value = ''
|
||
return
|
||
}
|
||
|
||
// 简单的地址识别逻辑(实际项目中可以使用更复杂的算法或API)
|
||
const address = addressInput.value
|
||
let province = ''
|
||
let city = ''
|
||
let district = ''
|
||
let detail = ''
|
||
|
||
// 识别省份
|
||
const provinces = provinceOptions.value.map(p => p.value)
|
||
for (const p of provinces) {
|
||
if (address.includes(p)) {
|
||
province = p
|
||
break
|
||
}
|
||
}
|
||
|
||
// 识别城市
|
||
// if (province && (cityData as Record<string, { label: string; value: string }[]>)[province]) {
|
||
// const cities = cityData[province].map((c: { value: any }) => c.value)
|
||
// for (const c of cities) {
|
||
// if (address.includes(c)) {
|
||
// city = c
|
||
// break
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// 识别区县
|
||
// if (province && city && districtData[`${province}-${city}`]) {
|
||
// const districts = districtData[`${province}-${city}`].map((d: { value: any }) => d.value)
|
||
// for (const d of districts) {
|
||
// if (address.includes(d)) {
|
||
// district = d
|
||
// break
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// 提取详细地址
|
||
if (province && city && district) {
|
||
detail = address.replace(province, '').replace(city, '').replace(district, '').trim()
|
||
} else if (province && city) {
|
||
detail = address.replace(province, '').replace(city, '').trim()
|
||
} else if (province) {
|
||
detail = address.replace(province, '').trim()
|
||
} else {
|
||
detail = address
|
||
}
|
||
|
||
// 更新表单
|
||
if (province) {
|
||
form.value.province = province
|
||
handleProvinceChange(province)
|
||
if (city) {
|
||
form.value.city = city
|
||
handleCityChange(city)
|
||
if (district) {
|
||
form.value.district = district
|
||
}
|
||
}
|
||
form.value.detail = detail
|
||
recognitionError.value = ''
|
||
} else {
|
||
recognitionError.value = '未能识别出省份,请手动选择'
|
||
}
|
||
}
|
||
|
||
// 省份变化处理
|
||
const handleProvinceChange = (province: string) => {
|
||
form.value.province = province
|
||
form.value.city = ''
|
||
form.value.district = ''
|
||
|
||
// 更新城市选项
|
||
if (province && cityData[province]) {
|
||
cityOptions.value = cityData[province]
|
||
} else {
|
||
cityOptions.value = []
|
||
}
|
||
districtOptions.value = []
|
||
|
||
// 清除验证错误
|
||
errors.value.province = ''
|
||
errors.value.city = ''
|
||
errors.value.district = ''
|
||
}
|
||
|
||
// 城市变化处理
|
||
const handleCityChange = (city: string) => {
|
||
form.value.city = city
|
||
form.value.district = ''
|
||
|
||
// 更新区县选项
|
||
if (form.value.province && city && districtData[`${form.value.province}-${city}`]) {
|
||
districtOptions.value = districtData[`${form.value.province}-${city}`] ?? []
|
||
} else {
|
||
districtOptions.value = []
|
||
}
|
||
|
||
// 清除验证错误
|
||
errors.value.city = ''
|
||
errors.value.district = ''
|
||
}
|
||
|
||
// 表单验证
|
||
const validateForm = (): boolean => {
|
||
let isValid = true
|
||
|
||
// 重置错误信息
|
||
errors.value = {
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
isDefault: ''
|
||
}
|
||
|
||
// 验证省份
|
||
if (!form.value.province) {
|
||
errors.value.province = '请选择省份'
|
||
isValid = false
|
||
}
|
||
|
||
// 验证城市
|
||
if (!form.value.city) {
|
||
errors.value.city = '请选择城市'
|
||
isValid = false
|
||
}
|
||
|
||
// 验证区县
|
||
if (!form.value.district) {
|
||
errors.value.district = '请选择区县'
|
||
isValid = false
|
||
}
|
||
|
||
// 验证详细地址
|
||
if (!form.value.detail) {
|
||
errors.value.detail = '请输入详细地址'
|
||
isValid = false
|
||
} else if (form.value.detail.length < 5) {
|
||
errors.value.detail = '详细地址至少需要5个字符'
|
||
isValid = false
|
||
}
|
||
|
||
return isValid
|
||
}
|
||
|
||
// 点击确认按钮 触发事件 用于更新父组件的 address
|
||
const handleOk = () => {
|
||
// 表单验证
|
||
if (!validateForm()) {
|
||
return
|
||
}
|
||
emit('update:updateaddress', form.value)
|
||
emit('update:visible', false)
|
||
}
|
||
|
||
// 点击取消按钮
|
||
const handleCancel = () => {
|
||
emit('update:visible', false)
|
||
}
|
||
</script>
|
||
<style scoped>
|
||
.modal-content {
|
||
padding: var(--spacing-md);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: var(--spacing-sm);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.required {
|
||
color: var(--error-color, #ff4d4f);
|
||
}
|
||
|
||
.error-message {
|
||
color: var(--error-color, #ff4d4f);
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: var(--spacing-sm);
|
||
padding: var(--spacing-md);
|
||
border-top: 1px solid var(--border-light, #e8e8e8);
|
||
margin-top: var(--spacing-lg);
|
||
}
|
||
|
||
/* 为textarea添加最小高度 */
|
||
:deep(.ant-input-textarea) {
|
||
min-height: 80px;
|
||
}
|
||
</style>
|