feat(withdraw): build dedicated withdrawal page with optimized layout and multichannel support

这个提交包含在:
xcbyy
2026-10-13 09:19:34 +08:00
父节点 e867488d0d
当前提交 1fb0cc4273
共有 9 个文件被更改,包括 1962 次插入 和 16 次删除

查看文件

@@ -72,6 +72,36 @@ export const ICONS = {
</svg>
`),
chevronLeft: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
`),
alipay: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#0284C7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="4"></rect>
<path d="M7 9h10"></path>
<path d="M12 9v4"></path>
<path d="M8 15c2 0 4-1 6-4"></path>
<path d="M11 13c1 2 3 4 5 4"></path>
</svg>
`),
bankCard: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="5" width="20" height="14" rx="2"></rect>
<line x1="2" y1="10" x2="22" y2="10"></line>
</svg>
`),
checkCircle: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="9 12 11 14 15 10"></polyline>
</svg>
`),
starFilled: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#F59E0B">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>

查看文件

@@ -112,6 +112,16 @@
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/user/withdraw",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "收益提现",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/activity/create",
"style": {

查看文件

@@ -168,12 +168,70 @@
<image class="action-svg" :src="ICONS.chat" mode="aspectFit" />
<text class="action-text">私聊</text>
</view>
<view class="icon-action btn-active" @click="openReportModal">
<image class="action-svg" :src="ICONS.shield" mode="aspectFit" />
<text class="action-text">举报</text>
</view>
<view class="price-summary">
<text class="summary-label">当前选定:</text>
<text class="summary-price">¥{{ currentSelectedService ? currentSelectedService.price : expert.price }}</text>
</view>
<button class="btn-book btn-active" @click="goToOrder">立即预约</button>
</view>
<!-- 举报不良行为弹窗 -->
<view class="review-modal-mask" v-if="reportModalVisible" @click.self="closeReportModal">
<view class="review-modal-content">
<view class="modal-header">
<view class="modal-header-left">
<text class="modal-title">举报违规行为</text>
<text class="modal-subtitle">
举报对象:{{ expert.nickname }}
</text>
</view>
<view class="modal-close" @click="closeReportModal">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 违规类型单选 -->
<view class="tags-section">
<text class="tags-label">选择违规类型</text>
<view class="tags-wrap">
<view
v-for="item in presetReportReasons"
:key="item"
class="review-tag"
:class="{ active: reportForm.reason === item }"
@click="reportForm.reason = item"
>
{{ item }}
</view>
</view>
</view>
<!-- 举报描述 -->
<view class="textarea-section">
<text class="textarea-label">详细违规线索说明</text>
<textarea
class="review-textarea"
v-model="reportForm.description"
placeholder="请填写具体违规线索(如涉黄言论、私下诱导转账、脱离平台等),平台将严肃核处..."
placeholder-class="textarea-placeholder"
maxlength="200"
/>
<text class="char-count">{{ reportForm.description.length }}/200</text>
</view>
<!-- 操作按钮 -->
<view class="modal-actions">
<button class="modal-btn cancel-btn btn-active" @click="closeReportModal">取消</button>
<button class="modal-btn submit-btn btn-active danger" :disabled="submittingReport" @click="submitReportTicket">
{{ submittingReport ? '正在提交...' : '确认提交举报' }}
</button>
</view>
</view>
</view>
</view>
</template>
@@ -181,7 +239,7 @@
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import SkeletonScreen from '@/components/SkeletonScreen.vue';
import { getCompanionDetail, getExpertReviews, replyOrderReview } from '@/common/api.js';
import { getCompanionDetail, getExpertReviews, replyOrderReview, submitReport as submitReportApi } from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
const isLoading = ref(true);
@@ -325,6 +383,57 @@ const goToOrder = () => {
url: `/pages/order/create?expertId=${expert.value.id}&serviceId=${srv.id}`
});
};
// 举报违规行为状态与提交
const reportModalVisible = ref(false);
const submittingReport = ref(false);
const presetReportReasons = [
'涉黄低俗言论',
'私下诱导转账/欺诈',
'态度恶劣/人身攻击',
'开挂/作弊/战绩造假',
'线下违规/安全隐患',
'其他违规行为'
];
const reportForm = ref({
reason: '涉黄低俗言论',
description: ''
});
const openReportModal = () => {
reportForm.value = {
reason: '涉黄低俗言论',
description: ''
};
reportModalVisible.value = true;
};
const closeReportModal = () => {
reportModalVisible.value = false;
};
const submitReportTicket = async () => {
submittingReport.value = true;
uni.showLoading({ title: '正在提交举报...' });
try {
await submitReportApi({
reporterId: 1001,
targetType: 'EXPERT',
targetId: String(expert.value.id || '101'),
targetName: expert.value.nickname || '同城陪伴师',
reason: reportForm.value.reason,
description: reportForm.value.description.trim()
});
uni.hideLoading();
closeReportModal();
uni.showToast({ title: '举报已受理,平台将在24小时内核实处置', icon: 'success' });
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
} finally {
submittingReport.value = false;
}
};
</script>
<style lang="scss" scoped>
@@ -870,4 +979,165 @@ const goToOrder = () => {
margin: 0;
}
}
.review-modal-mask {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.review-modal-content {
width: 100%;
max-width: 450px;
background: #151D30;
border-top-left-radius: 36rpx;
border-top-right-radius: 36rpx;
padding: 36rpx 36rpx calc(36rpx + env(safe-area-inset-bottom));
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease-out;
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32rpx;
.modal-header-left {
display: flex;
flex-direction: column;
gap: 6rpx;
.modal-title {
font-size: 34rpx;
font-weight: 700;
color: #F8FAFC;
}
.modal-subtitle {
font-size: 24rpx;
color: #94A3B8;
}
}
.modal-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 36rpx;
height: 36rpx;
}
}
}
.tags-section {
margin-bottom: 24rpx;
.tags-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 16rpx;
display: block;
}
.tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.review-tag {
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 30rpx;
transition: all 0.2s;
&.active {
color: #FFFFFF;
background: rgba(239, 68, 68, 0.25);
border-color: #EF4444;
font-weight: 600;
}
}
}
}
.textarea-section {
margin-bottom: 32rpx;
position: relative;
.textarea-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 12rpx;
display: block;
}
.review-textarea {
width: 100%;
height: 160rpx;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
color: #F8FAFC;
}
.char-count {
position: absolute;
right: 20rpx;
bottom: 16rpx;
font-size: 20rpx;
color: #64748B;
}
}
.modal-actions {
display: flex;
gap: 20rpx;
.modal-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 600;
margin: 0;
&.cancel-btn {
background: rgba(255, 255, 255, 0.08);
color: #94A3B8;
}
&.submit-btn {
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
}
}
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style>

查看文件

@@ -156,6 +156,7 @@
<text class="sec-title">陪玩服务管理 ({{ expertServices.length }})</text>
</view>
<view class="service-actions">
<button class="btn-wd-records btn-active" @click="goToWithdrawPage">收益提现</button>
<button class="btn-wd-records btn-active" @click="openWithdrawRecords">提现记录</button>
</view>
</view>
@@ -505,6 +506,12 @@ const toggleServiceStatus = async (item) => {
const showWithdrawRecordsModal = ref(false);
const withdrawRecords = ref([]);
const goToWithdrawPage = () => {
uni.navigateTo({
url: '/pages/user/withdraw'
});
};
const openWithdrawRecords = async () => {
try {
const list = await getUserWithdrawals(1001);

查看文件

@@ -1038,10 +1038,19 @@ const submitReview = async () => {
.order-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16rpx;
padding-top: 16rpx;
border-top: 1px solid rgba(255, 255, 255, 0.05);
.btn-report-link {
font-size: 22rpx;
color: #64748B;
text-decoration: underline;
margin-right: auto;
padding: 8rpx 0;
}
.action-btn {
height: 60rpx;
line-height: 58rpx;
@@ -1395,6 +1404,72 @@ const submitReview = async () => {
}
}
.evidence-section {
margin-bottom: 24rpx;
.evidence-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
.evidence-item {
position: relative;
width: 130rpx;
height: 130rpx;
border-radius: 16rpx;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
.evidence-img {
width: 100%;
height: 100%;
}
.evidence-delete {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 36rpx;
height: 36rpx;
background: rgba(0, 0, 0, 0.65);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 20rpx;
height: 20rpx;
}
}
}
.evidence-upload-btn {
width: 130rpx;
height: 130rpx;
border-radius: 16rpx;
border: 1px dashed rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
.upload-icon-svg {
width: 36rpx;
height: 36rpx;
}
.upload-txt {
font-size: 20rpx;
color: #94A3B8;
}
}
}
}
.modal-actions {
display: flex;
gap: 20rpx;
@@ -1416,6 +1491,11 @@ const submitReview = async () => {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
&.danger {
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
}
}
}
}
@@ -1482,6 +1562,19 @@ const submitReview = async () => {
}
}
.evidence-section {
.evidence-upload-btn {
background: #F5F2EC;
border-color: #E7E5E4;
.upload-txt {
color: #78716C;
}
}
.evidence-item {
border-color: #E7E5E4;
}
}
.modal-actions {
.modal-btn {
&.cancel-btn {

查看文件

@@ -936,20 +936,9 @@ const minWithdrawAmount = computed(() => {
const openWithdrawModal = () => {
if (!ensureLoggedIn()) return;
const comm = Number(user.value.commission || 0);
if (comm <= 0) {
uni.showToast({ title: '暂无可提现佣金', icon: 'none' });
return;
}
withdrawForm.value = {
channel: 'WECHAT',
amount: String(comm),
alipayAccount: '',
bankCard: '',
bankName: '',
realName: user.value.nickname || ''
};
showWithdrawModal.value = true;
uni.navigateTo({
url: '/pages/user/withdraw'
});
};
const openWithdrawRecordsModal = async () => {

文件差异内容过多而无法显示 加载差异