510 lines
14 KiB
Vue
510 lines
14 KiB
Vue
<template>
|
||
<view v-if='visible' class='popup-candidate' @tap.stop>
|
||
<view class='popup-candidate-card'>
|
||
<template v-if='isCandidateStep'>
|
||
<view class='popup-header'>
|
||
<view class='popup-heading'>{{ candidate?.title || '提示' }}</view>
|
||
<view class='popup-close' role='button' aria-label='关闭' @tap='dismiss'>×</view>
|
||
</view>
|
||
<scroll-view scroll-y class='popup-body'>
|
||
<text>{{ candidate?.body || '' }}</text>
|
||
</scroll-view>
|
||
<view class='popup-actions'>
|
||
<up-button v-if='isRiskExchange' type='primary' @tap='goToAddress'>申请换卡</up-button>
|
||
<up-button v-else-if='actionLabel' type='primary' @tap='handleAction'>{{ actionLabel }}</up-button>
|
||
<up-button plain @tap='dismiss'>稍后处理</up-button>
|
||
</view>
|
||
</template>
|
||
|
||
<template v-else-if='isAddressStep'>
|
||
<view class='popup-header'>
|
||
<view class='popup-heading'>换货收货信息</view>
|
||
<view class='popup-close' role='button' aria-label='关闭' @tap='dismiss'>×</view>
|
||
</view>
|
||
<scroll-view scroll-y class='popup-address-form'>
|
||
<view class='form-item'>
|
||
<view class='form-label'>粘贴收货信息</view>
|
||
<up-textarea
|
||
v-model='addressForm.raw_shipping_text'
|
||
placeholder='可粘贴包含收件人、电话、地址的一整段文字'
|
||
autoHeight
|
||
:maxlength='-1'
|
||
border='surround'
|
||
/>
|
||
<view class='form-helper'>识别结果可继续手动修改</view>
|
||
<view class='paste-action-row'>
|
||
<button class='btn-apple btn-secondary paste-action-btn' @tap='clearPastedText'>清除</button>
|
||
<button class='btn-apple btn-secondary paste-action-btn' @tap='fillFromPastedText'>识别并填充</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class='form-item'>
|
||
<view class='form-label'>收件人姓名</view>
|
||
<up-input v-model='addressForm.recipient_name' placeholder='请输入收件人姓名' border='surround' maxlength='50'/>
|
||
</view>
|
||
<view class='form-item'>
|
||
<view class='form-label'>收件人电话</view>
|
||
<up-input v-model='addressForm.recipient_phone' type='number' placeholder='请输入11位手机号' border='surround' maxlength='11'/>
|
||
</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='addressForm.recipient_address_detail'
|
||
placeholder='请输入详细收货地址'
|
||
autoHeight
|
||
:maxlength='-1'
|
||
border='surround'
|
||
/>
|
||
</view>
|
||
</scroll-view>
|
||
<view class='popup-address-actions'>
|
||
<up-button plain @tap='dismiss'>取消</up-button>
|
||
<up-button type='primary' :loading='submitting' :disabled='submitting' @tap='submitAddress'>提交</up-button>
|
||
</view>
|
||
</template>
|
||
|
||
<template v-else-if='isDoneStep'>
|
||
<view class='popup-header'>
|
||
<view class='popup-heading'>申请已提交</view>
|
||
</view>
|
||
<view class='result-item'>
|
||
<view class='result-label'>换货单号</view>
|
||
<view class='result-value'>{{ exchange?.exchange_no || '-' }}</view>
|
||
</view>
|
||
<view class='result-item'>
|
||
<view class='result-label'>状态</view>
|
||
<view class='result-value'>{{ exchange?.status_name || '-' }}</view>
|
||
</view>
|
||
<view v-if='exchange?.recipient_address' class='result-item'>
|
||
<view class='result-label'>收货地址</view>
|
||
<view class='result-value'>{{ exchange.recipient_address }}</view>
|
||
</view>
|
||
<view class='popup-actions'>
|
||
<up-button type='primary' @tap='finish'>完成</up-button>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
<up-picker
|
||
:show='showAreaPicker'
|
||
:columns='areaPickerColumns'
|
||
keyName='label'
|
||
title='选择所在地区'
|
||
:defaultIndex='areaPickerDefaultIndex'
|
||
closeOnClickOverlay
|
||
@confirm='confirmAreaPicker'
|
||
@change='handleAreaPickerChange'
|
||
@cancel='showAreaPicker = false'
|
||
@close='showAreaPicker = false'
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
||
import { popupApi, notificationApi } from '@/api/index.js';
|
||
import { handledPopupNotificationIds } from '@/utils/popup.js';
|
||
import {
|
||
buildRegionColumns,
|
||
composeRecipientAddress,
|
||
getRegionDisplayText,
|
||
parseShippingText
|
||
} from '@/utils/shippingAddress.js';
|
||
|
||
const PHONE_REG = /^1[3-9]\d{9}$/;
|
||
|
||
const props = defineProps({
|
||
page: {
|
||
type: String,
|
||
required: true
|
||
},
|
||
identifier: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['close']);
|
||
|
||
const visible = ref(false);
|
||
const step = ref('candidate');
|
||
const candidate = ref(null);
|
||
const exchange = ref(null);
|
||
const submitting = ref(false);
|
||
const showAreaPicker = ref(false);
|
||
const areaPickerColumns = ref([]);
|
||
const areaPickerDefaultIndex = ref([0, 0, 0]);
|
||
const addressForm = reactive({
|
||
raw_shipping_text: '',
|
||
recipient_name: '',
|
||
recipient_phone: '',
|
||
recipient_region_codes: [],
|
||
recipient_address_detail: ''
|
||
});
|
||
|
||
const isCandidateStep = computed(() => step.value === 'candidate');
|
||
const isAddressStep = computed(() => step.value === 'address');
|
||
const isDoneStep = computed(() => step.value === 'done');
|
||
const isRiskExchange = computed(() => candidate.value?.popup_type === 'risk_exchange');
|
||
const regionDisplayText = computed(() => getRegionDisplayText(addressForm.recipient_region_codes));
|
||
|
||
const actionLabelMap = {
|
||
package_purchase: '立即购买',
|
||
asset_wallet_recharge: '立即充值'
|
||
};
|
||
|
||
const actionRouteMap = {
|
||
package_purchase: '/pages/package-order/package-order',
|
||
asset_wallet_recharge: '/pages/my-wallet/my-wallet'
|
||
};
|
||
|
||
const actionLabel = computed(() => actionLabelMap[candidate.value?.action_type] || '');
|
||
|
||
const goToAddress = () => {
|
||
step.value = 'address';
|
||
};
|
||
|
||
const resetForm = () => {
|
||
addressForm.raw_shipping_text = '';
|
||
addressForm.recipient_name = '';
|
||
addressForm.recipient_phone = '';
|
||
addressForm.recipient_region_codes = [];
|
||
addressForm.recipient_address_detail = '';
|
||
};
|
||
|
||
const syncAreaPickerState = (regionCodes = []) => {
|
||
const { columns, defaultIndex } = buildRegionColumns(regionCodes);
|
||
areaPickerColumns.value = columns;
|
||
areaPickerDefaultIndex.value = defaultIndex.map((index) => (index < 0 ? 0 : index));
|
||
};
|
||
|
||
const openAreaPicker = () => {
|
||
syncAreaPickerState(addressForm.recipient_region_codes);
|
||
showAreaPicker.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) {
|
||
addressForm.recipient_region_codes = selectedCodes;
|
||
syncAreaPickerState(selectedCodes);
|
||
}
|
||
showAreaPicker.value = false;
|
||
};
|
||
|
||
const fillFromPastedText = () => {
|
||
if (!addressForm.raw_shipping_text.trim()) {
|
||
uni.showToast({ title: '请先粘贴收货信息', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
const parsed = parseShippingText(addressForm.raw_shipping_text);
|
||
let updatedCount = 0;
|
||
if (parsed.recipient_name) {
|
||
addressForm.recipient_name = parsed.recipient_name;
|
||
updatedCount += 1;
|
||
}
|
||
if (parsed.recipient_phone) {
|
||
addressForm.recipient_phone = parsed.recipient_phone;
|
||
updatedCount += 1;
|
||
}
|
||
if (parsed.regionCodes.length === 3) {
|
||
addressForm.recipient_region_codes = parsed.regionCodes;
|
||
syncAreaPickerState(parsed.regionCodes);
|
||
updatedCount += 1;
|
||
}
|
||
if (parsed.detailAddress) {
|
||
addressForm.recipient_address_detail = parsed.detailAddress;
|
||
updatedCount += 1;
|
||
}
|
||
|
||
if (!updatedCount) {
|
||
uni.showToast({ title: '未识别到有效收货信息', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.showToast({ title: '已识别并填充', icon: 'success' });
|
||
};
|
||
|
||
const clearPastedText = () => {
|
||
addressForm.raw_shipping_text = '';
|
||
};
|
||
|
||
const markPopupRead = async () => {
|
||
const id = candidate.value?.notification_id;
|
||
if (!id) return;
|
||
try {
|
||
await notificationApi.markRead(id);
|
||
} catch (e) {
|
||
console.error('标记弹窗通知已读失败', e);
|
||
}
|
||
};
|
||
|
||
const dismiss = () => {
|
||
visible.value = false;
|
||
showAreaPicker.value = false;
|
||
markPopupRead();
|
||
emit('close', 'dismissed');
|
||
};
|
||
|
||
const handleAction = () => {
|
||
const url = actionRouteMap[candidate.value?.action_type];
|
||
visible.value = false;
|
||
markPopupRead();
|
||
emit('close', 'action');
|
||
if (url) {
|
||
uni.navigateTo({ url });
|
||
}
|
||
};
|
||
|
||
const submitAddress = async () => {
|
||
if (submitting.value) return;
|
||
|
||
const recipientName = addressForm.recipient_name.trim();
|
||
const recipientPhone = addressForm.recipient_phone.trim();
|
||
const detailAddress = addressForm.recipient_address_detail.trim();
|
||
if (!recipientName) {
|
||
uni.showToast({ title: '请输入收件人姓名', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!recipientPhone) {
|
||
uni.showToast({ title: '请输入收件人电话', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!PHONE_REG.test(recipientPhone)) {
|
||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||
return;
|
||
}
|
||
if (addressForm.recipient_region_codes.length !== 3) {
|
||
uni.showToast({ title: '请选择省市区', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!detailAddress) {
|
||
uni.showToast({ title: '请输入详细收货地址', icon: 'none' });
|
||
return;
|
||
}
|
||
const recipientAddress = composeRecipientAddress(addressForm.recipient_region_codes, detailAddress);
|
||
|
||
const assetId = Number(candidate.value?.asset_id);
|
||
if (!Number.isInteger(assetId) || assetId <= 0) {
|
||
uni.showToast({ title: '资产信息无效', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
submitting.value = true;
|
||
try {
|
||
exchange.value = await popupApi.submitRiskExchangeAddress(assetId, {
|
||
recipient_name: recipientName,
|
||
recipient_phone: recipientPhone,
|
||
recipient_address: recipientAddress
|
||
});
|
||
await markPopupRead();
|
||
step.value = 'done';
|
||
} catch (e) {
|
||
console.error('提交风险换卡收货地址失败', e);
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
};
|
||
|
||
const finish = () => {
|
||
visible.value = false;
|
||
showAreaPicker.value = false;
|
||
emit('close', 'done');
|
||
};
|
||
|
||
let mounted = true;
|
||
onUnmounted(() => { mounted = false; });
|
||
onMounted(async () => {
|
||
if (!props.identifier) {
|
||
emit('close', 'empty');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const data = await popupApi.getCandidate(props.page, props.identifier);
|
||
if (!mounted) return;
|
||
const item = (data && data.candidate) || null;
|
||
if (!item || !item.notification_id || handledPopupNotificationIds.has(item.notification_id)) {
|
||
emit('close', 'empty');
|
||
return;
|
||
}
|
||
|
||
handledPopupNotificationIds.add(item.notification_id);
|
||
candidate.value = item;
|
||
step.value = 'candidate';
|
||
resetForm();
|
||
visible.value = true;
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
// 资源不可见(400/code=1180)与接口异常统一静默关闭,不做 UI 分支
|
||
console.error('查询弹窗候选失败', e);
|
||
emit('close', 'invisible');
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang='scss'>
|
||
.popup-candidate {
|
||
position: fixed;
|
||
z-index: 1100;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx;
|
||
background: rgba(0, 0, 0, 0.48);
|
||
}
|
||
|
||
.popup-candidate-card {
|
||
width: 100%;
|
||
max-width: 640rpx;
|
||
max-height: calc(100vh - 80rpx);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-radius: 24rpx;
|
||
background: #fff;
|
||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.popup-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx 32rpx 18rpx;
|
||
}
|
||
|
||
.popup-heading {
|
||
color: var(--text-primary);
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.popup-close {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
color: var(--text-tertiary);
|
||
font-size: 48rpx;
|
||
font-weight: 300;
|
||
line-height: 46rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.popup-body {
|
||
box-sizing: border-box;
|
||
max-height: 420rpx;
|
||
padding: 12rpx 32rpx 24rpx;
|
||
color: var(--text-secondary);
|
||
font-size: 28rpx;
|
||
line-height: 1.7;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.popup-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
padding: 20rpx 32rpx 32rpx;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.form-label {
|
||
margin-bottom: 10rpx;
|
||
color: var(--text-primary);
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.form-helper {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.popup-address-form {
|
||
box-sizing: border-box;
|
||
max-height: 60vh;
|
||
padding: 0 32rpx;
|
||
}
|
||
|
||
.paste-action-row,
|
||
.popup-address-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.paste-action-row {
|
||
margin-top: 16rpx;
|
||
|
||
.paste-action-btn {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.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-address-actions {
|
||
padding: 16rpx 32rpx 32rpx;
|
||
|
||
:deep(.u-button) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.result-item {
|
||
padding: 8rpx 32rpx;
|
||
}
|
||
|
||
.result-label {
|
||
color: var(--text-tertiary);
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.result-value {
|
||
margin-top: 4rpx;
|
||
color: var(--text-primary);
|
||
font-size: 28rpx;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|