文件

2179 行
58 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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" v-if="publishMode === 'ACTIVITY'">
<view
class="mode-item btn-active"
:class="{ active: form.payType === 'AA' }"
@click="form.payType = 'AA'"
>
<image class="mode-svg" :src="ICONS.bolt" mode="aspectFit" />
<view class="mode-text-box">
<text class="mode-title">AA制拼桌</text>
<text class="mode-desc">报名人均平摊费用</text>
</view>
</view>
<view
class="mode-item btn-active"
:class="{ active: form.payType === 'PROJECT' }"
@click="form.payType = 'PROJECT'"
>
<image class="mode-svg" :src="ICONS.trophy" mode="aspectFit" />
<view class="mode-text-box">
<text class="mode-title">房主请客</text>
<text class="mode-desc">房主全额买单请客</text>
</view>
</view>
</view>
<!-- 品类选择 -->
<view class="form-card">
<view class="card-label">选择活动品类</view>
<view class="game-pills" v-if="games.length > 0">
<view
v-for="g in games"
:key="g"
class="game-pill btn-active"
:class="{ selected: form.game === g }"
@click="form.game = g"
>
{{ g }}
</view>
</view>
<text v-else class="config-empty-tip" @click="loadLivePlaces">
{{ configLoading ? '品类加载中…' : '暂无可选品类,点击重试' }}
</text>
</view>
<!-- 基础信息录入 -->
<view class="form-card basic-info-card">
<view class="input-row">
<text class="row-label">活动标题</text>
<input
class="row-input"
v-model="form.title"
placeholder="如:三里屯铜锅涮肉AA探店 / 工体纯K麦霸欢唱局"
placeholder-class="ph"
/>
</view>
<view class="input-row">
<text class="row-label">活动时段</text>
<picker
class="time-picker-flex"
mode="multiSelector"
:range="timePickerRange"
:value="timePickerIndex"
@change="onTimePickerChange"
>
<view class="picker-display-box btn-active">
<text class="picker-val" :class="{ placeholder: !form.startTime }">
{{ form.startTime || '请选择活动时段' }}
</text>
<view class="picker-right-tag">
<text class="picker-tag-text">选择时间</text>
<text class="picker-arrow">›</text>
</view>
</view>
</picker>
</view>
<!-- 快捷时段点选胶囊 (告别纯手打易错) -->
<view class="quick-chip-row">
<text class="chip-label">快捷时段:</text>
<view class="quick-chips">
<view
v-for="timeItem in presetTimeOptions"
:key="timeItem"
class="time-chip btn-active"
:class="{ selected: form.startTime === timeItem }"
@click="selectQuickTime(timeItem)"
>
{{ timeItem }}
</view>
</view>
</view>
<view class="location-block">
<view class="location-header-row">
<text class="row-label">地点/商圈</text>
<view class="btn-choose-loc btn-active" @click="handleChooseLocation">
<image class="loc-pin-svg" :src="ICONS.mapPin" mode="aspectFit" />
<text class="loc-btn-text">地图选点</text>
<text class="loc-btn-arrow">›</text>
</view>
</view>
<view class="location-input-box">
<textarea
class="location-textarea"
auto-height
v-model="form.location"
maxlength="120"
placeholder="请输入详细活动地点,或点击上方地图选点 / 下方合规商圈"
placeholder-class="ph"
/>
</view>
</view>
<!-- 合规示范商圈一键点选 (防风控与合规保障) -->
<view class="quick-chip-row">
<text class="chip-label">合规商圈:</text>
<view class="quick-chips" v-if="recommendPublicZones.length > 0">
<view
v-for="zoneItem in recommendPublicZones.slice(0, 4)"
:key="zoneItem"
class="zone-chip-item btn-active"
:class="{ selected: form.location === zoneItem }"
@click="selectZone(zoneItem)"
>
{{ zoneItem.split(' ')[0] }}
</view>
</view>
<text v-else class="config-empty-tip" @click="loadLivePlaces">
{{ configLoading ? '商圈加载中…' : '暂无合规商圈,请手动填写地点(点击重试)' }}
</text>
</view>
</view>
<!-- 席位与费用配置 -->
<view class="form-card" v-if="publishMode === 'ACTIVITY'">
<view class="stepper-row">
<view class="stepper-info">
<text class="row-label">招募席位 (人)</text>
<text class="sub-label">包含房主在内的组局总人数 ({{ minParticipants }}-{{ maxParticipants }}人)</text>
</view>
<view class="stepper-box">
<button class="step-btn btn-active" :disabled="form.maxParticipants <= minParticipants" @click="changeParticipants(-1)">-</button>
<text class="step-val">{{ form.maxParticipants }} 人</text>
<button class="step-btn btn-active" :disabled="form.maxParticipants >= maxParticipants" @click="changeParticipants(1)">+</button>
</view>
</view>
<!-- 凑局性别限制 -->
<view class="gender-limit-block">
<text class="row-label">凑局性别限制</text>
<view class="gender-pill-group">
<view
v-for="opt in genderOptions"
:key="opt.value"
class="gender-pill btn-active"
:class="{ active: form.genderRequirement === opt.value }"
@click="form.genderRequirement = opt.value"
>
{{ opt.label }}
</view>
</view>
</view>
<view class="input-row" v-if="form.payType === 'AA'">
<text class="row-label">人均分摊 (元/人)</text>
<input
class="row-input price-input"
type="digit"
v-model="form.aaFee"
placeholder="如:50"
placeholder-class="price-ph"
/>
</view>
<view v-else>
<!-- 房主请客 / 悬赏组队 出价模式切换 -->
<view class="pricing-mode-row">
<text class="row-label">出价方式</text>
<view class="pricing-mode-group">
<view
class="pricing-mode-btn"
:class="{ active: form.pricingMode === 'PER_PERSON' }"
@click="form.pricingMode = 'PER_PERSON'"
>
按人出价
</view>
<view
class="pricing-mode-btn"
:class="{ active: form.pricingMode === 'TOTAL' }"
@click="form.pricingMode = 'TOTAL'"
>
包场总预算
</view>
</view>
</view>
<view class="input-row" v-if="form.pricingMode === 'PER_PERSON'">
<text class="row-label">单人出价 (元/人)</text>
<input
class="row-input price-input"
type="digit"
v-model="form.perPersonFee"
placeholder="如:500"
placeholder-class="price-ph"
/>
</view>
<!-- 常用赏金快捷选项 (按人出价) -->
<view class="preset-price-row" v-if="form.pricingMode === 'PER_PERSON'">
<text class="preset-label">快捷赏金:</text>
<view class="preset-chips">
<view
v-for="amt in presetPerPersonFees"
:key="amt"
class="preset-chip btn-active"
:class="{ selected: Number(form.perPersonFee) === amt }"
@click="form.perPersonFee = amt"
>
¥{{ amt }}
</view>
</view>
</view>
<view class="input-row" v-else>
<text class="row-label">房主买单总预算 (元)</text>
<input
class="row-input price-input"
type="digit"
v-model="form.totalFee"
placeholder="如:2000"
placeholder-class="price-ph"
/>
</view>
<!-- 常用总预算快捷选项 (包场总预算) -->
<view class="preset-price-row" v-if="form.pricingMode === 'TOTAL'">
<text class="preset-label">快捷预算:</text>
<view class="preset-chips">
<view
v-for="amt in presetTotalFees"
:key="amt"
class="preset-chip btn-active"
:class="{ selected: Number(form.totalFee) === amt }"
@click="form.totalFee = amt"
>
¥{{ amt }}
</view>
</view>
</view>
</view>
<view class="calc-preview">
<text class="preview-label">预算测算:</text>
<text class="preview-val highlight">
{{
form.payType === 'AA'
? `总费用预估约 ¥${calcTotalFee} (人均 ¥${form.aaFee || 0})`
: form.pricingMode === 'PER_PERSON'
? `单人出价 ¥${form.perPersonFee || 0} × 招募${form.maxParticipants}人 = 预估总额 ¥${calcTotalFee} (发单后可随时追加金额)`
: `房主全额买单请客,总预算约 ¥${form.totalFee || 0} (发单后可随时追加预算)`
}}
</text>
</view>
</view>
<!-- 房主调价与追加金额设置 (随时灵活追加) -->
<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>
<text class="sub-label">组局进行中允许房主随时追加金额、加赏加急或修改单价</text>
</view>
<switch
:checked="form.allowDynamicPrice"
@change="form.allowDynamicPrice = $event.detail.value"
color="#6366F1"
/>
</view>
<view class="dynamic-config-body" v-if="form.allowDynamicPrice">
<text class="config-sec-title">追加金额/改价补差规则</text>
<view class="rule-cards">
<view
class="rule-card"
:class="{ active: form.priceAdjustRule === 'UNIFIED_HIGH' }"
@click="form.priceAdjustRule = 'UNIFIED_HIGH'"
>
<view class="rule-card-header">
<text class="rule-name">全员同享追加新高价</text>
<text class="rule-tag recommend">推荐</text>
</view>
<text class="rule-desc">
房主追加金额提价时(如从 500元/人 追加提高到 800元/人),已报名的成员自动同步补齐 300元 差额。先行者不吃亏,大幅提升组队热度。
</text>
</view>
<view
class="rule-card"
:class="{ active: form.priceAdjustRule === 'TIERED' }"
@click="form.priceAdjustRule = 'TIERED'"
>
<view class="rule-card-header">
<text class="rule-name">阶梯差价招募</text>
</view>
<text class="rule-desc">
追加金额仅对后续新加入的成员生效。已报名的老成员按其报名时的出价结算,互不影响。
</text>
</view>
</view>
<!-- 调价场景与降价提示说明 -->
<view class="scenario-notice">
<text class="notice-title">灵活追加金额说明:</text>
<text class="notice-item">• 随时追加金额:如原定 500元/人 招募4人,已有2人报名;房主在活动页可随时点击【追加金额】,选择按人追加(如每人追加 +¥300)或整局追加预算,系统自动完成老成员差额补偿与看板刷新。</text>
<text class="notice-item">• 降价调整保护:若房主手动调低单价,已报名的 2 人仍按原价受协议保护,新报名单人出价生效。</text>
</view>
</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>
<!-- 敏感商圈智能风控友好预检弹窗 (P1安全风控) -->
<view class="risk-modal-mask" v-if="riskModalVisible" @click="riskModalVisible = false">
<view class="risk-modal-box" @click.stop="stopPropagation">
<view class="risk-modal-header">
<image class="risk-shield-icon" :src="ICONS.shield" mode="aspectFit" />
<text class="risk-title">同城线下组局安全风控提示</text>
</view>
<view class="risk-body">
<text class="risk-desc">
平台严格保障人身安全,严禁将私人住宅、酒店客房、封闭公寓等非公开场所作为履约地点。
</text>
<text class="risk-sub-tip" v-if="recommendPublicZones.length > 0">请选择以下平台认证的示范公开商圈:</text>
<text class="risk-sub-tip" v-else>暂无可推荐的合规商圈,请手动填写公开商业场所。</text>
<view class="recommend-zones" v-if="recommendPublicZones.length > 0">
<view
v-for="zone in recommendPublicZones"
:key="zone"
class="zone-chip btn-active"
@click="applyRecommendZone(zone)"
>
<image class="zone-pin-icon" :src="ICONS.mapPin" mode="aspectFit" />
<text class="zone-text">{{ zone }}</text>
</view>
</view>
</view>
<view class="risk-footer">
<button class="risk-cancel-btn btn-active" @click="riskModalVisible = false">修改地点</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { ICONS } from '@/common/icons.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';
import { useShare } from '@/common/share.js';
useShare({
title: '同城发单组队 · 快速发起AA聚会与同行邀约',
timelineTitle: '同城组队发单 · 来和我一起组局吧',
path: '/pages/activity/create'
});
const games = ref([]);
const configLoading = ref(true);
const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
loadLivePlaces();
});
onShow(() => {
if (!ensureLoginOrRedirect('/pages/activity/create')) {
return;
}
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadLivePlaces();
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const form = ref({
payType: 'PROJECT',
game: '',
title: '',
startTime: '',
location: '',
latitude: null,
longitude: null,
maxParticipants: 4,
genderRequirement: 'ALL',
aaFee: '',
pricingMode: 'PER_PERSON',
perPersonFee: '',
totalFee: '',
allowDynamicPrice: true,
priceAdjustRule: 'UNIFIED_HIGH',
});
const genderOptions = [
{ label: '仅女生', value: 'FEMALE_ONLY' },
{ label: '仅男生', value: 'MALE_ONLY' },
{ label: '不限', value: 'ALL' }
];
const calcTotalFee = computed(() => {
if (form.value.payType === 'AA') {
return (Number(form.value.aaFee) || 0) * form.value.maxParticipants;
}
if (form.value.pricingMode === 'PER_PERSON') {
return (Number(form.value.perPersonFee) || 0) * form.value.maxParticipants;
}
return Number(form.value.totalFee) || 0;
});
const stopPropagation = (e) => {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
};
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([]);
const presetTimeOptions = ref([
'今晚 19:30 - 21:30',
'今晚 20:30 - 22:30',
'明天 14:00 - 17:00',
'周六 18:30 - 21:30',
'周日 15:00 - 18:00'
]);
const timePickerDays = (() => {
const baseDays = ['今晚', '今天', '明天', '后天', '周五', '周六', '周日'];
const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const now = new Date();
for (let i = 1; i <= 7; i++) {
const d = new Date(now.getTime() + i * 86400000);
const m = d.getMonth() + 1;
const day = d.getDate();
const w = weekNames[d.getDay()];
baseDays.push(`${m}月${day}日(${w})`);
}
return baseDays;
})();
const timePickerStartSlots = [
'09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
'12:00', '12:30', '13:00', '13:30', '14:00', '14:30',
'15:00', '15:30', '16:00', '16:30', '17:00', '17:30',
'18:00', '18:30', '19:00', '19:30', '20:00', '20:30',
'21:00', '21:30', '22:00', '22:30', '23:00', '23:30',
'00:00', '00:30', '01:00'
];
const timePickerEndSlots = [
'09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30',
'13:00', '13:30', '14:00', '14:30', '15:00', '15:30',
'16:00', '16:30', '17:00', '17:30', '18:00', '18:30',
'19:00', '19:30', '20:00', '20:30', '21:00', '21:30',
'22:00', '22:30', '23:00', '23:30', '00:00', '00:30',
'01:00', '01:30', '02:00', '02:30', '03:00', '03:30',
'04:00', '05:00'
];
const timePickerRange = ref([timePickerDays, timePickerStartSlots, timePickerEndSlots]);
const timePickerIndex = ref([5, 19, 24]); // 默认对应 周六 18:30 - 21:30
const toSlotMinutes = (timeStr) => {
const [h, m] = String(timeStr || '').split(':').map(Number);
return (h || 0) * 60 + (m || 0);
};
const onTimePickerChange = (e) => {
const val = (e && e.detail && Array.isArray(e.detail.value)) ? e.detail.value : [0, 0, 0];
const dayIdx = Math.max(0, Math.min(timePickerDays.length - 1, Number(val[0]) || 0));
const startIdx = Math.max(0, Math.min(timePickerStartSlots.length - 1, Number(val[1]) || 0));
let endIdx = Math.max(0, Math.min(timePickerEndSlots.length - 1, Number(val[2]) || 0));
const dayStr = timePickerDays[dayIdx] || timePickerDays[0];
const startStr = timePickerStartSlots[startIdx] || timePickerStartSlots[0];
let endStr = timePickerEndSlots[endIdx] || timePickerEndSlots[0];
const startMin = toSlotMinutes(startStr);
let endMin = toSlotMinutes(endStr);
// 凌晨 00:00 - 05:00 视为跨夜次日
if (endMin <= startMin && endMin <= 300) {
endMin += 1440;
}
// 结束时间须晚于开始时间,若不合理则智能纠偏为开始后 2 小时
if (endMin <= startMin) {
const targetMin = startMin + 120;
let bestIdx = -1;
for (let i = 0; i < timePickerEndSlots.length; i++) {
let m = toSlotMinutes(timePickerEndSlots[i]);
if (m <= startMin && m <= 300) m += 1440;
if (m >= targetMin) {
bestIdx = i;
break;
}
}
if (bestIdx >= 0) {
endIdx = bestIdx;
endStr = timePickerEndSlots[bestIdx];
} else {
endIdx = timePickerEndSlots.length - 1;
endStr = timePickerEndSlots[endIdx];
}
uni.showToast({ title: '已自动为您将活动结束时间设为 2 小时后', icon: 'none' });
}
timePickerIndex.value = [dayIdx, startIdx, endIdx];
form.value.startTime = `${dayStr} ${startStr} - ${endStr}`;
};
const selectQuickTime = (timeItem) => {
form.value.startTime = timeItem;
const match = String(timeItem).trim().match(/^(\S+)\s+([\d:]+)(?:\s*-\s*([\d:]+))?$/);
if (match) {
const dayStr = match[1];
const startStr = match[2];
const endStr = match[3];
const dIdx = timePickerDays.indexOf(dayStr);
const sIdx = timePickerStartSlots.indexOf(startStr);
let eIdx = endStr ? timePickerEndSlots.indexOf(endStr) : -1;
if (eIdx < 0) {
eIdx = Math.min(timePickerEndSlots.length - 1, Math.max(0, sIdx + 4));
}
timePickerIndex.value = [
dIdx >= 0 ? dIdx : 0,
sIdx >= 0 ? sIdx : 0,
eIdx >= 0 ? eIdx : 0
];
}
};
const selectZone = (zoneItem) => {
form.value.location = zoneItem;
form.value.latitude = null;
form.value.longitude = null;
};
const handleChooseLocation = () => {
// #ifdef MP-WEIXIN || APP-PLUS
uni.chooseLocation({
success: (res) => {
if (res && (res.name || res.address)) {
form.value.location = res.name ? `${res.address ? res.address + ' ' : ''}${res.name}` : res.address;
form.value.latitude = Number.isFinite(Number(res.latitude)) ? Number(res.latitude) : null;
form.value.longitude = Number.isFinite(Number(res.longitude)) ? Number(res.longitude) : null;
uni.showToast({ title: '已选取地点', icon: 'success' });
}
},
fail: (err) => {
console.warn('[ActivityCreate] 地图选点取消或未授权:', err);
uni.showToast({ title: '未选择地点,可手动填写', icon: 'none' });
}
});
// #endif
// #ifndef MP-WEIXIN || APP-PLUS
uni.showToast({ title: '当前环境不支持地图选点,请手动填写', icon: 'none' });
// #endif
};
const minParticipants = ref(2);
const maxParticipants = ref(10);
const presetPerPersonFees = ref([200, 300, 500, 800, 1000]);
const presetTotalFees = ref([500, 1000, 2000, 3000, 5000]);
let placesRequestSeq = 0;
const loadLivePlaces = async () => {
const seq = ++placesRequestSeq;
configLoading.value = true;
try {
const [placesList, catList, configsRes] = await Promise.allSettled([
getSafetyPlaces(),
getNavCategories(),
getPlatformConfigs()
]);
if (seq !== placesRequestSeq) return;
// 商圈完全以后台「线下商圈场所库」为准,不再使用本地兜底数据
if (placesList.status === 'fulfilled' && Array.isArray(placesList.value)) {
recommendPublicZones.value = placesList.value
.filter((p) => p.status === '正常营业')
.map((p) => `${p.area} · ${p.name}`);
} else {
recommendPublicZones.value = [];
}
// 活动品类完全以后台「金刚区导航品类」为准,不再使用本地兜底数据
if (catList.status === 'fulfilled' && Array.isArray(catList.value)) {
const activeCats = catList.value
.filter((c) => c.status === 1 && c.name && c.name !== '全部活动')
.map((c) => c.name);
games.value = activeCats;
if (activeCats.length === 0) {
form.value.game = '';
} else if (!activeCats.includes(form.value.game)) {
form.value.game = activeCats[0];
}
} else {
games.value = [];
form.value.game = '';
}
if (configsRes.status === 'fulfilled' && configsRes.value?.activity_rules) {
const actRules = configsRes.value.activity_rules;
if (actRules.minParticipants) minParticipants.value = Number(actRules.minParticipants);
if (actRules.maxParticipants) maxParticipants.value = Number(actRules.maxParticipants);
if (Array.isArray(actRules.presetPerPersonFees) && actRules.presetPerPersonFees.length > 0) {
presetPerPersonFees.value = actRules.presetPerPersonFees;
}
if (Array.isArray(actRules.presetTotalFees) && actRules.presetTotalFees.length > 0) {
presetTotalFees.value = actRules.presetTotalFees;
}
}
} catch (e) {
console.warn('[ActivityCreate] 加载合规场所与品类异常:', e);
if (seq === placesRequestSeq) {
recommendPublicZones.value = [];
games.value = [];
form.value.game = '';
}
} finally {
if (seq === placesRequestSeq) {
configLoading.value = false;
}
}
};
const applyRecommendZone = (zone) => {
form.value.location = zone;
riskModalVisible.value = false;
uni.showToast({
title: '已选定合规商圈,请继续发起',
icon: 'none',
duration: 1500
});
};
const changeParticipants = (delta) => {
form.value.maxParticipants = Math.max(
minParticipants.value,
Math.min(maxParticipants.value, form.value.maxParticipants + 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: startTime ? `${title}|期望:${startTime}` : 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;
}
// 1. 必填项完整校验(严禁未填写任何内容直接发布)
if (!form.value.game || !String(form.value.game).trim()) {
uni.showToast({ title: '请选择活动品类', icon: 'none' });
return;
}
const trimmedTitle = String(form.value.title || '').trim();
if (!trimmedTitle) {
uni.showToast({ title: '请填写活动标题', icon: 'none' });
return;
}
if (trimmedTitle.length < 2) {
uni.showToast({ title: '活动标题至少2个字', icon: 'none' });
return;
}
const trimmedTime = String(form.value.startTime || '').trim();
if (!trimmedTime) {
uni.showToast({ title: '请选择活动时段', icon: 'none' });
return;
}
const trimmedLocation = String(form.value.location || '').trim();
if (!trimmedLocation) {
uni.showToast({ title: '请填写或选择活动地点', icon: 'none' });
return;
}
// 2. P1 敏感商圈安全风控友好预检拦截
const sensitivePattern = /(酒店|客房|宾馆|民宿|旅馆|公寓|小区|住宅|私密|住宅区|私人房|卧室)/;
if (sensitivePattern.test(trimmedLocation)) {
riskModalVisible.value = true;
return;
}
// 平台派单:直接创建订单并拉起支付,支付成功后进入抢单池
if (publishMode.value === 'DISPATCH') {
await submitDispatch(trimmedTitle, trimmedTime, trimmedLocation);
return;
}
// 3. 费用与预算必填及数值合法性校验
const isProject = form.value.payType === 'PROJECT';
if (!isProject) {
const aaVal = Number(form.value.aaFee);
if (!form.value.aaFee || isNaN(aaVal) || aaVal <= 0) {
uni.showToast({ title: '请填写有效的人均分摊', icon: 'none' });
return;
}
} else if (form.value.pricingMode === 'PER_PERSON') {
const ppVal = Number(form.value.perPersonFee);
if (!form.value.perPersonFee || isNaN(ppVal) || ppVal <= 0) {
uni.showToast({ title: '请填写或选择单人出价', icon: 'none' });
return;
}
} else {
const totalVal = Number(form.value.totalFee);
if (!form.value.totalFee || isNaN(totalVal) || totalVal <= 0) {
uni.showToast({ title: '请填写或选择总预算', icon: 'none' });
return;
}
}
const perPerson = isProject && form.value.pricingMode === 'PER_PERSON'
? (Number(form.value.perPersonFee) || 0)
: (!isProject ? (Number(form.value.aaFee) || 0) : 0);
const cachedUser = uni.getStorageSync('userInfo') || uni.getStorageSync('USER_INFO') || {};
if (cachedUser.status === '已封禁' || cachedUser.status === '封禁') {
uni.showToast({ title: '账号已被封禁,无法发起组局', icon: 'none' });
return;
}
const currentUserId = normalizeUserId();
const newActivityPayload = {
creatorId: currentUserId,
game: form.value.game,
title: trimmedTitle,
payType: form.value.payType,
pricingMode: isProject ? form.value.pricingMode : 'AA',
perPersonFee: perPerson,
allowDynamicPrice: isProject ? form.value.allowDynamicPrice : false,
priceAdjustRule: form.value.priceAdjustRule,
totalFee: calcTotalFee.value,
aaFee: Number(form.value.aaFee) || 0,
maxParticipants: form.value.maxParticipants,
genderRequirement: form.value.genderRequirement || 'ALL',
startTime: trimmedTime,
location: trimmedLocation,
latitude: form.value.latitude,
longitude: form.value.longitude,
creatorNickname: cachedUser.nickname || '同城用户',
creatorAvatar: cachedUser.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
};
uni.showLoading({ title: '正在发布组局...' });
try {
await createActivity(newActivityPayload);
uni.hideLoading();
// 重置已填表单项
form.value.title = '';
form.value.startTime = '';
form.value.location = '';
form.value.genderRequirement = 'ALL';
form.value.aaFee = '';
form.value.perPersonFee = '';
form.value.totalFee = '';
uni.showToast({
title: '组局发起成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
uni.switchTab({
url: '/pages/activity/list'
});
}, 1200);
} catch (err) {
uni.hideLoading();
uni.showToast({
title: err.message || '发布异常,请重试',
icon: 'none'
});
}
};
</script>
<style lang="scss">
.create-activity-container {
min-height: 100vh;
background-color: #0A0F1D;
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;
margin-bottom: 24rpx;
.mode-item {
flex: 1;
background: #151D30;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20rpx;
padding: 20rpx;
display: flex;
align-items: center;
gap: 14rpx;
.mode-svg {
width: 40rpx;
height: 40rpx;
}
.mode-text-box {
display: flex;
flex-direction: column;
.mode-title {
font-size: 26rpx;
font-weight: 700;
color: #F8FAFC;
margin-bottom: 4rpx;
}
.mode-desc {
font-size: 18rpx;
color: #64748B;
}
}
&.active {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.18) 0%, rgba(139, 92, 246, 0.1) 100%);
border-color: #6366F1;
box-shadow: 0 0 16rpx rgba(99, 102, 241, 0.3);
.mode-title {
color: #818CF8;
}
}
}
}
.form-card {
background: #151D30;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
.card-label {
font-size: 26rpx;
font-weight: 700;
color: #F8FAFC;
margin-bottom: 20rpx;
}
.game-pills {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
.game-pill {
font-size: 24rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 10rpx 24rpx;
border-radius: 28rpx;
&.selected {
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
border-color: #6366F1;
font-weight: 600;
}
}
}
.config-empty-tip {
display: block;
padding: 12rpx 0;
font-size: 22rpx;
color: #94A3B8;
}
&.basic-info-card {
.input-row {
.row-label {
width: 140rpx;
min-width: 140rpx;
}
}
}
.input-row {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
&:last-child {
border-bottom: none;
}
.row-label {
width: 220rpx;
font-size: 26rpx;
color: #CBD5E1;
height: 48rpx;
line-height: 48rpx;
display: flex;
align-items: center;
flex-shrink: 0;
}
.row-input {
flex: 1;
font-size: 26rpx;
color: #F8FAFC;
height: 48rpx;
line-height: 48rpx;
box-sizing: border-box;
&.price-input {
font-size: 34rpx;
font-weight: 700;
color: #F8FAFC;
letter-spacing: 0.5px;
font-variant-numeric: tabular-nums;
}
}
.time-picker-flex {
flex: 1;
min-width: 0;
.picker-display-box {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
min-height: 52rpx;
.picker-val {
flex: 1;
min-width: 0;
font-size: 26rpx;
font-weight: 600;
color: #F8FAFC;
font-variant-numeric: tabular-nums;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.placeholder {
color: #475569;
font-weight: 400;
}
}
.picker-right-tag {
flex-shrink: 0;
white-space: nowrap;
display: flex;
align-items: center;
gap: 6rpx;
background: rgba(99, 102, 241, 0.14);
border: 1px solid rgba(99, 102, 241, 0.3);
padding: 4rpx 14rpx;
border-radius: 20rpx;
.picker-tag-text {
font-size: 20rpx;
font-weight: 600;
color: #818CF8;
white-space: nowrap;
}
.picker-arrow {
font-size: 24rpx;
font-weight: 700;
color: #818CF8;
line-height: 1;
white-space: nowrap;
}
}
}
}
.ph {
color: #475569;
}
.price-ph {
font-size: 26rpx;
font-weight: normal;
color: #475569;
}
}
.location-block {
padding: 20rpx 0 18rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.location-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14rpx;
.row-label {
font-size: 26rpx;
color: #CBD5E1;
font-weight: 500;
}
.btn-choose-loc {
display: flex;
align-items: center;
gap: 6rpx;
background: rgba(56, 189, 248, 0.14);
border: 1px solid rgba(56, 189, 248, 0.35);
padding: 4rpx 16rpx;
border-radius: 22rpx;
white-space: nowrap;
flex-shrink: 0;
.loc-pin-svg {
width: 24rpx;
height: 24rpx;
flex-shrink: 0;
}
.loc-btn-text {
font-size: 20rpx;
font-weight: 600;
color: #38BDF8;
line-height: 1.2;
}
.loc-btn-arrow {
font-size: 24rpx;
font-weight: 700;
color: #38BDF8;
line-height: 1;
}
}
}
.location-input-box {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16rpx;
padding: 16rpx 20rpx;
.location-textarea {
width: 100%;
min-height: 40rpx;
font-size: 26rpx;
font-weight: 600;
color: #F8FAFC;
line-height: 1.45;
box-sizing: border-box;
}
.ph {
color: #475569;
font-weight: 400;
}
}
}
.quick-chip-row {
display: flex;
align-items: center;
padding: 12rpx 0 16rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.chip-label {
font-size: 22rpx;
color: #64748B;
width: 140rpx;
flex-shrink: 0;
}
.quick-chips {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.time-chip, .zone-chip-item {
font-size: 22rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 6rpx 16rpx;
border-radius: 12rpx;
white-space: nowrap;
transition: all 0.15s;
&.selected {
color: #818CF8;
border-color: #6366F1;
background: rgba(99, 102, 241, 0.15);
font-weight: 600;
}
}
.zone-chip-item.selected {
color: #38BDF8;
border-color: #38BDF8;
background: rgba(56, 189, 248, 0.15);
}
}
}
.stepper-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.stepper-info {
display: flex;
flex-direction: column;
.row-label {
font-size: 26rpx;
color: #CBD5E1;
font-weight: 600;
}
.sub-label {
font-size: 20rpx;
color: #64748B;
margin-top: 4rpx;
}
}
.stepper-box {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.04);
border-radius: 16rpx;
padding: 6rpx;
.step-btn {
width: 68rpx;
height: 68rpx;
line-height: 64rpx;
padding: 0;
background: #1E2942;
color: #FFF;
font-size: 32rpx;
border-radius: 12rpx;
border: none;
margin: 0;
}
.step-val {
font-size: 28rpx;
color: #F8FAFC;
font-weight: 600;
margin: 0 20rpx;
font-variant-numeric: tabular-nums;
}
}
}
.gender-limit-block {
padding: 24rpx 0 20rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.row-label {
font-size: 26rpx;
color: #CBD5E1;
font-weight: 600;
margin-bottom: 18rpx;
display: block;
}
.gender-pill-group {
display: flex;
gap: 16rpx;
.gender-pill {
flex: 1;
height: 72rpx;
line-height: 72rpx;
text-align: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16rpx;
font-size: 26rpx;
color: #94A3B8;
transition: all 0.15s ease-out;
&.active {
background: rgba(99, 102, 241, 0.2);
border-color: #6366F1;
color: #818CF8;
font-weight: 700;
}
}
}
}
.calc-preview {
margin-top: 20rpx;
padding: 16rpx 20rpx;
background: rgba(99, 102, 241, 0.08);
border-radius: 14rpx;
display: flex;
align-items: center;
.preview-label {
font-size: 22rpx;
color: #94A3B8;
}
.preview-val {
font-size: 22rpx;
color: #10B981;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
}
.pricing-mode-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.pricing-mode-group {
display: flex;
gap: 12rpx;
.pricing-mode-btn {
font-size: 22rpx;
padding: 8rpx 22rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.05);
color: #94A3B8;
border: 1px solid rgba(255, 255, 255, 0.08);
&.active {
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
border-color: #6366F1;
font-weight: 600;
}
}
}
}
.preset-price-row {
display: flex;
align-items: center;
padding: 14rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
.preset-label {
font-size: 20rpx;
color: #94A3B8;
width: 140rpx;
}
.preset-chips {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.preset-chip {
font-size: 22rpx;
padding: 6rpx 18rpx;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #CBD5E1;
&.selected {
background: rgba(99, 102, 241, 0.2);
border-color: #6366F1;
color: #818CF8;
font-weight: 600;
}
}
}
}
&.dynamic-price-card {
.card-header-switch {
display: flex;
justify-content: space-between;
align-items: center;
.switch-title-box {
display: flex;
flex-direction: column;
.form-title {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
.sub-label {
font-size: 20rpx;
color: #64748B;
margin-top: 4rpx;
}
}
}
.dynamic-config-body {
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 1px solid rgba(255, 255, 255, 0.04);
.config-sec-title {
font-size: 22rpx;
font-weight: 600;
color: #94A3B8;
display: block;
margin-bottom: 14rpx;
}
.rule-cards {
display: flex;
flex-direction: column;
gap: 14rpx;
.rule-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16rpx;
padding: 18rpx 20rpx;
&.active {
background: rgba(99, 102, 241, 0.12);
border-color: #6366F1;
}
.rule-card-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
.rule-name {
font-size: 26rpx;
font-weight: 700;
color: #F8FAFC;
}
.rule-tag {
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
&.recommend {
background: rgba(16, 185, 129, 0.2);
color: #10B981;
}
}
}
.rule-desc {
font-size: 20rpx;
color: #94A3B8;
line-height: 1.45;
display: block;
}
}
}
.scenario-notice {
margin-top: 20rpx;
padding: 16rpx 20rpx;
background: rgba(99, 102, 241, 0.08);
border: 1px dashed rgba(99, 102, 241, 0.3);
border-radius: 14rpx;
.notice-title {
font-size: 22rpx;
font-weight: 700;
color: #818CF8;
display: block;
margin-bottom: 6rpx;
}
.notice-item {
font-size: 20rpx;
color: #94A3B8;
line-height: 1.5;
display: block;
}
}
}
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(120rpx + env(safe-area-inset-bottom));
padding: 0 32rpx env(safe-area-inset-bottom);
box-sizing: border-box;
background-color: rgba(21, 29, 48, 0.95);
backdrop-filter: blur(12px);
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
z-index: 100;
max-width: 450px;
margin: 0 auto;
.btn-submit {
flex: 1;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFF;
font-size: 28rpx;
font-weight: 700;
border-radius: 44rpx;
height: 84rpx;
line-height: 84rpx;
box-shadow: 0 4rpx 20rpx rgba(99, 102, 241, 0.4);
margin: 0;
}
}
.create-activity-container.theme-warm {
.form-card .config-empty-tip {
color: #78716C;
}
background-color: #FDFBF7;
.mode-switch-wrap .mode-item {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 4rpx 16rpx rgba(28, 25, 23, 0.04);
.mode-text-box .mode-title {
color: #1C1917;
}
.mode-text-box .mode-desc {
color: #78716C;
}
&.active {
background: rgba(234, 88, 12, 0.08);
border-color: #EA580C;
.mode-text-box .mode-title {
color: #EA580C;
}
}
}
.form-card {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 4rpx 20rpx rgba(28, 25, 23, 0.05);
.card-label {
color: #1C1917;
}
.game-pills .game-pill {
background: #F8F5F0;
border-color: #E7E5E4;
color: #78716C;
&.selected {
background: rgba(234, 88, 12, 0.1);
border-color: #EA580C;
color: #EA580C;
font-weight: 700;
}
}
.stepper-row {
border-bottom-color: #F5F2EC;
.stepper-info {
.row-label {
color: #1C1917;
}
.sub-label {
color: #78716C;
}
}
.stepper-box {
background: #F8F5F0;
border: 1px solid #E7E5E4;
.step-btn {
background: #FFFFFF;
color: #1C1917;
border: 1px solid #E7E5E4;
box-shadow: 0 2rpx 6rpx rgba(28, 25, 23, 0.04);
&:disabled {
color: #A8A29E;
background: #F5F2EC;
border-color: #E7E5E4;
}
}
.step-val {
color: #1C1917;
font-weight: 700;
}
}
}
.gender-limit-block {
border-bottom-color: #F5F2EC;
.row-label {
color: #1C1917;
}
.gender-pill-group {
.gender-pill {
background: #F8F5F0;
border-color: #E7E5E4;
color: #57534E;
&.active {
background: #FFF7ED;
border-color: #EA580C;
color: #EA580C;
font-weight: 700;
}
}
}
}
.form-title {
color: #1C1917;
}
.input-row {
border-bottom-color: #F5F2EC;
.row-label {
color: #1C1917;
}
.row-input {
color: #1C1917;
&.price-input {
color: #000000;
font-weight: 800;
}
}
.time-picker-flex .picker-display-box {
.picker-val {
color: #1C1917;
&.placeholder {
color: #A8A29E;
}
}
.picker-right-tag {
background: #FFF7ED;
border-color: #FDBA74;
flex-shrink: 0;
white-space: nowrap;
.picker-tag-text,
.picker-arrow {
color: #EA580C;
white-space: nowrap;
}
}
}
.ph,
.price-ph {
color: #A8A29E;
}
}
.location-block {
border-bottom-color: #F5F2EC;
.location-header-row {
.row-label {
color: #1C1917;
}
.btn-choose-loc {
background: #FFF7ED;
border-color: #FDBA74;
.loc-btn-text,
.loc-btn-arrow {
color: #EA580C;
}
}
}
.location-input-box {
background: #FAF8F5;
border-color: #E7E5E4;
.location-textarea {
color: #1C1917;
}
.ph {
color: #A8A29E;
}
}
}
.quick-chip-row {
border-bottom-color: #F5F2EC;
.chip-label {
color: #78716C;
}
.quick-chips {
.time-chip,
.zone-chip-item {
background: #F8F5F0;
border-color: #E7E5E4;
color: #57534E;
&.selected {
background: #FFF7ED;
border-color: #EA580C;
color: #EA580C;
font-weight: 700;
}
}
}
}
.calc-preview {
background: #F8F5F0;
.preview-label {
color: #78716C;
}
}
.pricing-mode-row {
border-bottom-color: #F5F2EC;
.pricing-mode-btn {
background: #F8F5F0;
color: #78716C;
border-color: #E7E5E4;
&.active {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
border-color: #EA580C;
}
}
}
.preset-price-row {
border-bottom-color: #F5F2EC;
.preset-label {
color: #78716C;
}
.preset-chips .preset-chip {
background: #F8F5F0;
border-color: #E7E5E4;
color: #78716C;
&.selected {
background: rgba(234, 88, 12, 0.1);
border-color: #EA580C;
color: #EA580C;
font-weight: 700;
}
}
}
&.dynamic-price-card {
.card-header-switch .switch-title-box .form-title {
color: #1C1917;
}
.dynamic-config-body {
border-top-color: #F5F2EC;
.config-sec-title {
color: #78716C;
}
.rule-cards .rule-card {
background: #FAFAF9;
border-color: #E7E5E4;
&.active {
background: rgba(234, 88, 12, 0.06);
border-color: #EA580C;
.rule-card-header .rule-name {
color: #EA580C;
}
}
.rule-card-header .rule-name {
color: #1C1917;
}
.rule-desc {
color: #78716C;
}
}
.scenario-notice {
background: #FFF7ED;
border-color: #FDBA74;
.notice-title {
color: #EA580C;
}
.notice-item {
color: #78716C;
}
}
}
}
}
.bottom-bar {
background-color: rgba(253, 251, 247, 0.95);
border-top-color: #F0ECE4;
.btn-submit {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 20rpx rgba(234, 88, 12, 0.35);
}
}
.risk-modal-box {
background: #FFFFFF;
border-color: rgba(239, 68, 68, 0.4);
.risk-modal-header .risk-title {
color: #DC2626;
}
.risk-body {
.risk-desc {
color: #475569;
}
.risk-sub-tip {
color: #64748B;
}
.recommend-zones .zone-chip {
background: rgba(234, 88, 12, 0.08);
border-color: rgba(234, 88, 12, 0.25);
.zone-text {
color: #EA580C;
}
}
}
.risk-footer .risk-cancel-btn {
background: #F1F5F9;
color: #334155;
border-color: #CBD5E1;
}
}
}
.risk-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
padding: 40rpx;
backdrop-filter: blur(6px);
}
.risk-modal-box {
width: 100%;
max-width: 620rpx;
background: #151D30;
border: 1px solid rgba(239, 68, 68, 0.35);
border-radius: 28rpx;
padding: 36rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.6);
.risk-modal-header {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 20rpx;
.risk-shield-icon {
width: 44rpx;
height: 44rpx;
}
.risk-title {
font-size: 30rpx;
font-weight: 700;
color: #F87171;
}
}
.risk-body {
margin-bottom: 30rpx;
.risk-desc {
display: block;
font-size: 24rpx;
color: #CBD5E1;
line-height: 1.6;
margin-bottom: 20rpx;
}
.risk-sub-tip {
display: block;
font-size: 22rpx;
color: #94A3B8;
margin-bottom: 16rpx;
font-weight: 600;
}
.recommend-zones {
display: flex;
flex-direction: column;
gap: 12rpx;
.zone-chip {
display: flex;
align-items: center;
gap: 12rpx;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 14rpx;
padding: 16rpx 20rpx;
.zone-pin-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.zone-text {
font-size: 24rpx;
color: #A5B4FC;
font-weight: 500;
}
}
}
}
.risk-footer {
display: flex;
justify-content: flex-end;
.risk-cancel-btn {
width: 100%;
height: 72rpx;
line-height: 72rpx;
background: #1E293B;
border: 1px solid rgba(255, 255, 255, 0.1);
color: #E2E8F0;
font-size: 26rpx;
border-radius: 16rpx;
}
}
}
/* ===== Premium visual layer: 发起组局 / 平台派单 ===== */
.create-activity-container {
background:
radial-gradient(110% 40% at 50% -6%, rgba(99, 102, 241, 0.14), transparent 62%),
var(--surface-0);
.publish-mode-item {
background: rgba(22, 31, 51, 0.78);
border: 1rpx solid var(--hairline);
border-radius: 24rpx;
transition: transform var(--motion-fast, 120ms) var(--ease-out, ease-out);
&:active {
transform: scale(0.985);
}
&.active {
background:
linear-gradient(145deg, rgba(99, 102, 241, 0.16), rgba(16, 185, 129, 0.08));
border-color: rgba(129, 140, 248, 0.4);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.06),
0 10rpx 24rpx rgba(0, 0, 0, 0.16);
}
}
.form-card {
background: linear-gradient(180deg, rgba(22, 31, 51, 0.84) 0%, rgba(15, 22, 38, 0.9) 100%);
border-color: var(--hairline);
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.14);
}
.game-pill,
.time-chip,
.zone-chip-item {
background: rgba(255, 255, 255, 0.035);
border-color: rgba(255, 255, 255, 0.07);
color: #A7B3CB;
border-radius: 999rpx;
&.selected {
background: rgba(99, 102, 241, 0.16);
border-color: rgba(129, 140, 248, 0.4);
color: #C7D2FE;
font-weight: 600;
}
}
.config-empty-tip {
color: #7C89A6;
}
.btn-submit {
border-radius: 999rpx;
background: linear-gradient(135deg, #F97316 0%, #EA580C 55%, #C2410C 100%);
box-shadow:
0 16rpx 34rpx rgba(234, 88, 12, 0.26),
inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
transition: transform var(--motion-fast, 120ms) var(--ease-out, ease-out);
}
.btn-submit:active {
transform: scale(0.985);
}
.bottom-bar {
background: rgba(10, 15, 29, 0.9);
backdrop-filter: blur(20px);
border-top: 1rpx solid rgba(255, 255, 255, 0.07);
}
}
.create-activity-container.theme-warm {
background:
radial-gradient(100% 38% at 50% -6%, rgba(234, 88, 12, 0.07), transparent 62%),
#FDFBF7;
.publish-mode-item {
background: #FFFFFF;
border-color: #F0ECE4;
&.active {
background: linear-gradient(145deg, #FFF7ED, #FFFFFF);
border-color: #FED7AA;
box-shadow: 0 10rpx 24rpx rgba(234, 88, 12, 0.06);
}
}
.form-card {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 12rpx 28rpx rgba(28, 25, 23, 0.05);
}
.game-pill,
.time-chip,
.zone-chip-item {
background: #F8F5F0;
border-color: #F0ECE4;
color: #78716C;
&.selected {
background: #FFF7ED;
border-color: #FED7AA;
color: #C2410C;
}
}
.config-empty-tip {
color: #A8A29E;
}
.bottom-bar {
background: rgba(255, 255, 255, 0.92);
border-top-color: #F0ECE4;
}
}
</style>