feat: new

This commit is contained in:
luo
2026-09-16 16:32:08 +08:00
parent 6a1543f9f6
commit 8bd9de15cd
22 changed files with 2502 additions and 33 deletions

View File

@@ -0,0 +1,296 @@
<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>
<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='请输入收件人电话' border='surround' maxlength='20'/>
</view>
<view class='form-item'>
<view class='form-label'>收货地址</view>
<up-textarea v-model='addressForm.recipient_address' placeholder='请输入完整收货地址'/>
</view>
<view class='popup-actions'>
<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>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { popupApi, notificationApi } from '@/api/index.js';
import { handledPopupNotificationIds } from '@/utils/popup.js';
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 addressForm = reactive({
recipient_name: '',
recipient_phone: '',
recipient_address: ''
});
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 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.recipient_name = '';
addressForm.recipient_phone = '';
addressForm.recipient_address = '';
};
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;
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 { recipient_name, recipient_phone, recipient_address } = addressForm;
if (!recipient_name || !recipient_phone || !recipient_address) {
uni.showToast({ title: '请填写完整收货信息', icon: 'none' });
return;
}
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,
recipient_phone,
recipient_address
});
await markPopupRead();
step.value = 'done';
} catch (e) {
console.error('提交风险换卡收货地址失败', e);
} finally {
submitting.value = false;
}
};
const finish = () => {
visible.value = false;
emit('close', 'done');
};
onMounted(async () => {
if (!props.identifier) {
emit('close', 'empty');
return;
}
try {
const data = await popupApi.getCandidate(props.page, props.identifier);
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) {
// 资源不可见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;
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 {
padding: 0 32rpx 20rpx;
}
.form-label {
margin-bottom: 12rpx;
color: var(--text-primary);
font-size: 26rpx;
}
.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>