|
@@ -0,0 +1,459 @@
|
|
|
+<template>
|
|
|
+ <el-select
|
|
|
+ v-model="currentValue"
|
|
|
+ :placeholder="placeholder"
|
|
|
+ :disabled="disabled"
|
|
|
+ :clearable="clearable"
|
|
|
+ :filterable="false"
|
|
|
+ :loading="loading"
|
|
|
+ :size="size"
|
|
|
+ :class="selectClass"
|
|
|
+ @change="handleChange"
|
|
|
+ @clear="handleClear"
|
|
|
+ @focus="handleFocus"
|
|
|
+ @blur="handleBlur"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in addressOptions"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.id"
|
|
|
+ >
|
|
|
+ <span style="float: left">{{ item.receiverName }}</span>
|
|
|
+ <span style="float: right; color: #8492a6; font-size: 13px">{{ item.receiverPhone }}</span>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getList } from '@/api/order/address'
|
|
|
+
|
|
|
+/**
|
|
|
+ * @typedef {import('@/api/types/address').CustomerAddressRecord} CustomerAddressRecord
|
|
|
+ * @typedef {import('@/api/types/address').CustomerAddressQueryParams} CustomerAddressQueryParams
|
|
|
+ * @typedef {import('@/api/types/address').CustomerAddressListResponse} CustomerAddressListResponse
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 地址选择器选项接口
|
|
|
+ * @typedef {Object} AddressSelectOption
|
|
|
+ * @property {string|number} id - 地址唯一标识
|
|
|
+ * @property {string} label - 显示标签
|
|
|
+ * @property {string} receiverName - 收货人姓名
|
|
|
+ * @property {string} receiverPhone - 收货人电话
|
|
|
+ * @property {string} regionCode - 地区编码
|
|
|
+ * @property {string} regionName - 地区名称
|
|
|
+ * @property {string} detailAddress - 详细地址
|
|
|
+ * @property {string} postalCode - 邮政编码
|
|
|
+ * @property {0|1} isDefault - 是否默认地址
|
|
|
+ * @property {CustomerAddressRecord} addressData - 完整地址数据
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 地址回显信息接口
|
|
|
+ * @typedef {Object} AddressEchoInfo
|
|
|
+ * @property {string} receiverName - 收货人姓名
|
|
|
+ * @property {string} receiverPhone - 收货人电话
|
|
|
+ * @property {string} regionName - 地区名称
|
|
|
+ * @property {string} detailAddress - 详细地址
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 地址选择事件数据接口
|
|
|
+ * @typedef {Object} AddressSelectedData
|
|
|
+ * @property {string|number} addressId - 地址ID
|
|
|
+ * @property {string} receiverName - 收货人姓名
|
|
|
+ * @property {string} receiverPhone - 收货人电话
|
|
|
+ * @property {string} regionCode - 地区编码
|
|
|
+ * @property {string} regionName - 地区名称
|
|
|
+ * @property {string} detailAddress - 详细地址
|
|
|
+ * @property {string} postalCode - 邮政编码
|
|
|
+ * @property {CustomerAddressRecord} [addressData] - 完整地址数据
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 地址选择组件
|
|
|
+ * @description 基于Element UI Select的地址选择组件,根据客户编码动态加载地址列表
|
|
|
+ * @component
|
|
|
+ */
|
|
|
+export default {
|
|
|
+ name: 'AddressSelect',
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组件属性定义
|
|
|
+ * @description 定义组件接收的外部属性及其类型约束
|
|
|
+ */
|
|
|
+ props: {
|
|
|
+ /**
|
|
|
+ * 绑定值
|
|
|
+ * @description 当前选中的地址ID
|
|
|
+ * @type {string|number}
|
|
|
+ * @default ''
|
|
|
+ */
|
|
|
+ value: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: '',
|
|
|
+ validator: (value) => value === '' || value === null || value === undefined || typeof value === 'string' || typeof value === 'number'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 客户编码
|
|
|
+ * @description 用于筛选地址的客户编码,当此值变化时会重新加载地址列表
|
|
|
+ * @type {string}
|
|
|
+ * @default ''
|
|
|
+ */
|
|
|
+ customerCode: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ validator: (value) => typeof value === 'string'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 占位符文本
|
|
|
+ * @description 输入框的占位符提示文本
|
|
|
+ * @type {string}
|
|
|
+ * @default '请选择收货地址'
|
|
|
+ */
|
|
|
+ placeholder: {
|
|
|
+ type: String,
|
|
|
+ default: '请选择收货地址',
|
|
|
+ validator: (value) => typeof value === 'string'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 是否禁用
|
|
|
+ * @description 控制组件是否处于禁用状态
|
|
|
+ * @type {boolean}
|
|
|
+ * @default false
|
|
|
+ */
|
|
|
+ disabled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ validator: (value) => typeof value === 'boolean'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 是否可清空
|
|
|
+ * @description 控制是否显示清空按钮
|
|
|
+ * @type {boolean}
|
|
|
+ * @default true
|
|
|
+ */
|
|
|
+ clearable: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ validator: (value) => typeof value === 'boolean'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 组件尺寸
|
|
|
+ * @description 控制组件的显示尺寸
|
|
|
+ * @type {'large'|'medium'|'small'|'mini'}
|
|
|
+ * @default 'small'
|
|
|
+ */
|
|
|
+ size: {
|
|
|
+ type: String,
|
|
|
+ default: 'small',
|
|
|
+ validator: (value) => ['large', 'medium', 'small', 'mini'].includes(value)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组件响应式数据
|
|
|
+ * @description 定义组件的内部状态数据
|
|
|
+ * @returns {Object} 组件数据对象
|
|
|
+ */
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ /**
|
|
|
+ * 当前选中值
|
|
|
+ * @type {string|number}
|
|
|
+ */
|
|
|
+ currentValue: this.value,
|
|
|
+ /**
|
|
|
+ * 地址选项列表
|
|
|
+ * @type {AddressSelectOption[]}
|
|
|
+ */
|
|
|
+ addressOptions: [],
|
|
|
+ /**
|
|
|
+ * 加载状态
|
|
|
+ * @type {boolean}
|
|
|
+ */
|
|
|
+ loading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算属性
|
|
|
+ * @description 组件的响应式计算属性
|
|
|
+ */
|
|
|
+ computed: {
|
|
|
+ /**
|
|
|
+ * 选择器样式类
|
|
|
+ * @description 返回与Avue组件一致的样式类名
|
|
|
+ * @returns {Object} 样式类对象
|
|
|
+ */
|
|
|
+ selectClass() {
|
|
|
+ return {
|
|
|
+ 'avue-select': true,
|
|
|
+ 'el-select': true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 监听器
|
|
|
+ * @description 监听属性变化并执行相应的处理逻辑
|
|
|
+ */
|
|
|
+ watch: {
|
|
|
+ /**
|
|
|
+ * 监听value属性变化
|
|
|
+ * @description 当外部传入的value发生变化时,同步更新内部状态
|
|
|
+ * @param {string|number} newVal - 新的地址ID值
|
|
|
+ * @param {string|number} oldVal - 旧的地址ID值
|
|
|
+ */
|
|
|
+ value(newVal, oldVal) {
|
|
|
+ if (newVal !== oldVal) {
|
|
|
+ this.currentValue = newVal
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 监听currentValue变化
|
|
|
+ * @description 当内部值发生变化时,向父组件发送input事件
|
|
|
+ * @param {string|number} newVal - 新的值
|
|
|
+ */
|
|
|
+ currentValue(newVal) {
|
|
|
+ this.$emit('input', newVal)
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 监听customerCode变化
|
|
|
+ * @description 当客户编码变化时,重新加载地址列表并清空当前选择
|
|
|
+ * @param {string} newVal - 新的客户编码
|
|
|
+ * @param {string} oldVal - 旧的客户编码
|
|
|
+ */
|
|
|
+ customerCode(newVal, oldVal) {
|
|
|
+ if (newVal !== oldVal) {
|
|
|
+ this.currentValue = ''
|
|
|
+ this.loadAddressList(newVal)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组件挂载钩子
|
|
|
+ * @description 组件挂载完成后执行的初始化逻辑
|
|
|
+ */
|
|
|
+ mounted() {
|
|
|
+ // 组件挂载时,如果有客户编码,加载对应的地址列表
|
|
|
+ if (this.customerCode) {
|
|
|
+ this.loadAddressList(this.customerCode)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组件方法
|
|
|
+ * @description 组件的业务逻辑方法集合
|
|
|
+ */
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 加载地址列表
|
|
|
+ * @description 根据客户编码异步加载地址列表数据
|
|
|
+ * @param {string} customerCode - 客户编码
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ * @throws {Error} 当API调用失败时抛出异常
|
|
|
+ */
|
|
|
+ async loadAddressList(customerCode) {
|
|
|
+ if (!customerCode || typeof customerCode !== 'string') {
|
|
|
+ this.addressOptions = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.loading = true
|
|
|
+
|
|
|
+ /** @type {CustomerAddressQueryParams} */
|
|
|
+ const queryParams = {
|
|
|
+ customerCode: customerCode
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @type {CustomerAddressListResponse} */
|
|
|
+ const response = await getList(1, 100, queryParams)
|
|
|
+
|
|
|
+ if (response?.data?.success && response.data.data?.records) {
|
|
|
+ this.addressOptions = this.transformAddressData(response.data.data.records)
|
|
|
+ } else {
|
|
|
+ this.addressOptions = []
|
|
|
+ const errorMsg = response?.data?.msg || '获取地址列表失败'
|
|
|
+ console.warn('获取地址列表失败:', errorMsg)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载地址列表API调用失败:', error)
|
|
|
+ this.addressOptions = []
|
|
|
+ this.$message.error('网络错误,加载地址列表失败')
|
|
|
+ throw error
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换地址数据
|
|
|
+ * @description 将API返回的地址数据转换为组件所需的选项格式
|
|
|
+ * @param {CustomerAddressRecord[]} addressRecords - 地址记录数组
|
|
|
+ * @returns {AddressSelectOption[]} 转换后的地址选项数组
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ transformAddressData(addressRecords) {
|
|
|
+ return addressRecords.map(address => ({
|
|
|
+ id: address.id,
|
|
|
+ label: `${address.receiverName} - ${address.receiverPhone} - ${address.regionName}${address.detailAddress}`,
|
|
|
+ receiverName: address.receiverName,
|
|
|
+ receiverPhone: address.receiverPhone,
|
|
|
+ regionCode: address.regionCode,
|
|
|
+ regionName: address.regionName,
|
|
|
+ detailAddress: address.detailAddress,
|
|
|
+ postalCode: address.postalCode || '',
|
|
|
+ isDefault: address.isDefault,
|
|
|
+ addressData: address
|
|
|
+ }))
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置回显值
|
|
|
+ * @description 在编辑模式下,根据地址信息匹配并设置选中的地址
|
|
|
+ * @param {AddressEchoInfo} addressInfo - 地址信息对象
|
|
|
+ * @returns {void}
|
|
|
+ */
|
|
|
+ setEchoValue(addressInfo) {
|
|
|
+ if (!this.customerCode || !addressInfo) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果地址列表为空,先加载数据
|
|
|
+ if (this.addressOptions.length === 0) {
|
|
|
+ this.loadAddressList(this.customerCode).then(() => {
|
|
|
+ this.matchAndSetAddress(addressInfo)
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('加载地址列表失败:', error)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.matchAndSetAddress(addressInfo)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 匹配并设置地址
|
|
|
+ * @description 根据地址信息匹配地址列表中的项目并设置选中值
|
|
|
+ * @param {AddressEchoInfo} addressInfo - 地址信息对象
|
|
|
+ * @returns {void}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ matchAndSetAddress(addressInfo) {
|
|
|
+ /** @type {AddressSelectOption|undefined} */
|
|
|
+ const matchedAddress = this.addressOptions.find(address => {
|
|
|
+ return address.receiverName === addressInfo.receiverName &&
|
|
|
+ address.receiverPhone === addressInfo.receiverPhone &&
|
|
|
+ address.regionName === addressInfo.regionName &&
|
|
|
+ address.detailAddress === addressInfo.detailAddress
|
|
|
+ })
|
|
|
+
|
|
|
+ if (matchedAddress) {
|
|
|
+ this.currentValue = matchedAddress.id
|
|
|
+ // 触发选择事件,确保父组件状态同步
|
|
|
+ this.handleChange(matchedAddress.id)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 选择变化处理
|
|
|
+ * @description 当用户选择地址时触发,更新内部状态并向父组件发送相关事件
|
|
|
+ * @param {string|number} value - 选中的地址ID
|
|
|
+ * @returns {void}
|
|
|
+ */
|
|
|
+ handleChange(value) {
|
|
|
+ this.currentValue = value
|
|
|
+ this.$emit('change', value)
|
|
|
+
|
|
|
+ /** @type {AddressSelectOption|undefined} */
|
|
|
+ const selectedAddress = this.addressOptions.find(item => item.id == value)
|
|
|
+
|
|
|
+ if (selectedAddress) {
|
|
|
+ /** @type {AddressSelectedData} */
|
|
|
+ const addressData = {
|
|
|
+ addressId: selectedAddress.id,
|
|
|
+ receiverName: selectedAddress.receiverName,
|
|
|
+ receiverPhone: selectedAddress.receiverPhone,
|
|
|
+ regionCode: selectedAddress.regionCode,
|
|
|
+ regionName: selectedAddress.regionName,
|
|
|
+ detailAddress: selectedAddress.detailAddress,
|
|
|
+ postalCode: selectedAddress.postalCode,
|
|
|
+ addressData: selectedAddress.addressData
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$emit('address-selected', addressData)
|
|
|
+ } else {
|
|
|
+ // 如果没有找到对应的地址,发送空数据
|
|
|
+ this.emitEmptyAddressData()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送空地址数据
|
|
|
+ * @description 发送空的地址选择事件数据
|
|
|
+ * @returns {void}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ emitEmptyAddressData() {
|
|
|
+ /** @type {AddressSelectedData} */
|
|
|
+ const emptyData = {
|
|
|
+ addressId: '',
|
|
|
+ receiverName: '',
|
|
|
+ receiverPhone: '',
|
|
|
+ regionCode: '',
|
|
|
+ regionName: '',
|
|
|
+ detailAddress: '',
|
|
|
+ postalCode: ''
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$emit('address-selected', emptyData)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清空处理
|
|
|
+ * @description 清空选择并发送相应事件
|
|
|
+ * @returns {void}
|
|
|
+ */
|
|
|
+ handleClear() {
|
|
|
+ this.currentValue = ''
|
|
|
+ this.$emit('clear')
|
|
|
+ this.emitEmptyAddressData()
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得焦点处理
|
|
|
+ * @description 组件获得焦点时的处理逻辑
|
|
|
+ * @returns {void}
|
|
|
+ */
|
|
|
+ handleFocus() {
|
|
|
+ this.$emit('focus')
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 失去焦点处理
|
|
|
+ * @description 组件失去焦点时的处理逻辑
|
|
|
+ * @returns {void}
|
|
|
+ */
|
|
|
+ handleBlur() {
|
|
|
+ this.$emit('blur')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 确保样式与Avue select保持一致 */
|
|
|
+.avue-select {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 选项样式优化 */
|
|
|
+.el-select-dropdown__item {
|
|
|
+ height: auto;
|
|
|
+ line-height: 1.5;
|
|
|
+ padding: 8px 20px;
|
|
|
+}
|
|
|
+</style>
|