feat(activity): 发单页统一组局与平台派单入口,接单大厅展示附近组局
- 移除附近活动页顶部单独的派单按钮(入口重复) - 「我要发单」新增发布类型切换:发起组局 / 平台派单,派单模式填预算与时长并内联拉起支付,支付成功进入待接单池 - 接单大厅新增「附近组局(可报名)」区块,展示招募中的组局并可进入详情报名 - 113 套件/738 用例全绿
这个提交包含在:
@@ -1,7 +1,27 @@
|
||||
<template>
|
||||
<view class="create-activity-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
|
||||
<!-- 发布类型切换:发起组局 / 平台派单 -->
|
||||
<view class="publish-mode-wrap">
|
||||
<view
|
||||
class="publish-mode-item btn-active"
|
||||
:class="{ active: publishMode === 'ACTIVITY' }"
|
||||
@click="publishMode = 'ACTIVITY'"
|
||||
>
|
||||
<text class="publish-mode-title">发起组局</text>
|
||||
<text class="publish-mode-desc">多人报名 · AA 或房主请客</text>
|
||||
</view>
|
||||
<view
|
||||
class="publish-mode-item btn-active"
|
||||
:class="{ active: publishMode === 'DISPATCH' }"
|
||||
@click="publishMode = 'DISPATCH'"
|
||||
>
|
||||
<text class="publish-mode-title">平台派单</text>
|
||||
<text class="publish-mode-desc">发布需求 · 附近大神抢单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 模式选择 Tab (AA制 / 项目制) -->
|
||||
<view class="mode-switch-wrap">
|
||||
<view class="mode-switch-wrap" v-if="publishMode === 'ACTIVITY'">
|
||||
<view
|
||||
class="mode-item btn-active"
|
||||
:class="{ active: form.payType === 'AA' }"
|
||||
@@ -130,7 +150,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 席位与费用配置 -->
|
||||
<view class="form-card">
|
||||
<view class="form-card" v-if="publishMode === 'ACTIVITY'">
|
||||
<view class="stepper-row">
|
||||
<view class="stepper-info">
|
||||
<text class="row-label">招募席位 (人)</text>
|
||||
@@ -246,7 +266,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 房主调价与追加金额设置 (随时灵活追加) -->
|
||||
<view class="form-card dynamic-price-card" v-if="form.payType === 'PROJECT'">
|
||||
<view class="form-card dynamic-price-card" v-if="form.payType === 'PROJECT' && publishMode === 'ACTIVITY'">
|
||||
<view class="card-header-switch">
|
||||
<view class="switch-title-box">
|
||||
<text class="form-title">灵活调价与追加金额设置</text>
|
||||
@@ -299,10 +319,40 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 平台派单:预算与时长 -->
|
||||
<view class="form-card" v-if="publishMode === 'DISPATCH'">
|
||||
<view class="input-row">
|
||||
<text class="row-label">预算 (元/小时)</text>
|
||||
<input
|
||||
class="row-input price-input"
|
||||
type="digit"
|
||||
v-model="dispatchBudget"
|
||||
placeholder="如:88"
|
||||
placeholder-class="price-ph"
|
||||
/>
|
||||
</view>
|
||||
<view class="stepper-row">
|
||||
<view class="stepper-info">
|
||||
<text class="row-label">服务时长</text>
|
||||
<text class="sub-label">1 - 8 小时</text>
|
||||
</view>
|
||||
<view class="stepper-box">
|
||||
<button class="step-btn btn-active" :disabled="dispatchHours <= 1" @click="changeDispatchHours(-1)">-</button>
|
||||
<text class="step-val">{{ dispatchHours }} 小时</text>
|
||||
<button class="step-btn btn-active" :disabled="dispatchHours >= 8" @click="changeDispatchHours(1)">+</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dispatch-total-row">
|
||||
<text class="row-label">预计支付</text>
|
||||
<text class="dispatch-total">¥{{ ((Number(dispatchBudget) || 0) * dispatchHours).toFixed(2) }}</text>
|
||||
</view>
|
||||
<text class="sub-label">支付后进入附近 5km 派单池,认证大神抢单;30 分钟无人接单自动全额退款。</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部发起操作栏 (适配安全区) -->
|
||||
<view class="bottom-bar">
|
||||
<button class="btn-submit btn-active" @click="handleSubmit">
|
||||
立即发起同城组局
|
||||
{{ publishMode === 'DISPATCH' ? '发布需求并支付' : '立即发起同城组局' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -342,7 +392,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { createActivity, getSafetyPlaces, getNavCategories, getPlatformConfigs } from '@/common/api.js';
|
||||
import { createActivity, createOrder, payOrder as payOrderApi, requestWechatPayment, normalizeUserId, getSafetyPlaces, getNavCategories, getPlatformConfigs } from '@/common/api.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { ensureLoginOrRedirect } from '@/common/role.js';
|
||||
|
||||
@@ -408,6 +458,15 @@ const stopPropagation = (e) => {
|
||||
};
|
||||
|
||||
const riskModalVisible = ref(false);
|
||||
|
||||
// 发布类型:ACTIVITY=组局(多人报名),DISPATCH=平台派单(订单进抢单池)
|
||||
const publishMode = ref('ACTIVITY');
|
||||
const dispatchBudget = ref(88);
|
||||
const dispatchHours = ref(2);
|
||||
|
||||
const changeDispatchHours = (delta) => {
|
||||
dispatchHours.value = Math.min(8, Math.max(1, Number(dispatchHours.value) + delta));
|
||||
};
|
||||
const recommendPublicZones = ref([
|
||||
'北京市朝阳区三里屯太古里 (AAA公开商圈)',
|
||||
'北京市朝阳区工体纯K量贩KTV (正规量贩)',
|
||||
@@ -565,6 +624,71 @@ const changeParticipants = (delta) => {
|
||||
);
|
||||
};
|
||||
|
||||
const submitDispatch = async (title, startTime, location) => {
|
||||
const budget = Number(dispatchBudget.value);
|
||||
if (!budget || budget <= 0) {
|
||||
uni.showToast({ title: '请输入有效的预算', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (budget > 2000) {
|
||||
uni.showToast({ title: '预算不能超过 ¥2000/小时', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const hours = Math.min(8, Math.max(1, Math.round(Number(dispatchHours.value) || 1)));
|
||||
const amount = Number((budget * hours).toFixed(2));
|
||||
const uid = normalizeUserId();
|
||||
const cachedUser = uni.getStorageSync('userInfo') || {};
|
||||
|
||||
uni.showLoading({ title: '正在创建需求单...' });
|
||||
let order = null;
|
||||
try {
|
||||
order = await createOrder({
|
||||
buyerId: uid,
|
||||
clientName: cachedUser.nickname || '同城用户',
|
||||
expertId: 0,
|
||||
companionName: '平台派单',
|
||||
expertAvatar: '',
|
||||
track: form.value.game,
|
||||
serviceTitle: title,
|
||||
hourlyRate: budget,
|
||||
durationHours: hours,
|
||||
amount,
|
||||
payType: 'WECHAT',
|
||||
location,
|
||||
latitude: form.value.latitude,
|
||||
longitude: form.value.longitude,
|
||||
expenseBurden: 'AA制各自付',
|
||||
remark: title,
|
||||
});
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '需求发布失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.hideLoading();
|
||||
|
||||
try {
|
||||
await requestWechatPayment({
|
||||
amount,
|
||||
title: `${form.value.game} · ${title}`,
|
||||
orderId: order.id,
|
||||
scene: 'ORDER',
|
||||
userId: uid,
|
||||
});
|
||||
await payOrderApi(order.id, { targetStatus: '待接单', payType: 'WECHAT', buyerId: uid });
|
||||
uni.showToast({ title: '需求已发布,已进入 5km 派单池', icon: 'success', duration: 2000 });
|
||||
} catch (payErr) {
|
||||
uni.showToast({
|
||||
title: (payErr && payErr.cancelled) ? '已取消支付,可在订单中心继续支付' : (payErr.message || '支付未完成,可在订单中心继续支付'),
|
||||
icon: 'none',
|
||||
duration: 2200,
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/order/list' });
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!ensureLoginOrRedirect('/pages/activity/create')) {
|
||||
return;
|
||||
@@ -604,6 +728,12 @@ const handleSubmit = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 平台派单:直接创建订单并拉起支付,支付成功后进入抢单池
|
||||
if (publishMode.value === 'DISPATCH') {
|
||||
await submitDispatch(trimmedTitle, trimmedTime, trimmedLocation);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 费用与预算必填及数值合法性校验
|
||||
const isProject = form.value.payType === 'PROJECT';
|
||||
if (!isProject) {
|
||||
@@ -695,6 +825,54 @@ const handleSubmit = async () => {
|
||||
padding: 24rpx 32rpx calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.publish-mode-wrap {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.publish-mode-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.28);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
|
||||
&.active {
|
||||
border-color: #10B981;
|
||||
background: rgba(16, 185, 129, 0.14);
|
||||
}
|
||||
|
||||
.publish-mode-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #F8FAFC);
|
||||
}
|
||||
|
||||
.publish-mode-desc {
|
||||
font-size: 21rpx;
|
||||
color: var(--text-muted, #94A3B8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dispatch-total-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 12rpx 0;
|
||||
|
||||
.dispatch-total {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #10B981;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-switch-wrap {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
@@ -22,15 +22,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 平台派单入口:不指定大神,发布需求后附近大神抢单 -->
|
||||
<view class="demand-entry btn-active" @click="goPublishDemand">
|
||||
<view class="demand-entry-left">
|
||||
<text class="demand-entry-title">发布陪玩需求(平台派单)</text>
|
||||
<text class="demand-entry-desc">不指定大神 · 5km 内认证大神抢单 · 30 分钟无人接单自动退款</text>
|
||||
</view>
|
||||
<text class="demand-entry-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 顶部模式切换选项卡 (矢量SVG模式图标) -->
|
||||
<view class="tab-header">
|
||||
<view
|
||||
@@ -189,7 +180,6 @@ import { getActivities, getNavCategories, normalizeUserId } from '@/common/api.j
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { ensureLoginOrRedirect } from '@/common/role.js';
|
||||
|
||||
const CURRENT_USER_ID = normalizeUserId();
|
||||
|
||||
@@ -379,11 +369,6 @@ const switchTab = (tab) => {
|
||||
currentTab.value = tab;
|
||||
};
|
||||
|
||||
const goPublishDemand = () => {
|
||||
if (!ensureLoginOrRedirect('/pages/activity/list')) return;
|
||||
uni.navigateTo({ url: '/pages/order/create' });
|
||||
};
|
||||
|
||||
const switchCategory = (cat) => {
|
||||
currentCategory.value = cat;
|
||||
};
|
||||
@@ -408,40 +393,6 @@ const createActivity = () => {
|
||||
padding: 0 32rpx calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.demand-entry {
|
||||
margin: 0 32rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(99, 102, 241, 0.35);
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.16), rgba(139, 92, 246, 0.10));
|
||||
|
||||
.demand-entry-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.demand-entry-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #F8FAFC);
|
||||
}
|
||||
|
||||
.demand-entry-desc {
|
||||
font-size: 21rpx;
|
||||
color: var(--text-muted, #94A3B8);
|
||||
}
|
||||
|
||||
.demand-entry-arrow {
|
||||
font-size: 36rpx;
|
||||
color: var(--color-brand, #6366F1);
|
||||
}
|
||||
}
|
||||
|
||||
.header-search {
|
||||
padding-top: var(--safe-header-gap, 36rpx);
|
||||
padding-bottom: 24rpx;
|
||||
|
||||
@@ -108,6 +108,26 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附近组局(招募中的组局,大神可报名参加) -->
|
||||
<view class="nearby-activities-section" v-if="nearbyActivities.length > 0">
|
||||
<view class="nearby-activity-head">
|
||||
<text class="nearby-activity-title">附近组局(可报名)</text>
|
||||
<text class="nearby-activity-more" @click="goActivityHall">查看全部 ›</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="act in nearbyActivities"
|
||||
:key="act.id"
|
||||
class="nearby-activity-card btn-active"
|
||||
@click="openActivity(act)"
|
||||
>
|
||||
<view class="na-row">
|
||||
<text class="na-title">{{ act.title }}</text>
|
||||
<text class="na-fee">{{ act.payType === 'PROJECT' ? '房主请客' : `人均 ¥${act.perPersonFee}` }}</text>
|
||||
</view>
|
||||
<text class="na-meta">{{ act.startTime || '时间待定' }} · {{ act.location || '地点待定' }} · {{ act.currentParticipants }}/{{ act.maxParticipants }} 人</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 派单需求全貌看板弹窗 (方案1) -->
|
||||
<view class="dispatch-detail-mask" v-if="showDetailModal" @click="closeOrderDetailModal">
|
||||
<view class="dispatch-detail-modal" @click.stop="stopPropagation">
|
||||
@@ -231,7 +251,7 @@ import AppTabBar from '@/components/CustomTabBar.vue';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { isExpertCertified } from '@/common/role.js';
|
||||
import { claimOrder, getOrdersPool, getExpertStats, updateCompanionProfile, getUserProfile } from '@/common/api.js';
|
||||
import { claimOrder, getOrdersPool, getExpertStats, updateCompanionProfile, getUserProfile, getActivities } from '@/common/api.js';
|
||||
import { getUserLocation } from '@/common/location.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
|
||||
@@ -276,6 +296,17 @@ const todayStats = ref({
|
||||
waitingVerify: 0
|
||||
});
|
||||
|
||||
const nearbyActivities = ref([]);
|
||||
|
||||
const openActivity = (act) => {
|
||||
if (!act || !act.id) return;
|
||||
uni.navigateTo({ url: `/pages/activity/detail?id=${act.id}` });
|
||||
};
|
||||
|
||||
const goActivityHall = () => {
|
||||
uni.switchTab({ url: '/pages/activity/list' });
|
||||
};
|
||||
|
||||
const onThemeChanged = (newTheme) => {
|
||||
currentThemeKey.value = newTheme;
|
||||
applyThemeToNativeUI(newTheme);
|
||||
@@ -344,6 +375,13 @@ const loadHallData = async () => {
|
||||
};
|
||||
});
|
||||
}
|
||||
}).catch(() => {}),
|
||||
|
||||
getActivities().then((list) => {
|
||||
const arr = Array.isArray(list) ? list : [];
|
||||
nearbyActivities.value = arr
|
||||
.filter((a) => a && (a.status === 'RECRUITING' || a.status === '招募中'))
|
||||
.slice(0, 5);
|
||||
}).catch(() => {})
|
||||
]);
|
||||
} catch (err) {
|
||||
@@ -1474,4 +1512,65 @@ const copyOrderNo = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 附近组局(招募中) */
|
||||
.nearby-activities-section {
|
||||
margin-top: 28rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #151D30;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
|
||||
.nearby-activity-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.nearby-activity-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.nearby-activity-more {
|
||||
font-size: 22rpx;
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.nearby-activity-card {
|
||||
padding: 18rpx 0;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.na-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.na-title {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #E2E8F0;
|
||||
}
|
||||
|
||||
.na-fee {
|
||||
font-size: 24rpx;
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.na-meta {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
在新工单中引用
屏蔽一个用户