文件
shanghaipeiwan/pwuniapp/pages/activity/create.vue
xcbyy 68e41ca962 feat(lbs): 真实距离与按距离派单落地
- 数据库:orders/activities 新增 latitude/longitude(含存量表 ALTER 迁移)

- 服务端:Haversine 计算 distanceKm/distanceText,派单池支持 lat/lng/radius 过滤并按距离升序;历史无坐标订单用腾讯地理编码兜底并回写缓存

- 新增 /api/lbs/reverse 逆地址解析(Key 仅服务端持有,登录 + 限流)

- 小程序:雷达/接单大厅改用真实定位与真实距离,删除全部写死演示距离;下单页新增地图选点,组局页保存选点经纬度

- 修复接单大厅点接单会新建订单的问题,改为真正 claimOrder 认领

- 新增 lbs-distance 护栏(110 套件/729 用例全绿)
2026-10-21 14:29:33 +08:00

1681 行
44 KiB
Vue

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

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

<template>
<view class="create-activity-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 模式选择 Tab (AA制 / 项目制) -->
<view class="mode-switch-wrap">
<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">
<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>
</view>
<!-- 基础信息录入 -->
<view class="form-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">
<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>
</view>
</view>
<!-- 席位与费用配置 -->
<view class="form-card">
<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="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'">
<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="bottom-bar">
<button class="btn-submit btn-active" @click="handleSubmit">
立即发起同城组局
</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">请选择以下平台认证的示范公开商圈:</text>
<view class="recommend-zones">
<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, getSafetyPlaces, getNavCategories, getPlatformConfigs } from '@/common/api.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
const games = ref(['美食陪吃', '同城陪唱', '下午茶陪喝', '同城微醺', '同城桌游', '热门电竞']);
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,
aaFee: '',
pricingMode: 'PER_PERSON',
perPersonFee: '',
totalFee: '',
allowDynamicPrice: true,
priceAdjustRule: 'UNIFIED_HIGH',
});
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);
const recommendPublicZones = ref([
'北京市朝阳区三里屯太古里 (AAA公开商圈)',
'北京市朝阳区工体纯K量贩KTV (正规量贩)',
'北京市朝阳区国贸CBD商业街 (阳光示范商圈)',
'北京市海淀区五道口购物中心 (高校商圈)'
]);
const presetTimeOptions = ref([
'今晚 19:30',
'今晚 20:30',
'明天 14:00',
'周六 18:30',
'周日 15: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 timePickerSlots = [
'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 timePickerRange = ref([timePickerDays, timePickerSlots]);
const timePickerIndex = ref([5, 19]); // 默认对应 周六 18:30
const onTimePickerChange = (e) => {
const val = (e && e.detail && Array.isArray(e.detail.value)) ? e.detail.value : [0, 0];
const dayIdx = Number(val[0]) || 0;
const slotIdx = Number(val[1]) || 0;
timePickerIndex.value = [dayIdx, slotIdx];
const dayStr = timePickerDays[dayIdx] || timePickerDays[0];
const slotStr = timePickerSlots[slotIdx] || timePickerSlots[0];
form.value.startTime = `${dayStr} ${slotStr}`;
};
const selectQuickTime = (timeItem) => {
form.value.startTime = timeItem;
const parts = String(timeItem).split(' ');
if (parts.length === 2) {
const dIdx = timePickerDays.indexOf(parts[0]);
const sIdx = timePickerSlots.indexOf(parts[1]);
timePickerIndex.value = [
dIdx >= 0 ? dIdx : 0,
sIdx >= 0 ? sIdx : 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);
// 若未授权或在H5/开发环境,平滑引导点选推荐公开商圈
if (recommendPublicZones.value.length > 0) {
selectZone(recommendPublicZones.value[0]);
uni.showToast({ title: '已填入推荐商圈', icon: 'none' });
}
}
});
// #endif
// #ifndef MP-WEIXIN || APP-PLUS
if (recommendPublicZones.value.length > 0) {
selectZone(recommendPublicZones.value[0]);
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]);
const loadLivePlaces = async () => {
try {
const [placesList, catList, configsRes] = await Promise.allSettled([
getSafetyPlaces(),
getNavCategories(),
getPlatformConfigs()
]);
if (placesList.status === 'fulfilled' && Array.isArray(placesList.value) && placesList.value.length > 0) {
recommendPublicZones.value = placesList.value
.filter((p) => p.status === '正常营业')
.map((p) => `${p.area} · ${p.name}`);
}
if (catList.status === 'fulfilled' && Array.isArray(catList.value) && catList.value.length > 0) {
const activeCats = catList.value
.filter((c) => c.status === 1 && c.name && c.name !== '全部活动')
.map((c) => c.name);
if (activeCats.length > 0) {
games.value = activeCats;
if (!games.value.includes(form.value.game)) {
form.value.game = games.value[0];
}
}
}
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);
}
};
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 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;
}
// 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 = Number(uni.getStorageSync('userId')) || Number(cachedUser.id) || 1001;
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,
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.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));
}
.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;
}
}
}
.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;
.picker-display-box {
display: flex;
align-items: center;
justify-content: space-between;
height: 52rpx;
.picker-val {
font-size: 26rpx;
font-weight: 600;
color: #F8FAFC;
font-variant-numeric: tabular-nums;
&.placeholder {
color: #475569;
font-weight: 400;
}
}
.picker-right-tag {
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;
}
.picker-arrow {
font-size: 24rpx;
font-weight: 700;
color: #818CF8;
line-height: 1;
}
}
}
}
.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;
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;
}
}
}
.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 {
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;
}
}
}
.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;
.picker-tag-text,
.picker-arrow {
color: #EA580C;
}
}
}
.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;
}
}
}
</style>