Files
device-voice-h5/pages/device-exchange/device-exchange.vue
sexygoat b611808316
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m48s
fix: 优化换货
2026-05-14 12:14:05 +08:00

476 lines
12 KiB
Vue

<template>
<view class="container">
<view v-if="!exchangeData && !loading" class="empty-state">
<view class="empty-icon">📧</view>
<view class="empty-title">暂无换货记录</view>
<view class="empty-desc">当前账号下暂无设备换货记录</view>
</view>
<view v-else-if="exchangeData" class="card exchange-card">
<view class="exchange-header flex-row-sb">
<view class="exchange-no">{{ exchangeData.exchange_no }}</view>
<view class="tag-apple" :class="getStatusClass(exchangeData.status)">
{{ exchangeData.status_text }}
</view>
</view>
<view class="exchange-info">
<view class="info-row flex-row-sb">
<view class="info-label">换货原因</view>
<view class="info-value">{{ exchangeData.exchange_reason }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">申请时间</view>
<view class="info-value">{{ formatTime(exchangeData.created_at) }}</view>
</view>
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_name">
<view class="info-label">收件人</view>
<view class="info-value">{{ exchangeData.recipient_name }}</view>
</view>
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_phone">
<view class="info-label">联系电话</view>
<view class="info-value">{{ exchangeData.recipient_phone }}</view>
</view>
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_address">
<view class="info-label">收货地址</view>
<view class="info-value address-value">{{ exchangeData.recipient_address }}</view>
</view>
</view>
<view class="exchange-actions" v-if="exchangeData.status === 1">
<button class="btn-apple btn-primary" @tap="openPopup">填写信息</button>
</view>
</view>
<up-popup :show="showPopup" mode="center" @close="handlePopupClose">
<view class="popup-content">
<view class="popup-title">填写换货信息</view>
<view class="form-item">
<view class="form-label">粘贴收货信息</view>
<up-textarea
v-model="form.raw_shipping_text"
placeholder="可粘贴包含收件人、电话、地址的一整段文字"
autoHeight
:maxlength="-1"
border="surround"
/>
<view class="form-helper">识别结果可继续手动修改</view>
<button class="btn-apple btn-secondary parse-btn" @tap="fillFromPastedText">
识别并填充
</button>
</view>
<view class="form-item">
<view class="form-label">收件人姓名</view>
<up-input
v-model="form.recipient_name"
placeholder="请输入收件人姓名"
border="surround"
/>
</view>
<view class="form-item">
<view class="form-label">收件人电话</view>
<up-input
v-model="form.recipient_phone"
placeholder="请输入收件人电话"
border="surround"
/>
</view>
<view class="form-item">
<view class="form-label">所在地区</view>
<view class="region-trigger" @tap="openAreaPicker">
<text class="region-trigger-text" :class="{ placeholder: !regionDisplayText }">
{{ regionDisplayText || '请选择省 / 市 / 区' }}
</text>
</view>
</view>
<view class="form-item">
<view class="form-label">详细地址</view>
<up-textarea
v-model="form.recipient_address_detail"
placeholder="请输入详细收货地址"
autoHeight
:maxlength="-1"
border="surround"
/>
</view>
<view class="popup-btn-group">
<button class="btn-apple btn-secondary" @tap="handlePopupClose">取消</button>
<button class="btn-apple btn-primary" @tap="submitExchange">提交</button>
</view>
</view>
</up-popup>
<up-picker
:show="showAreaPicker"
:columns="areaPickerColumns"
keyName="label"
title="选择所在地区"
:defaultIndex="areaPickerDefaultIndex"
closeOnClickOverlay
@confirm="confirmAreaPicker"
@change="handleAreaPickerChange"
@cancel="showAreaPicker = false"
@close="showAreaPicker = false"
/>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { exchangeApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import {
buildRegionColumns,
composeRecipientAddress,
getRegionDisplayText,
parseSavedRecipientAddress,
parseShippingText
} from '@/utils/shippingAddress.js';
const PHONE_REG = /^1[3-9]\d{9}$/;
const userStore = useUserStore();
const exchangeData = ref(null);
const loading = ref(false);
const showPopup = ref(false);
const showAreaPicker = ref(false);
const areaPickerColumns = ref([]);
const areaPickerDefaultIndex = ref([0, 0, 0]);
const form = reactive({
raw_shipping_text: '',
recipient_name: '',
recipient_phone: '',
recipient_region_codes: [],
recipient_address_detail: ''
});
const regionDisplayText = computed(() => {
return getRegionDisplayText(form.recipient_region_codes);
});
const getStatusClass = (status) => {
const classMap = {
1: 'tag-warning',
2: 'tag-primary',
3: 'tag-success',
4: 'tag-success',
5: 'tag-danger'
};
return classMap[status] || '';
};
const formatTime = (time) => {
if (!time) return '-';
return String(time).split('T').join(' ').slice(0, 19);
};
const syncAreaPickerState = (regionCodes = []) => {
const { columns, defaultIndex } = buildRegionColumns(regionCodes);
areaPickerColumns.value = columns;
areaPickerDefaultIndex.value = defaultIndex.map((index) => (index < 0 ? 0 : index));
};
const loadExchange = async () => {
loading.value = true;
try {
const data = await exchangeApi.getPending(userStore.state.identifier);
exchangeData.value = data || null;
} catch (error) {
console.error('加载换货记录失败', error);
exchangeData.value = null;
} finally {
loading.value = false;
}
};
const handlePopupClose = () => {
showPopup.value = false;
showAreaPicker.value = false;
};
const openAreaPicker = () => {
syncAreaPickerState(form.recipient_region_codes);
showAreaPicker.value = true;
};
const openPopup = () => {
const currentExchange = exchangeData.value || {};
const { regionCodes, detailAddress } = parseSavedRecipientAddress(
currentExchange.recipient_address || ''
);
form.raw_shipping_text = '';
form.recipient_name = currentExchange.recipient_name || '';
form.recipient_phone = currentExchange.recipient_phone || '';
form.recipient_region_codes = regionCodes;
form.recipient_address_detail = detailAddress;
syncAreaPickerState(regionCodes);
showPopup.value = true;
};
const handleAreaPickerChange = (event) => {
const selectedValues = event.value || [];
const selectedProvinceCode = selectedValues[0]?.value || '';
const selectedCityCode =
event.columnIndex === 0 ? '' : selectedValues[1]?.value || '';
const selectedDistrictCode =
event.columnIndex === 2 ? selectedValues[2]?.value || '' : '';
const { columns, defaultIndex } = buildRegionColumns([
selectedProvinceCode,
selectedCityCode,
selectedDistrictCode
]);
areaPickerColumns.value = columns;
areaPickerDefaultIndex.value = defaultIndex.map((index) => (index < 0 ? 0 : index));
};
const confirmAreaPicker = ({ value }) => {
const selectedCodes = (value || []).map((item) => item?.value).filter(Boolean);
if (selectedCodes.length === 3) {
form.recipient_region_codes = selectedCodes;
syncAreaPickerState(selectedCodes);
}
showAreaPicker.value = false;
};
const fillFromPastedText = () => {
if (!form.raw_shipping_text.trim()) {
uni.showToast({ title: '请先粘贴收货信息', icon: 'none' });
return;
}
const parsed = parseShippingText(form.raw_shipping_text);
let updatedCount = 0;
if (parsed.recipient_name) {
form.recipient_name = parsed.recipient_name;
updatedCount += 1;
}
if (parsed.recipient_phone) {
form.recipient_phone = parsed.recipient_phone;
updatedCount += 1;
}
if (parsed.regionCodes.length === 3) {
form.recipient_region_codes = parsed.regionCodes;
syncAreaPickerState(parsed.regionCodes);
updatedCount += 1;
}
if (parsed.detailAddress) {
form.recipient_address_detail = parsed.detailAddress;
updatedCount += 1;
}
if (!updatedCount) {
uni.showToast({ title: '未识别到有效收货信息', icon: 'none' });
return;
}
uni.showToast({ title: '已识别并填充', icon: 'success' });
};
const submitExchange = async () => {
if (!form.recipient_name.trim()) {
uni.showToast({ title: '请输入收件人姓名', icon: 'none' });
return;
}
if (!form.recipient_phone.trim()) {
uni.showToast({ title: '请输入收件人电话', icon: 'none' });
return;
}
if (!PHONE_REG.test(form.recipient_phone.trim())) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (form.recipient_region_codes.length !== 3) {
uni.showToast({ title: '请选择省市区', icon: 'none' });
return;
}
if (!form.recipient_address_detail.trim()) {
uni.showToast({ title: '请输入详细收货地址', icon: 'none' });
return;
}
const recipientAddress = composeRecipientAddress(
form.recipient_region_codes,
form.recipient_address_detail
);
try {
await exchangeApi.submitShippingInfo(
exchangeData.value.id,
form.recipient_name.trim(),
form.recipient_phone.trim(),
recipientAddress
);
uni.showToast({ title: '提交成功', icon: 'success' });
handlePopupClose();
await loadExchange();
} catch (error) {
console.error('提交换货信息失败', error);
}
};
onMounted(() => {
syncAreaPickerState();
loadExchange();
});
</script>
<style lang="scss" scoped>
.container {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
min-height: 60vh;
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: var(--text-tertiary);
text-align: center;
margin-bottom: 40rpx;
}
}
.exchange-card {
.exchange-header {
margin-bottom: var(--space-md);
.exchange-no {
font-size: 28rpx;
font-weight: 600;
color: var(--text-primary);
}
}
.exchange-info {
.info-row {
padding: var(--space-xs) 0;
.info-label {
font-size: 24rpx;
color: var(--text-tertiary);
}
.info-value {
font-size: 24rpx;
color: var(--text-primary);
}
.address-value {
max-width: 400rpx;
text-align: right;
}
}
}
.exchange-actions {
margin-top: var(--space-lg);
padding-top: var(--space-md);
border-top: 1rpx solid var(--gray-200);
.btn-apple {
width: 100%;
}
}
}
}
.popup-content {
width: 660rpx;
max-height: 80vh;
padding: 30rpx;
box-sizing: border-box;
overflow-y: auto;
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
text-align: center;
margin-bottom: var(--space-lg);
}
.form-item {
margin-bottom: var(--space-md);
.form-label {
font-size: 26rpx;
color: var(--text-secondary);
margin-bottom: var(--space-xs);
}
.form-helper {
margin-top: var(--space-xs);
font-size: 22rpx;
color: var(--text-tertiary);
}
.parse-btn {
width: 100%;
margin-top: var(--space-sm);
}
}
.region-trigger {
display: flex;
align-items: center;
min-height: 84rpx;
padding: 0 24rpx;
border: 2rpx solid var(--border-light);
border-radius: 12rpx;
background: #fff;
.region-trigger-text {
font-size: 28rpx;
color: var(--text-primary);
&.placeholder {
color: var(--text-tertiary);
}
}
}
.popup-btn-group {
display: flex;
gap: var(--space-md);
margin-top: var(--space-lg);
.btn-apple {
flex: 1;
}
}
}
</style>