文件
shanghaipeiwan/pwuniapp/pages/activity/detail.vue

2697 行
73 KiB
Vue

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

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

<template>
<view class="detail-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 组局已解散状态警示横幅 -->
<view class="cancelled-banner" v-if="activity.status === 'CANCELLED'">
<image class="cancelled-icon" :src="ICONS.shield" mode="aspectFit" />
<view class="cancelled-text-box">
<text class="cancelled-title">本组局已由局长解散</text>
<text class="cancelled-desc">组局招募已终止,所有已报名搭子款项均已全额原路退还至钱包余额。</text>
</view>
</view>
<!-- 头部活动基本信息 -->
<view class="hero-card" :class="{ 'is-cancelled': activity.status === 'CANCELLED' }">
<view class="badge-row">
<text class="game-tag">{{ activity.game }}</text>
<text class="mode-tag" :class="activity.payType === 'AA' ? 'aa-mode' : 'project-mode'">
{{ activity.payType === 'AA' ? 'AA制平摊组局' : '房主请客组局' }}
</text>
<text class="status-tag" :class="activity.status || 'RECRUITING'">
{{ getStatusText(activity.status) }}
</text>
</view>
<view class="act-title">{{ activity.title }}</view>
<!-- 费用看板 -->
<view class="fee-board">
<view class="fee-col">
<text class="col-label">总预算支出</text>
<text class="col-val">¥{{ activity.totalFee }}</text>
</view>
<view class="fee-divider"></view>
<view class="fee-col">
<text class="col-label">{{ activity.payType === 'AA' ? '人均分摊' : (activity.pricingMode === 'PER_PERSON' || activity.perPersonFee ? '单人出价' : '房主请客') }}</text>
<text class="col-val highlight">
¥{{ activity.payType === 'AA' ? activity.aaFee : (activity.perPersonFee || activity.totalFee) }}{{ activity.payType !== 'AA' && (activity.pricingMode === 'PER_PERSON' || activity.perPersonFee) ? '/人' : '' }}
</text>
</view>
<view class="fee-divider"></view>
<view class="fee-col">
<text class="col-label">招募席位</text>
<text class="col-val">{{ activity.currentParticipants }}/{{ activity.maxParticipants }}人</text>
</view>
</view>
<!-- 房主追加金额与手动改价操作栏 (支持随时灵活追加赏金) -->
<view class="dynamic-price-banner" v-if="activity.payType === 'PROJECT' || activity.allowDynamicPrice">
<view class="dp-info">
<view class="dp-tag-row">
<text class="dp-badge">支持随时追加金额</text>
<text class="dp-rule-tag">
{{ activity.priceAdjustRule === 'TIERED' ? '阶梯差价制' : '全员同享追加新高价 (自动补差)' }}
</text>
</view>
<text class="dp-status-text">
当前单人出价 ¥{{ activity.perPersonFee || 0 }}/人 · 招募{{ activity.maxParticipants }}人 · 已报名{{ joinedCount }}人
</text>
</view>
<view class="dp-btn-group">
<button class="btn-adjust-price btn-append btn-active" @click="openAppendModal">
追加金额
</button>
<button class="btn-adjust-price btn-edit btn-active" @click="openAdjustModal">
改价
</button>
</view>
</view>
</view>
<!-- 房主与阵地信息 -->
<view class="section-card">
<view class="sec-header">
<text class="sec-title">组局阵地与发起人</text>
</view>
<view class="creator-box">
<image v-if="activity.creatorAvatar" class="creator-avatar btn-active" :src="activity.creatorAvatar" mode="aspectFill" @click="openHostProfile" />
<view v-else class="creator-avatar creator-avatar-empty btn-active" @click="openHostProfile">
<text class="creator-avatar-placeholder">?</text>
</view>
<view class="creator-info btn-active" @click="openHostProfile">
<view class="creator-name-row">
<text class="creator-name">{{ activity.creatorNickname }}</text>
<text class="host-badge">发起人</text>
</view>
<text class="creator-sub">同城组局召集人</text>
</view>
<button class="btn-chat btn-active" @click.stop="openSupportSheet">联系客服</button>
</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">活动时段</text>
<text class="info-val">{{ activity.startTime }}</text>
</view>
<view class="info-row">
<text class="info-label">活动地点</text>
<view class="copy-val" @click="copyLocation">
<text class="info-val">{{ activity.location }}</text>
<text class="copy-btn">复制</text>
</view>
</view>
<view class="info-row">
<text class="info-label">性别限制</text>
<text
class="info-val"
:class="{
'female-text': activity.genderRequirement === 'FEMALE_ONLY' || activity.genderRequirement === '仅女生',
'male-text': activity.genderRequirement === 'MALE_ONLY' || activity.genderRequirement === '仅男生'
}"
>
{{ formatGenderRequirement(activity.genderRequirement) }}
</text>
</view>
<view class="info-row">
<text class="info-label">核销机制</text>
<text class="info-val green-text">现场出示4位动态门票码核销签到</text>
</view>
</view>
</view>
<!-- 局长现场核销搭子签到专属卡片 -->
<view class="section-card host-verify-card" v-if="isHost && activity.status !== 'CANCELLED'">
<view class="sec-header">
<view class="title-with-badge">
<text class="sec-title">现场核销搭子签到</text>
<text class="host-pill">局长专属</text>
</view>
<text class="sec-sub">现场验证4位数字门票码</text>
</view>
<view class="verify-action-row">
<view class="verify-input-wrap">
<image class="verify-svg" :src="ICONS.ticket" mode="aspectFit" />
<input
class="verify-code-input tabular-num"
type="number"
maxlength="4"
v-model="verifyCodeInput"
placeholder="输入搭子4位核销码"
placeholder-class="ph"
/>
</view>
<button class="btn-verify-now btn-active" @click="handleHostVerify">
核销签到
</button>
</view>
<text class="verify-helper-tip">搭子在「我的门票」出示4位动态入局码,局长核验后标记为已核销并锁定履约。</text>
</view>
<!-- 已报名成员列表 -->
<view class="section-card">
<view class="sec-header">
<text class="sec-title">已加入成员 ({{ activity.participants ? activity.participants.length : 0 }})</text>
<text class="sec-sub">满员即成局</text>
</view>
<view class="member-list" v-if="activity.participants && activity.participants.length > 0">
<view v-for="(p, index) in activity.participants" :key="p.id" class="member-item" :class="{ 'my-member-item': Number(p.userId) === currentUserId }">
<image class="member-avatar" :src="p.avatar" mode="aspectFill" />
<view class="member-info">
<view class="member-top-line">
<text class="member-name">{{ p.nickname }}</text>
<text class="my-badge" v-if="Number(p.userId) === currentUserId">我的席位</text>
<text class="member-price-tag" v-if="p.priceAtJoin">
出价: ¥{{ p.priceAtJoin }}
</text>
</view>
<view class="member-sub-line">
<text class="member-seat">#{{ index + 1 }}号位</text>
<text class="member-comp-tag" v-if="p.diffCompensated && p.diffCompensated > 0">
已补差 +¥{{ p.diffCompensated }}
</text>
</view>
</view>
<view class="member-status-wrap">
<view class="checkin-badge" :class="{ checked: p.checkedIn }">
{{ p.checkedIn ? '已核销到场' : '待现场核销' }}
</view>
<text class="btn-micro-leave btn-active" v-if="Number(p.userId) === currentUserId && !p.checkedIn" @click.stop="handleLeave">
退席
</text>
</view>
</view>
</view>
<view v-else class="empty-members">
<text>暂无其他成员,快抢先加入同城聚会!</text>
</view>
</view>
<!-- 房主手动改价记录 -->
<view class="section-card history-card" v-if="activity.priceAdjustHistory && activity.priceAdjustHistory.length > 0">
<view class="sec-header">
<text class="sec-title">手动调价记录</text>
<text class="sec-sub">共 {{ activity.priceAdjustHistory.length }} 次调整</text>
</view>
<view class="history-list">
<view v-for="(h, idx) in activity.priceAdjustHistory" :key="idx" class="history-item">
<view class="history-left">
<text class="history-action" :class="h.diff >= 0 ? 'up' : 'down'">
{{ h.diff >= 0 ? '手动提价加急' : '手动下调出价' }}
</text>
<text class="history-time">{{ h.time }}</text>
</view>
<view class="history-right">
<text class="history-diff" :class="h.diff >= 0 ? 'diff-up' : 'diff-down'">
¥{{ h.oldPrice }} → ¥{{ h.newPrice }} ({{ h.diff >= 0 ? '+' : '' }}¥{{ h.diff }}/人)
</text>
<text class="history-note" v-if="h.note">{{ h.note }}</text>
</view>
</view>
</view>
</view>
<!-- 房主追加金额/改价弹窗 -->
<view class="adjust-modal-mask" v-if="adjustModalVisible" @click="closeAdjustModal">
<view class="adjust-modal-content" @click.stop="stopPropagation">
<view class="sheet-grabber"></view>
<view class="modal-header">
<view class="modal-title-box">
<text class="modal-title">{{ modalMode === 'APPEND' ? '房主追加金额' : '房主手动调整出价' }}</text>
<text class="modal-subtitle">
{{ modalMode === 'APPEND' ? '向本局追加单人赏金,加急吸引附近搭子加入' : '手动调整单人出价,支持提价加急或降价调整' }}
</text>
</view>
<view class="modal-close-btn btn-active" @click="closeAdjustModal">
<text class="close-icon">×</text>
</view>
</view>
<!-- 模式切换 Tab -->
<view class="modal-mode-tabs">
<view
class="mode-tab btn-active"
:class="{ active: modalMode === 'APPEND' }"
@click="switchModalMode('APPEND')"
>
追加赏金 (加急成局)
</view>
<view
class="mode-tab btn-active"
:class="{ active: modalMode === 'EXACT' }"
@click="switchModalMode('EXACT')"
>
直接设定出价
</view>
</view>
<view class="modal-body">
<!-- 当前出价看板 -->
<view class="modal-stat-box">
<view class="stat-item">
<text class="stat-label">当前单人出价</text>
<text class="stat-val highlight">¥{{ currentPerPerson }}<text class="unit">/人</text></text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">当前总预算</text>
<text class="stat-val">¥{{ activity.totalFee || (currentPerPerson * (activity.maxParticipants || 4)) }}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">当前报名人数</text>
<text class="stat-val">{{ joinedCount }}/{{ activity.maxParticipants || 4 }}<text class="unit">人</text></text>
</view>
</view>
<!-- 追加金额模式 -->
<view v-if="modalMode === 'APPEND'">
<view class="modal-input-group">
<text class="group-label">每人追加赏金 (元/人)</text>
<view class="input-wrap">
<text class="currency-prefix">+¥</text>
<input
class="modal-input"
type="number"
v-model="appendDeltaInput"
placeholder="请输入追加金额,如 100"
placeholder-class="ph"
/>
<text class="input-unit">元/人</text>
</view>
</view>
<!-- 快捷追加面额 (双行对称卡片) -->
<view class="quick-options">
<view
v-for="opt in appendQuickOptions"
:key="opt.delta"
class="quick-btn btn-active"
:class="{ highlight: Number(appendDeltaInput) === opt.delta }"
@click="appendDeltaInput = String(opt.delta)"
>
<text class="q-val">+¥{{ opt.delta }}</text>
<text class="q-sub">{{ opt.sub }}</text>
</view>
</view>
</view>
<!-- 直接设定出价模式 -->
<view v-else>
<view class="modal-input-group">
<text class="group-label">设定目标单人出价 (元/人)</text>
<view class="input-wrap">
<text class="currency-prefix">¥</text>
<input
class="modal-input"
type="number"
v-model="newPriceInput"
placeholder="请输入新单人出价"
placeholder-class="ph"
/>
<text class="input-unit">元/人</text>
</view>
</view>
<!-- 快速设定选项 (双行对称卡片) -->
<view class="quick-options">
<view
v-for="opt in exactQuickOptions"
:key="opt.price"
class="quick-btn btn-active"
:class="{ highlight: Number(newPriceInput) === opt.price, down: opt.isDown }"
@click="setQuickPrice(opt.price)"
>
<text class="q-val">¥{{ opt.price }}</text>
<text class="q-sub">{{ opt.sub }}</text>
</view>
</view>
</view>
<!-- 实时测算展示 -->
<view class="calc-change-box" v-if="isValidAction">
<view class="calc-top">
<text class="calc-title">{{ modalMode === 'APPEND' ? '追加金额效果测算' : '改价差额测算' }}</text>
<text class="calc-pill" :class="effectiveDiff >= 0 ? 'up' : 'down'">
{{ effectiveDiff >= 0 ? `单人提升 +¥${effectiveDiff}/人` : `单人下调 -¥${Math.abs(effectiveDiff)}/人` }}
</text>
</view>
<view class="calc-summary-grid">
<view class="calc-summary-cell">
<text class="cell-label">调整后单人出价</text>
<view class="cell-val-row">
<text class="cell-price">¥{{ targetPerPerson }}<text class="cell-unit">/人</text></text>
<text class="cell-origin">原 ¥{{ currentPerPerson }}</text>
</view>
</view>
<view class="calc-summary-divider"></view>
<view class="calc-summary-cell">
<text class="cell-label">调整后总预算约</text>
<view class="cell-val-row">
<text class="cell-price total">¥{{ estimatedTotal }}</text>
<text class="cell-delta" :class="effectiveDiff >= 0 ? 'up' : 'down'">
{{ estimatedTotal - (activity.totalFee || 0) >= 0 ? `+¥${estimatedTotal - (activity.totalFee || 0)}` : `-¥${Math.abs(estimatedTotal - (activity.totalFee || 0))}` }}
</text>
</view>
</view>
</view>
<view class="calc-notes">
<text class="note-row" v-if="effectiveDiff > 0 && activity.priceAdjustRule === 'UNIFIED_HIGH'">
规则说明:全员同享追加新高价。已报名的 {{ joinedCount }} 位老成员,每人自动补齐差额 ¥{{ effectiveDiff }}(老成员补差合计 ¥{{ effectiveDiff * joinedCount }})。
</text>
<text class="note-row" v-else-if="effectiveDiff > 0">
规则说明:阶梯差价招募。已报名的老成员按原出价结算,新加入席位按追加后单价 ¥{{ targetPerPerson }}/人 结算。
</text>
<text class="note-row" v-else-if="effectiveDiff < 0">
降价保护:已报名的 {{ joinedCount }} 位老成员出价保持原出价受保护,后续新席位按 ¥{{ targetPerPerson }}/人 执行。
</text>
<text class="note-row" v-else>
当前设定价格与原单人出价一致,未产生差额变动。
</text>
</view>
</view>
</view>
<view class="modal-footer">
<button class="footer-btn cancel-btn btn-active" @click="closeAdjustModal">取消</button>
<button class="footer-btn confirm-btn btn-active" @click="confirmAdjustPrice">
{{ modalMode === 'APPEND' ? '确认追加金额' : '确认修改出价' }}
</button>
</view>
</view>
</view>
<!-- 底部固定操作栏 -->
<view class="bottom-bar">
<!-- 1. 组局已解散状态 -->
<template v-if="activity.status === 'CANCELLED'">
<button class="btn-action-full btn-disabled">
该组局已解散 (款项已原路退还)
</button>
</template>
<!-- 2. 局长视角 (可追加出价 / 解散组局) -->
<template v-else-if="isHost">
<button class="btn-host-secondary btn-active" @click="openAppendModal">
追加赏金
</button>
<button class="btn-host-danger btn-active" @click="handleHostCancel">
解散组局
</button>
</template>
<!-- 3. 普通成员视角:已报名 (可查看门票 / 申请退席) -->
<template v-else-if="isJoined">
<view class="ticket-link btn-active" @click="goToMyTicket(myParticipantInfo ? myParticipantInfo.verifyCode : '')">
<image class="ticket-svg" :src="ICONS.ticket" mode="aspectFit" />
<text class="ticket-text">我的门票</text>
</view>
<button class="btn-leave btn-active" @click="handleLeave">
申请退席 (全额退款)
</button>
</template>
<!-- 4. 普通成员视角:未报名 -->
<template v-else>
<view class="ticket-link btn-active" @click="goToMyTicket('')">
<image class="ticket-svg" :src="ICONS.ticket" mode="aspectFit" />
<text class="ticket-text">我的门票</text>
</view>
<button class="btn-join btn-active" @click="handleJoin">
立即加入 (¥{{ activity.payType === 'AA' ? activity.aaFee : 0 }})
</button>
</template>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import {
getActivityDetail,
joinActivity,
adjustActivityPrice,
verifyActivityCheckin,
cancelActivity,
leaveActivity,
normalizeUserId
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { openSupportSheet } from '@/common/support.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
import { preloadSubscribeTemplates, requestSubscribeFor } from '@/common/subscribe.js';
import { useShare } from '@/common/share.js';
const currentUserId = computed(() => {
try {
const uid = uni.getStorageSync('userId');
return uid ? Number(uid) : 0;
} catch (_) {
return 0;
}
});
const currentActId = ref('1');
const verifyCodeInput = ref('');
const currentThemeKey = ref(getCurrentThemeKey());
const isHost = computed(() => {
if (!activity.value) return false;
return Number(activity.value.creatorId) === currentUserId.value;
});
const myParticipantInfo = computed(() => {
if (!activity.value || !activity.value.participants) return null;
const uid = currentUserId.value;
return activity.value.participants.find(p => Number(p.userId) === uid || Number(p.id) === uid);
});
const isJoined = computed(() => !!myParticipantInfo.value);
const getStatusText = (status) => {
if (status === 'CANCELLED') return '已解散';
if (status === 'COMPLETED') return '已完成';
if (status === 'FULL') return '已满员';
if (status === 'IN_PROGRESS' || status === 'RUNNING') return '进行中';
return '招募中';
};
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
preloadSubscribeTemplates();
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
if (currentActId.value) {
loadLiveDetail(currentActId.value);
}
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const stopPropagation = (e) => {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
};
const activity = ref({
id: '',
title: '',
game: '',
payType: 'AA',
totalFee: 0,
perPersonFee: 0,
maxParticipants: 0,
currentParticipants: 0,
genderRequirement: 'ALL',
startTime: '',
location: '',
creatorId: 0,
creatorNickname: '',
creatorAvatar: '',
participants: [],
priceAdjustHistory: []
});
const formatGenderRequirement = (req) => {
if (req === 'FEMALE_ONLY' || req === '仅女生') return '仅女生';
if (req === 'MALE_ONLY' || req === '仅男生') return '仅男生';
return '不限性别';
};
useShare(() => {
const act = activity.value;
const title = act && act.title ? `【组局】${act.title}` : '同城热门组局活动邀你加入';
const timelineTitle = act && act.title ? `【同城组局】${act.title}` : '同城热门组局活动邀你加入';
return {
title,
timelineTitle,
path: '/pages/activity/detail',
params: { id: currentActId.value },
imageUrl: (act && (act.coverImage || act.image)) || ''
};
});
const loadLiveDetail = async (id) => {
try {
const detail = await getActivityDetail(id);
if (detail) {
activity.value = detail;
} else {
uni.showToast({ title: '组局不存在或已下架', icon: 'none' });
}
} catch (err) {
console.warn('[ActivityDetail] 获取详情失败:', err);
uni.showToast({ title: err.message || '组局加载失败', icon: 'none' });
}
};
onLoad((query) => {
const targetId = (query && query.id) ? query.id : '1';
currentActId.value = targetId;
loadLiveDetail(targetId);
});
const adjustModalVisible = ref(false);
const modalMode = ref('APPEND'); // 'APPEND' | 'EXACT'
const appendDeltaInput = ref('300');
const newPriceInput = ref('800');
const openAppendModal = () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) return;
modalMode.value = 'APPEND';
appendDeltaInput.value = '300';
adjustModalVisible.value = true;
};
const openAdjustModal = () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) return;
modalMode.value = 'EXACT';
newPriceInput.value = String(currentPerPerson.value + 300);
adjustModalVisible.value = true;
};
const closeAdjustModal = () => {
adjustModalVisible.value = false;
};
const switchModalMode = (mode) => {
modalMode.value = mode;
if (mode === 'APPEND') {
appendDeltaInput.value = '300';
} else {
newPriceInput.value = String(targetPerPerson.value);
}
};
const setQuickPrice = (val) => {
newPriceInput.value = String(val);
};
const joinedCount = computed(() => {
if (activity.value && Array.isArray(activity.value.participants)) {
return activity.value.participants.length;
}
return 0;
});
const currentPerPerson = computed(() => {
return Number(activity.value.perPersonFee) || 0;
});
const appendQuickOptions = computed(() => {
const base = currentPerPerson.value;
return [
{ delta: 100, sub: `加至¥${base + 100}` },
{ delta: 200, sub: `加至¥${base + 200}` },
{ delta: 300, sub: `加至¥${base + 300}` },
{ delta: 500, sub: '顶格加急' }
];
});
const exactQuickOptions = computed(() => {
const base = currentPerPerson.value;
const downPrice = Math.max(10, Math.round((base * 0.8) / 10) * 10);
return [
{ price: base + 100, sub: `提至¥${base + 100}`, isDown: false },
{ price: base + 200, sub: '推荐加急', isDown: false },
{ price: base + 300, sub: '极速成局', isDown: false },
{ price: downPrice, sub: '手动降价', isDown: true }
];
});
const targetPerPerson = computed(() => {
if (modalMode.value === 'APPEND') {
const delta = Number(appendDeltaInput.value) || 0;
return currentPerPerson.value + delta;
}
return Number(newPriceInput.value) || currentPerPerson.value;
});
const effectiveDiff = computed(() => {
return targetPerPerson.value - currentPerPerson.value;
});
const isValidAction = computed(() => {
if (modalMode.value === 'APPEND') {
const d = Number(appendDeltaInput.value);
return !isNaN(d) && d > 0;
}
const p = Number(newPriceInput.value);
return !isNaN(p) && p > 0;
});
const estimatedTotal = computed(() => {
const maxP = activity.value.maxParticipants || 4;
return targetPerPerson.value * maxP;
});
const confirmAdjustPrice = async () => {
const newP = targetPerPerson.value;
if (!newP || newP <= 0) {
uni.showToast({ title: '请输入有效的金额', icon: 'none' });
return;
}
const oldP = currentPerPerson.value;
const diff = newP - oldP;
try {
uni.showLoading({ title: '正在同步调整出价...' });
await adjustActivityPrice(activity.value.id, newP);
uni.hideLoading();
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '调价失败,请稍后重试', icon: 'none' });
return;
}
activity.value.perPersonFee = newP;
activity.value.totalFee = newP * (activity.value.maxParticipants || 4);
if (!activity.value.priceAdjustHistory) {
activity.value.priceAdjustHistory = [];
}
let note = '';
if (diff > 0 && activity.value.priceAdjustRule === 'UNIFIED_HIGH') {
const count = activity.value.participants ? activity.value.participants.length : 0;
note = `已为在榜 ${count} 位成员各补差 ¥${diff}`;
if (activity.value.participants) {
activity.value.participants.forEach(p => {
p.priceAtJoin = newP;
p.diffCompensated = (p.diffCompensated || 0) + diff;
});
}
} else if (diff < 0) {
note = '下调出价,老成员价格不受影响';
}
activity.value.priceAdjustHistory.unshift({
time: '刚刚',
action: diff > 0 ? (modalMode.value === 'APPEND' ? '追加赏金' : '手动提价') : '手动下调',
oldPrice: oldP,
newPrice: newP,
diff,
note
});
adjustModalVisible.value = false;
uni.showToast({
title: diff > 0
? `已成功追加金额!单人出价升至¥${newP}/人`
: `已成功调整出价为¥${newP}/人`,
icon: 'success',
duration: 2000
});
};
const copyLocation = () => {
uni.setClipboardData({
data: activity.value.location,
success: () => {
uni.showToast({ title: '活动地点已复制', icon: 'success' });
}
});
};
const openHostProfile = () => {
const hostId = Number(activity.value.creatorId) || 0;
if (!hostId) {
uni.showToast({ title: '发起人信息不完整', icon: 'none' });
return;
}
const hostName = activity.value.creatorNickname || '同城用户';
const hostAvatar = activity.value.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150';
const hostSnapshot = {
id: hostId,
userId: hostId,
nickname: hostName,
avatar: hostAvatar,
rankTag: '发起人',
levelTag: '',
game: activity.value.game || '',
city: activity.value.location || '',
bio: activity.value.title ? `【${activity.value.title}】发起人` : '',
photos: hostAvatar ? [hostAvatar] : []
};
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', hostSnapshot);
uni.navigateTo({
url: `/pages/expert/detail?id=${encodeURIComponent(String(hostId))}&name=${encodeURIComponent(hostName)}&avatar=${encodeURIComponent(hostAvatar)}&tag=${encodeURIComponent('发起人')}&city=${encodeURIComponent(hostSnapshot.city)}`
});
};
const goToMyTicket = (code) => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const c = code || ((activity.value.participants || []).find(p => Number(p.userId) === Number(currentUserId.value)) || {}).verifyCode || '';
uni.navigateTo({
url: `/pages/activity/my-ticket?id=${activity.value.id}${c ? `&code=${encodeURIComponent(c)}` : ''}`
});
};
const handleHostVerify = async () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const code = (verifyCodeInput.value || '').trim();
if (!code || code.length !== 4) {
uni.showToast({ title: '请输入4位数字核销码', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '正在核销签到...' });
await verifyActivityCheckin(activity.value.id, code, currentUserId.value);
uni.hideLoading();
verifyCodeInput.value = '';
uni.showToast({ title: '核销成功!搭子已签到', icon: 'success' });
loadLiveDetail(activity.value.id);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '核销失败,核销码无效或已核销', icon: 'none' });
}
};
const handleHostCancel = () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const participants = activity.value.participants || [];
const count = participants.length;
const isAA = activity.value.payType === 'AA';
const totalRefund = isAA ? count * Number(activity.value.aaFee || 0) : 0;
let content = '当前暂无搭子报名,解散后将立即撤回本组局。是否确认解散?';
if (count > 0) {
if (totalRefund > 0) {
content = `当前已有 ${count} 位搭子报名,解散后将全额退款共 ¥${totalRefund} 至搭子钱包余额。此操作不可撤销,是否确认解散?`;
} else {
content = `当前已有 ${count} 位搭子报名,解散后席位将自动释放并通知搭子。此操作不可撤销,是否确认解散?`;
}
}
uni.showModal({
title: '确认解散组局?',
content,
confirmText: '确认解散',
confirmColor: '#EF4444',
cancelText: '再想想',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '正在解散并全额退款...' });
await cancelActivity(activity.value.id, currentUserId.value, '局长主动解散组局');
uni.hideLoading();
uni.showToast({ title: '组局已成功解散', icon: 'success' });
activity.value.status = 'CANCELLED';
loadLiveDetail(activity.value.id);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '解散失败,请重试', icon: 'none' });
}
}
}
});
};
const handleLeave = () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const isAA = activity.value.payType === 'AA';
const fee = isAA ? Number(activity.value.aaFee || 0) : 0;
uni.showModal({
title: '确认申请退席?',
content: `退席后席位将自动释放${fee > 0 ? `,已支付的分摊款 ¥${fee} 将全额原路退还至您的钱包余额` : ''}。是否确认退出?`,
confirmText: '确认退席',
confirmColor: '#EF4444',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '正在办理退席...' });
await leaveActivity(activity.value.id, currentUserId.value);
uni.hideLoading();
uni.showToast({ title: '已退席,款项已退回钱包', icon: 'success' });
loadLiveDetail(activity.value.id);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '退出失败,请重试', icon: 'none' });
}
}
}
});
};
const handleJoin = () => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const cachedUser = uni.getStorageSync('userInfo') || uni.getStorageSync('USER_INFO') || {};
const userGender = String(cachedUser.gender || '').trim();
const reqGender = activity.value.genderRequirement;
if ((reqGender === 'FEMALE_ONLY' || reqGender === '仅女生') && userGender === '男') {
uni.showToast({ title: '该组局仅限女生报名参与', icon: 'none' });
return;
}
if ((reqGender === 'MALE_ONLY' || reqGender === '仅男生') && userGender === '女') {
uni.showToast({ title: '该组局仅限男生报名参与', icon: 'none' });
return;
}
uni.showModal({
title: '确认报名',
content: `报名该【${activity.value.payType === 'AA' ? 'AA制' : '房主请客'}】组局,${activity.value.payType === 'AA' ? `需支付分摊款 ¥${activity.value.aaFee}` : '由房主全额买单请客'}。报名后自动生成4位入局核销码。`,
confirmText: '确认报名',
confirmColor: '#6366F1',
success: async (res) => {
if (res.confirm) {
// 报名前请求一次性订阅授权(组局报名通知)
requestSubscribeFor('activity');
uni.showLoading({ title: '正在报名生成核销码...' });
let code = '';
try {
const fee = activity.value.payType === 'AA' ? activity.value.aaFee : 0;
const activeUid = normalizeUserId();
const joinRes = await joinActivity(activity.value.id, {
userId: activeUid,
payAmount: fee,
});
code = joinRes && joinRes.verifyCode ? joinRes.verifyCode : '';
uni.hideLoading();
uni.showToast({ title: '报名成功', icon: 'success' });
setTimeout(() => {
goToMyTicket(code);
}, 1000);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '报名失败,请重试', icon: 'none' });
}
}
}
});
};
</script>
<style lang="scss">
.detail-container {
min-height: 100vh;
background-color: #0A0F1D;
padding: 24rpx 32rpx calc(160rpx + env(safe-area-inset-bottom));
}
.cancelled-banner {
display: flex;
align-items: center;
gap: 16rpx;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35);
border-radius: 20rpx;
padding: 20rpx 24rpx;
margin-bottom: 24rpx;
.cancelled-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
}
.cancelled-text-box {
display: flex;
flex-direction: column;
gap: 4rpx;
.cancelled-title {
font-size: 26rpx;
font-weight: 700;
color: #F87171;
}
.cancelled-desc {
font-size: 22rpx;
color: #FCA5A5;
line-height: 1.4;
}
}
}
.hero-card {
background: linear-gradient(135deg, #1E1B4B 0%, #151D30 100%);
border: 1px solid rgba(99, 102, 241, 0.25);
border-radius: 28rpx;
padding: 32rpx;
margin-bottom: 24rpx;
&.is-cancelled {
opacity: 0.8;
filter: grayscale(0.15);
}
.badge-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
.game-tag {
background: rgba(255, 255, 255, 0.1);
color: #F8FAFC;
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
}
.mode-tag {
font-size: 22rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-weight: 600;
&.aa-mode {
background: rgba(16, 185, 129, 0.2);
color: #10B981;
border: 1px solid rgba(16, 185, 129, 0.4);
}
&.project-mode {
background: rgba(245, 158, 11, 0.2);
color: #F59E0B;
border: 1px solid rgba(245, 158, 11, 0.4);
}
}
.status-tag {
margin-left: auto;
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 600;
&.RECRUITING {
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
}
&.FULL {
background: rgba(245, 158, 11, 0.2);
color: #F59E0B;
}
&.IN_PROGRESS,
&.RUNNING {
background: rgba(16, 185, 129, 0.2);
color: #10B981;
}
&.COMPLETED {
background: rgba(148, 163, 184, 0.2);
color: #94A3B8;
}
&.CANCELLED {
background: rgba(239, 68, 68, 0.2);
color: #F87171;
border: 1px solid rgba(239, 68, 68, 0.4);
}
}
}
.act-title {
font-size: 34rpx;
font-weight: 700;
color: #FFFFFF;
line-height: 1.4;
margin-bottom: 28rpx;
}
.fee-board {
display: flex;
justify-content: space-around;
align-items: center;
background: rgba(10, 15, 29, 0.6);
border-radius: 18rpx;
padding: 20rpx 0;
.fee-col {
display: flex;
flex-direction: column;
align-items: center;
.col-label {
font-size: 22rpx;
color: #94A3B8;
margin-bottom: 6rpx;
}
.col-val {
font-size: 30rpx;
font-weight: 700;
color: #F8FAFC;
&.highlight {
color: #10B981;
font-size: 34rpx;
}
}
}
.fee-divider {
width: 1px;
height: 40rpx;
background: rgba(255, 255, 255, 0.1);
}
}
.dynamic-price-banner {
margin-top: 24rpx;
padding: 22rpx 24rpx;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 18rpx;
.dp-info {
width: 100%;
margin-right: 0;
.dp-tag-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 10rpx;
flex-wrap: nowrap;
.dp-badge {
background: #6366F1;
color: #FFF;
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
line-height: 1.3;
}
.dp-rule-tag {
color: #A5B4FC;
font-size: 22rpx;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.dp-status-text {
font-size: 24rpx;
color: #F8FAFC;
font-weight: 500;
display: block;
line-height: 1.4;
}
}
.dp-btn-group {
display: flex;
gap: 16rpx;
align-items: center;
padding-top: 14rpx;
border-top: 1px dashed rgba(99, 102, 241, 0.22);
.btn-adjust-price {
flex: 1;
font-size: 24rpx;
font-weight: 600;
height: 64rpx;
line-height: 64rpx;
padding: 0 24rpx;
border-radius: 32rpx;
margin: 0;
white-space: nowrap;
text-align: center;
&.btn-append {
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
color: #FFF;
box-shadow: 0 4rpx 12rpx rgba(217, 119, 6, 0.35);
}
&.btn-edit {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #CBD5E1;
box-shadow: none;
}
}
}
}
}
.section-card {
background-color: #151D30;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
.sec-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.sec-title {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
.sec-sub {
font-size: 22rpx;
color: #64748B;
}
}
.creator-box {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border-radius: 16rpx;
padding: 16rpx;
margin-bottom: 24rpx;
.creator-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.creator-avatar-empty {
display: flex;
align-items: center;
justify-content: center;
background: rgba(148, 163, 184, 0.18);
border: 1px solid rgba(148, 163, 184, 0.3);
.creator-avatar-placeholder {
font-size: 30rpx;
color: #94A3B8;
font-weight: 700;
}
}
.creator-info {
flex: 1;
.creator-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 6rpx;
.creator-name {
font-size: 26rpx;
font-weight: 600;
color: #F8FAFC;
}
.host-badge {
background: rgba(99, 102, 241, 0.2);
color: #818CF8;
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
}
}
.creator-sub {
font-size: 20rpx;
color: #94A3B8;
}
}
.btn-chat {
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
color: #818CF8;
font-size: 22rpx;
padding: 0 24rpx;
height: 52rpx;
line-height: 50rpx;
border-radius: 26rpx;
margin: 0;
}
}
.info-list {
display: flex;
flex-direction: column;
gap: 16rpx;
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
.info-label {
color: #64748B;
}
.info-val {
color: #E2E8F0;
font-weight: 500;
&.green-text {
color: #10B981;
}
&.female-text {
color: #EC4899;
font-weight: 600;
}
&.male-text {
color: #38BDF8;
font-weight: 600;
}
}
.copy-val {
display: flex;
align-items: center;
gap: 12rpx;
.copy-btn {
color: #6366F1;
font-size: 22rpx;
text-decoration: underline;
}
}
}
}
.member-list {
display: flex;
flex-direction: column;
gap: 16rpx;
.member-item {
display: flex;
align-items: center;
padding: 12rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
&:last-child {
border-bottom: none;
}
.member-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
margin-right: 16rpx;
}
.member-info {
flex: 1;
.member-top-line {
display: flex;
align-items: center;
gap: 12rpx;
.member-name {
font-size: 24rpx;
font-weight: 600;
color: #F8FAFC;
}
.member-price-tag {
font-size: 18rpx;
color: #F59E0B;
background: rgba(245, 158, 11, 0.15);
padding: 2rpx 8rpx;
border-radius: 6rpx;
}
}
.member-sub-line {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 4rpx;
.member-seat {
font-size: 20rpx;
color: #64748B;
}
.member-comp-tag {
font-size: 18rpx;
color: #10B981;
background: rgba(16, 185, 129, 0.15);
padding: 2rpx 8rpx;
border-radius: 6rpx;
}
}
}
&.my-member-item {
background: rgba(99, 102, 241, 0.08);
border-radius: 12rpx;
padding: 12rpx 14rpx;
}
.my-badge {
font-size: 18rpx;
color: #6366F1;
background: rgba(99, 102, 241, 0.18);
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-weight: 600;
border: 1px solid rgba(99, 102, 241, 0.35);
}
.member-status-wrap {
display: flex;
align-items: center;
gap: 12rpx;
.btn-micro-leave {
font-size: 20rpx;
color: #EF4444;
background: rgba(239, 68, 68, 0.12);
border: 1px solid rgba(239, 68, 68, 0.3);
padding: 4rpx 16rpx;
border-radius: 10rpx;
}
}
.checkin-badge {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 12rpx;
background: rgba(100, 116, 139, 0.2);
color: #94A3B8;
&.checked {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
}
}
}
&.host-verify-card {
border: 1px solid rgba(99, 102, 241, 0.35);
background: linear-gradient(135deg, rgba(30, 27, 75, 0.6) 0%, rgba(21, 29, 48, 0.95) 100%);
.title-with-badge {
display: flex;
align-items: center;
gap: 12rpx;
.host-pill {
font-size: 18rpx;
background: rgba(245, 158, 11, 0.18);
color: #F59E0B;
border: 1px solid rgba(245, 158, 11, 0.4);
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-weight: 600;
}
}
.verify-action-row {
display: flex;
align-items: center;
gap: 16rpx;
margin: 20rpx 0 14rpx;
.verify-input-wrap {
flex: 1;
display: flex;
align-items: center;
background: rgba(10, 15, 29, 0.7);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 16rpx;
padding: 0 20rpx;
height: 76rpx;
.verify-svg {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.verify-code-input {
flex: 1;
font-size: 28rpx;
font-weight: 700;
color: #FFFFFF;
letter-spacing: 4rpx;
}
}
.btn-verify-now {
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFFFFF;
font-size: 26rpx;
font-weight: 700;
padding: 0 28rpx;
height: 76rpx;
line-height: 76rpx;
border-radius: 16rpx;
margin: 0;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.35);
white-space: nowrap;
}
}
.verify-helper-tip {
font-size: 20rpx;
color: #94A3B8;
line-height: 1.4;
}
}
&.history-card {
.history-list {
display: flex;
flex-direction: column;
gap: 14rpx;
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 14rpx 18rpx;
.history-left {
display: flex;
flex-direction: column;
.history-action {
font-size: 22rpx;
font-weight: 700;
&.up {
color: #EF4444;
}
&.down {
color: #10B981;
}
}
.history-time {
font-size: 18rpx;
color: #64748B;
margin-top: 2rpx;
}
}
.history-right {
display: flex;
flex-direction: column;
align-items: flex-end;
.history-diff {
font-size: 22rpx;
font-weight: 600;
&.diff-up {
color: #F8FAFC;
}
&.diff-down {
color: #94A3B8;
}
}
.history-note {
font-size: 18rpx;
color: #10B981;
margin-top: 2rpx;
}
}
}
}
}
.empty-members {
text-align: center;
padding: 30rpx 0;
color: #64748B;
font-size: 24rpx;
}
}
.adjust-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(8px);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
.adjust-modal-content {
width: 100%;
max-width: 450px;
background: #151D30;
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 20rpx 32rpx calc(36rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
.sheet-grabber {
width: 68rpx;
height: 8rpx;
border-radius: 4rpx;
background: rgba(255, 255, 255, 0.16);
margin: 0 auto 20rpx;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24rpx;
.modal-title-box {
flex: 1;
padding-right: 16rpx;
.modal-title {
font-size: 34rpx;
font-weight: 800;
color: #F8FAFC;
display: block;
letter-spacing: 0.5rpx;
}
.modal-subtitle {
font-size: 22rpx;
color: #94A3B8;
margin-top: 6rpx;
display: block;
line-height: 1.4;
}
}
.modal-close-btn {
width: 56rpx;
height: 56rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.close-icon {
font-size: 36rpx;
line-height: 1;
color: #94A3B8;
font-weight: 500;
}
}
}
.modal-mode-tabs {
display: flex;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 18rpx;
padding: 6rpx;
margin-bottom: 24rpx;
gap: 8rpx;
.mode-tab {
flex: 1;
text-align: center;
font-size: 24rpx;
font-weight: 700;
color: #94A3B8;
padding: 15rpx 0;
border-radius: 14rpx;
transition: all 0.2s ease;
&.active {
background: #6366F1;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.35);
}
}
}
.modal-body {
.modal-stat-box {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20rpx;
padding: 20rpx 12rpx;
margin-bottom: 24rpx;
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.stat-label {
font-size: 21rpx;
color: #94A3B8;
margin-bottom: 6rpx;
}
.stat-val {
font-size: 30rpx;
font-weight: 800;
color: #F8FAFC;
font-variant-numeric: tabular-nums;
&.highlight {
color: #6366F1;
}
.unit {
font-size: 20rpx;
font-weight: 500;
color: #94A3B8;
margin-left: 2rpx;
}
}
}
.stat-divider {
width: 1px;
height: 44rpx;
background: rgba(255, 255, 255, 0.1);
}
}
.modal-input-group {
margin-bottom: 20rpx;
.group-label {
font-size: 25rpx;
font-weight: 700;
color: #CBD5E1;
display: block;
margin-bottom: 12rpx;
}
.input-wrap {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.05);
border: 2rpx solid rgba(99, 102, 241, 0.45);
border-radius: 18rpx;
padding: 0 24rpx;
height: 88rpx;
.currency-prefix {
font-size: 34rpx;
font-weight: 800;
color: #6366F1;
margin-right: 12rpx;
}
.modal-input {
flex: 1;
font-size: 36rpx;
font-weight: 800;
color: #F8FAFC;
font-variant-numeric: tabular-nums;
}
.input-unit {
font-size: 22rpx;
font-weight: 600;
color: #94A3B8;
margin-left: 12rpx;
}
.ph {
color: #475569;
font-size: 26rpx;
font-weight: normal;
}
}
}
.quick-options {
display: flex;
gap: 14rpx;
margin-bottom: 24rpx;
.quick-btn {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
padding: 14rpx 6rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16rpx;
color: #CBD5E1;
transition: all 0.18s ease;
.q-val {
font-size: 25rpx;
font-weight: 800;
line-height: 1.2;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.q-sub {
font-size: 19rpx;
font-weight: 500;
line-height: 1.2;
opacity: 0.78;
white-space: nowrap;
}
&.highlight {
background: rgba(99, 102, 241, 0.16);
border-color: #6366F1;
color: #818CF8;
.q-sub {
opacity: 0.95;
}
}
&.down {
color: #94A3B8;
}
}
}
.calc-change-box {
background: rgba(99, 102, 241, 0.08);
border: 1px dashed rgba(99, 102, 241, 0.32);
border-radius: 20rpx;
padding: 20rpx 22rpx;
margin-bottom: 28rpx;
.calc-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.calc-title {
font-size: 25rpx;
font-weight: 700;
color: #818CF8;
}
.calc-pill {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
font-weight: 700;
&.up {
background: rgba(239, 68, 68, 0.18);
color: #F87171;
}
&.down {
background: rgba(16, 185, 129, 0.18);
color: #10B981;
}
}
}
.calc-summary-grid {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14rpx;
padding: 14rpx 18rpx;
margin-bottom: 14rpx;
.calc-summary-cell {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
.cell-label {
font-size: 20rpx;
color: #94A3B8;
}
.cell-val-row {
display: flex;
align-items: baseline;
gap: 10rpx;
.cell-price {
font-size: 28rpx;
font-weight: 800;
color: #F8FAFC;
font-variant-numeric: tabular-nums;
&.total {
color: #10B981;
}
.cell-unit {
font-size: 19rpx;
font-weight: 500;
color: #94A3B8;
margin-left: 2rpx;
}
}
.cell-origin {
font-size: 19rpx;
color: #64748B;
text-decoration: line-through;
}
.cell-delta {
font-size: 19rpx;
font-weight: 700;
padding: 1rpx 8rpx;
border-radius: 6rpx;
&.up {
color: #10B981;
background: rgba(16, 185, 129, 0.12);
}
&.down {
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
}
}
}
}
.calc-summary-divider {
width: 1px;
height: 44rpx;
background: rgba(255, 255, 255, 0.08);
margin: 0 18rpx;
}
}
.calc-notes {
display: flex;
flex-direction: column;
gap: 6rpx;
.note-row {
font-size: 21rpx;
color: #94A3B8;
line-height: 1.5;
}
}
}
}
.modal-footer {
display: flex;
gap: 20rpx;
.footer-btn {
height: 84rpx;
line-height: 84rpx;
border-radius: 42rpx;
font-size: 28rpx;
font-weight: 700;
margin: 0;
&::after {
border: none;
}
&.cancel-btn {
flex: 0.72;
background: rgba(255, 255, 255, 0.06);
color: #94A3B8;
}
&.confirm-btn {
flex: 1.28;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFF;
box-shadow: 0 6rpx 18rpx rgba(99, 102, 241, 0.35);
}
}
}
}
}
.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;
gap: 24rpx;
z-index: 100;
max-width: 450px;
margin: 0 auto;
.ticket-link {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 120rpx;
.ticket-svg {
width: 38rpx;
height: 38rpx;
}
.ticket-text {
font-size: 20rpx;
color: #94A3B8;
margin-top: 4rpx;
}
}
.btn-join {
flex: 1;
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
border-radius: 44rpx;
height: 80rpx;
line-height: 80rpx;
box-shadow: 0 4rpx 20rpx rgba(16, 185, 129, 0.35);
margin: 0;
font-variant-numeric: tabular-nums;
}
.btn-host-secondary {
flex: 1;
background: rgba(99, 102, 241, 0.15);
color: #818CF8;
border: 1px solid rgba(99, 102, 241, 0.4);
font-size: 28rpx;
font-weight: 700;
border-radius: 44rpx;
height: 80rpx;
line-height: 80rpx;
margin: 0;
}
.btn-host-danger {
flex: 1;
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
border-radius: 44rpx;
height: 80rpx;
line-height: 80rpx;
margin: 0;
box-shadow: 0 4rpx 20rpx rgba(239, 68, 68, 0.4);
}
.btn-leave {
flex: 1;
background: rgba(239, 68, 68, 0.15);
color: #F87171;
border: 1px solid rgba(239, 68, 68, 0.4);
font-size: 28rpx;
font-weight: 700;
border-radius: 44rpx;
height: 80rpx;
line-height: 80rpx;
margin: 0;
}
.btn-action-full {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 44rpx;
font-size: 28rpx;
font-weight: 600;
margin: 0;
&.btn-disabled {
background: rgba(255, 255, 255, 0.08);
color: #94A3B8;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
}
.detail-container.theme-warm {
background-color: #FDFBF7;
.cancelled-banner {
background: #FEF2F2;
border-color: #FECACA;
.cancelled-text-box {
.cancelled-title {
color: #DC2626;
}
.cancelled-desc {
color: #EF4444;
}
}
}
.hero-card {
background: linear-gradient(135deg, #FFF6EE 0%, #FFFFFF 100%);
border-color: #F0ECE4;
box-shadow: 0 6rpx 24rpx rgba(28, 25, 23, 0.05);
.badge-row {
.game-tag {
background: #FFEDD5;
color: #EA580C;
border: 1px solid #FED7AA;
font-weight: 600;
}
.mode-tag {
&.aa-mode {
background: rgba(16, 185, 129, 0.12);
color: #059669;
border-color: rgba(16, 185, 129, 0.35);
}
&.project-mode {
background: #FEF3C7;
color: #D97706;
border-color: #FDE68A;
}
}
.status-tag {
&.RECRUITING {
background: #FFF7ED;
color: #EA580C;
border: 1px solid #FDBA74;
}
&.COMPLETED {
background: #F5F5F4;
color: #78716C;
}
}
}
.act-title {
color: #1C1917;
}
.fee-board {
background: #F8F5F0;
border: 1px solid #F0ECE4;
.fee-col {
.col-label {
color: #78716C;
}
.col-val {
color: #1C1917;
&.highlight {
color: #EA580C;
}
}
}
.fee-divider {
background: #E7E5E4;
}
}
.dynamic-price-banner {
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
border-color: #FDBA74;
.dp-info {
.dp-tag-row {
.dp-badge {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 2rpx 8rpx rgba(234, 88, 12, 0.25);
}
.dp-rule-tag {
color: #C2410C;
}
}
.dp-status-text {
color: #44403C;
}
}
.dp-btn-group {
border-top-color: rgba(234, 88, 12, 0.2);
.btn-adjust-price {
&.btn-append {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.3);
}
&.btn-edit {
background: #FFFFFF;
border-color: #FDBA74;
color: #C2410C;
box-shadow: none;
}
}
}
}
}
.section-card {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 6rpx 24rpx rgba(28, 25, 23, 0.05);
.sec-header {
.sec-title {
color: #1C1917;
}
.sec-sub {
color: #78716C;
}
}
.creator-box {
background: #F8F5F0;
border: 1px solid #F0ECE4;
.creator-info {
.creator-name-row {
.creator-name {
color: #1C1917;
}
.host-badge {
background: #FFEDD5;
color: #EA580C;
border: 1px solid #FED7AA;
font-weight: 600;
}
}
.creator-sub {
color: #78716C;
}
}
.btn-chat {
background: #FFF7ED;
border-color: #FDBA74;
color: #EA580C;
font-weight: 600;
}
}
.info-list .info-row {
.info-label {
color: #78716C;
}
.info-val {
color: #1C1917;
&.green-text {
color: #059669;
}
&.female-text {
color: #DB2777;
font-weight: 600;
}
&.male-text {
color: #2563EB;
font-weight: 600;
}
}
.copy-val .copy-btn {
color: #EA580C;
background: #FFF7ED;
border: 1px solid #FED7AA;
padding: 2rpx 14rpx;
border-radius: 20rpx;
text-decoration: none;
font-weight: 600;
}
}
.member-list .member-item {
border-bottom-color: #F5F2EC;
.member-info .member-top-line .member-name {
color: #1C1917;
}
&.my-member-item {
background: #FFF7ED;
border: 1px solid #FFEDD5;
}
.my-badge {
color: #EA580C;
background: #FFEDD5;
border-color: #FDBA74;
}
.checkin-badge {
background: #F5F5F4;
color: #78716C;
&.checked {
background: rgba(16, 185, 129, 0.12);
color: #059669;
border-color: rgba(16, 185, 129, 0.3);
}
}
}
&.host-verify-card {
background: linear-gradient(135deg, #FFF7ED 0%, #FFFFFF 100%);
border: 1px solid #FED7AA;
box-shadow: 0 6rpx 24rpx rgba(234, 88, 12, 0.06);
.sec-header {
.sec-title {
color: #1C1917;
}
.sec-sub {
color: #9A3412;
}
}
.title-with-badge .host-pill {
background: #FFEDD5;
color: #EA580C;
border-color: #FDBA74;
}
.verify-action-row {
.verify-input-wrap {
background: #FFFFFF;
border: 1px solid #FDBA74;
box-shadow: inset 0 2rpx 6rpx rgba(234, 88, 12, 0.04);
.verify-code-input {
color: #1C1917;
}
.ph {
color: #A8A29E;
font-weight: 500;
}
}
.btn-verify-now {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
color: #FFFFFF;
box-shadow: 0 6rpx 16rpx rgba(234, 88, 12, 0.28);
}
}
.verify-helper-tip {
color: #78716C;
}
}
&.history-card .history-list .history-item {
background: #FAFAF9;
border-color: #E7E5E4;
.history-right .history-diff {
color: #1C1917;
}
}
}
.adjust-modal-mask .adjust-modal-content {
background: #FFFFFF;
border-top-color: #F0ECE4;
box-shadow: 0 -12rpx 40rpx rgba(28, 25, 23, 0.08);
.sheet-grabber {
background: #E7E5E4;
}
.modal-header {
.modal-title {
color: #1C1917;
}
.modal-subtitle {
color: #78716C;
}
.modal-close-btn {
background: #F5F2EC;
border: 1px solid #E7E5E4;
.close-icon {
color: #57534E;
}
}
}
.modal-mode-tabs {
background: #F5F2EC;
border-color: #E7E5E4;
.mode-tab {
color: #78716C;
&.active {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.32);
}
}
}
.modal-body {
.modal-stat-box {
background: #FAF8F5;
border-color: #EFECE6;
.stat-item {
.stat-label {
color: #78716C;
}
.stat-val {
color: #1C1917;
&.highlight {
color: #EA580C;
}
.unit {
color: #78716C;
}
}
}
.stat-divider {
background: #E7E5E4;
}
}
.modal-input-group {
.group-label {
color: #1C1917;
}
.input-wrap {
background: #FFFBF7;
border-color: #F97316;
box-shadow: 0 2rpx 10rpx rgba(234, 88, 12, 0.06);
.currency-prefix {
color: #EA580C;
}
.modal-input {
color: #1C1917;
}
.input-unit {
color: #78716C;
}
.ph {
color: #A8A29E;
}
}
}
.quick-options .quick-btn {
background: #F8F5F0;
border-color: #E7E5E4;
color: #44403C;
.q-sub {
color: #78716C;
}
&.highlight {
background: #FFF7ED;
border-color: #EA580C;
color: #EA580C;
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.12);
.q-sub {
color: #C2410C;
}
}
&.down {
color: #57534E;
}
}
.calc-change-box {
background: linear-gradient(135deg, #FFF7ED 0%, #FFFAF3 100%);
border-color: #FDBA74;
.calc-top {
.calc-title {
color: #C2410C;
}
.calc-pill {
&.up {
background: #FFEDD5;
color: #EA580C;
border: 1px solid #FDBA74;
}
&.down {
background: rgba(16, 185, 129, 0.12);
color: #059669;
border: 1px solid rgba(16, 185, 129, 0.3);
}
}
}
.calc-summary-grid {
background: #FFFFFF;
border-color: #FED7AA;
.calc-summary-cell {
.cell-label {
color: #78716C;
}
.cell-val-row {
.cell-price {
color: #1C1917;
&.total {
color: #EA580C;
}
.cell-unit {
color: #78716C;
}
}
.cell-origin {
color: #A8A29E;
}
.cell-delta {
&.up {
color: #059669;
background: rgba(16, 185, 129, 0.12);
}
&.down {
color: #78716C;
background: #F5F5F4;
}
}
}
}
.calc-summary-divider {
background: #FED7AA;
}
}
.calc-notes {
.note-row {
color: #57534E;
}
}
}
}
.modal-footer {
.cancel-btn {
background: #F8F5F0;
border: 1px solid #E7E5E4;
color: #57534E;
}
.confirm-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 6rpx 20rpx rgba(234, 88, 12, 0.35);
}
}
}
.bottom-bar {
background-color: rgba(253, 251, 247, 0.96);
border-top-color: #F0ECE4;
.ticket-link .ticket-text {
color: #57534E;
}
.btn-join {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 20rpx rgba(234, 88, 12, 0.35);
}
.btn-host-secondary {
background: #FFF7ED;
color: #EA580C;
border-color: #FDBA74;
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.08);
}
}
}
/* ===== Premium visual layer: 组局详情 ===== */
.detail-container {
background:
radial-gradient(110% 42% at 50% -6%, rgba(99, 102, 241, 0.18), transparent 62%),
var(--surface-0);
.hero-card {
background:
radial-gradient(circle at 88% 8%, rgba(217, 164, 65, 0.12), transparent 46%),
linear-gradient(145deg, #1B2340 0%, #111827 58%, #0D1424 100%);
border-color: rgba(129, 140, 248, 0.22);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.06),
0 18rpx 44rpx rgba(0, 0, 0, 0.28);
animation: detailSectionEnter 0.45s var(--ease-out) both;
.badge-row .game-tag,
.badge-row .mode-tag,
.badge-row .status-tag {
backdrop-filter: blur(8px);
}
.hero-title {
font-size: 38rpx;
line-height: 1.4;
letter-spacing: 0.3rpx;
}
.hero-price {
font-family: var(--font-num, monospace);
}
}
.section-card {
background: linear-gradient(180deg, rgba(22, 31, 51, 0.82) 0%, rgba(15, 22, 38, 0.9) 100%);
border-color: var(--hairline);
box-shadow: 0 12rpx 30rpx rgba(0, 0, 0, 0.16);
animation: detailSectionEnter 0.5s var(--ease-out) both;
.sec-header .sec-title {
letter-spacing: 0.2rpx;
}
.info-row {
border-bottom-color: rgba(255, 255, 255, 0.045);
}
}
.creator-box {
.creator-name {
font-size: 30rpx;
font-weight: 700;
}
.host-badge {
background: rgba(217, 164, 65, 0.1);
border: 1rpx solid rgba(217, 164, 65, 0.28);
color: #F1C878;
}
.btn-chat {
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.04);
border-color: var(--hairline);
color: #CBD5E1;
}
}
.bottom-bar {
background: rgba(10, 15, 29, 0.9);
backdrop-filter: blur(20px);
border-top: 1rpx solid rgba(255, 255, 255, 0.07);
box-shadow: 0 -14rpx 36rpx rgba(0, 0, 0, 0.28);
.btn-join,
.btn-host-secondary,
.btn-host-danger,
.btn-leave,
.ticket-link {
border-radius: 999rpx;
transition: transform var(--motion-fast, 120ms) var(--ease-out, ease-out);
}
.btn-join {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
box-shadow: 0 10rpx 24rpx rgba(16, 185, 129, 0.28);
}
.btn-join:active,
.btn-host-secondary:active,
.btn-host-danger:active,
.btn-leave:active {
transform: scale(0.98);
}
}
}
.detail-container.theme-warm {
background:
radial-gradient(100% 40% at 50% -6%, rgba(234, 88, 12, 0.08), transparent 62%),
#FDFBF7;
.hero-card {
background:
radial-gradient(circle at 88% 8%, rgba(234, 88, 12, 0.06), transparent 46%),
linear-gradient(145deg, #FFFFFF 0%, #FFFBF5 100%);
border-color: #F0ECE4;
box-shadow: 0 16rpx 38rpx rgba(28, 25, 23, 0.06);
.hero-title {
color: #1C1917;
}
}
.section-card {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 12rpx 28rpx rgba(28, 25, 23, 0.05);
}
.creator-box {
.btn-chat {
background: #F8F5F0;
border-color: #F0ECE4;
color: #57534E;
}
}
.bottom-bar {
background: rgba(255, 255, 255, 0.92);
border-top-color: #F0ECE4;
box-shadow: 0 -12rpx 30rpx rgba(28, 25, 23, 0.07);
}
}
@keyframes detailSectionEnter {
from {
opacity: 0;
transform: translateY(14rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>