feat(activity&admin): 优化组局起止时段与性别限制,PC后台核销看板与登录页体验升级

这个提交包含在:
xcbyy
2026-11-05 16:06:18 +08:00
父节点 a1907f18a8
当前提交 f3da2e4a63
共有 21 个文件被更改,包括 1990 次插入 和 203 次删除

查看文件

@@ -44,6 +44,7 @@ export function formatActivityData(raw) {
status: raw.status || (joinedCount >= (raw.maxParticipants || 4) ? 'FULL' : 'RECRUITING'),
allowDynamicPrice: Boolean(raw.allowDynamicPrice),
priceAdjustRule: raw.priceAdjustRule || 'UNIFIED_HIGH',
genderRequirement: raw.genderRequirement || raw.gender_requirement || 'ALL',
participants,
priceAdjustHistory: raw.priceAdjustHistory || [],
};
@@ -117,6 +118,7 @@ export async function createActivity(payload) {
maxParticipants: Number(payload.maxParticipants) || 4,
allowDynamicPrice: payload.allowDynamicPrice !== undefined ? (payload.allowDynamicPrice ? 1 : 0) : 1,
priceAdjustRule: payload.priceAdjustRule || 'UNIFIED_HIGH',
genderRequirement: payload.genderRequirement || 'ALL',
};
try {

查看文件

@@ -66,7 +66,7 @@
</view>
<!-- 基础信息录入 -->
<view class="form-card">
<view class="form-card basic-info-card">
<view class="input-row">
<text class="row-label">活动标题</text>
<input
@@ -88,7 +88,7 @@
>
<view class="picker-display-box btn-active">
<text class="picker-val" :class="{ placeholder: !form.startTime }">
{{ form.startTime || '请选择活动开始时段' }}
{{ form.startTime || '请选择活动时段' }}
</text>
<view class="picker-right-tag">
<text class="picker-tag-text">选择时间</text>
@@ -169,6 +169,22 @@
</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
@@ -448,6 +464,7 @@ const form = ref({
latitude: null,
longitude: null,
maxParticipants: 4,
genderRequirement: 'ALL',
aaFee: '',
pricingMode: 'PER_PERSON',
perPersonFee: '',
@@ -456,6 +473,12 @@ const form = ref({
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;
@@ -485,11 +508,11 @@ const changeDispatchHours = (delta) => {
const recommendPublicZones = ref([]);
const presetTimeOptions = ref([
'今晚 19:30',
'今晚 20:30',
'明天 14:00',
'周六 18:30',
'周日 15:00'
'今晚 19:30 - 21:30',
'今晚 20:30 - 22:30',
'明天 14:00 - 17:00',
'周六 18:30 - 21:30',
'周日 15:00 - 18:00'
]);
const timePickerDays = (() => {
@@ -506,7 +529,7 @@ const timePickerDays = (() => {
return baseDays;
})();
const timePickerSlots = [
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',
@@ -515,28 +538,86 @@ const timePickerSlots = [
'00:00', '00:30', '01:00'
];
const timePickerRange = ref([timePickerDays, timePickerSlots]);
const timePickerIndex = ref([5, 19]); // 默认对应 周六 18:30
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];
const dayIdx = Number(val[0]) || 0;
const slotIdx = Number(val[1]) || 0;
timePickerIndex.value = [dayIdx, slotIdx];
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 slotStr = timePickerSlots[slotIdx] || timePickerSlots[0];
form.value.startTime = `${dayStr} ${slotStr}`;
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 parts = String(timeItem).split(' ');
if (parts.length === 2) {
const dIdx = timePickerDays.indexOf(parts[0]);
const sIdx = timePickerSlots.indexOf(parts[1]);
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
sIdx >= 0 ? sIdx : 0,
eIdx >= 0 ? eIdx : 0
];
}
};
@@ -809,6 +890,7 @@ const handleSubmit = async () => {
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,
@@ -826,6 +908,7 @@ const handleSubmit = async () => {
form.value.title = '';
form.value.startTime = '';
form.value.location = '';
form.value.genderRequirement = 'ALL';
form.value.aaFee = '';
form.value.perPersonFee = '';
form.value.totalFee = '';
@@ -995,6 +1078,15 @@ const handleSubmit = async () => {
color: #94A3B8;
}
&.basic-info-card {
.input-row {
.row-label {
width: 140rpx;
min-width: 140rpx;
}
}
}
.input-row {
display: flex;
align-items: center;
@@ -1035,18 +1127,25 @@ const handleSubmit = async () => {
.time-picker-flex {
flex: 1;
min-width: 0;
.picker-display-box {
display: flex;
align-items: center;
justify-content: space-between;
height: 52rpx;
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;
@@ -1055,6 +1154,8 @@ const handleSubmit = async () => {
}
.picker-right-tag {
flex-shrink: 0;
white-space: nowrap;
display: flex;
align-items: center;
gap: 6rpx;
@@ -1067,6 +1168,7 @@ const handleSubmit = async () => {
font-size: 20rpx;
font-weight: 600;
color: #818CF8;
white-space: nowrap;
}
.picker-arrow {
@@ -1074,6 +1176,7 @@ const handleSubmit = async () => {
font-weight: 700;
color: #818CF8;
line-height: 1;
white-space: nowrap;
}
}
}
@@ -1187,6 +1290,7 @@ const handleSubmit = async () => {
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 6rpx 16rpx;
border-radius: 12rpx;
white-space: nowrap;
transition: all 0.15s;
&.selected {
@@ -1258,6 +1362,44 @@ const handleSubmit = async () => {
}
}
.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;
@@ -1572,6 +1714,29 @@ const handleSubmit = async () => {
}
}
.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;
}
@@ -1598,9 +1763,12 @@ const handleSubmit = async () => {
.picker-right-tag {
background: #FFF7ED;
border-color: #FDBA74;
flex-shrink: 0;
white-space: nowrap;
.picker-tag-text,
.picker-arrow {
color: #EA580C;
white-space: nowrap;
}
}
}

查看文件

@@ -88,7 +88,7 @@
<view class="info-list">
<view class="info-row">
<text class="info-label">开始时间</text>
<text class="info-label">活动时段</text>
<text class="info-val">{{ activity.startTime }}</text>
</view>
<view class="info-row">
@@ -98,6 +98,18 @@
<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>
@@ -502,6 +514,7 @@ const activity = ref({
perPersonFee: 0,
maxParticipants: 0,
currentParticipants: 0,
genderRequirement: 'ALL',
startTime: '',
location: '',
creatorId: 0,
@@ -511,6 +524,12 @@ const activity = ref({
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}` : '同城热门组局活动邀你加入';
@@ -841,6 +860,17 @@ 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位入局核销码。`,
@@ -1244,6 +1274,16 @@ const handleJoin = () => {
&.green-text {
color: #10B981;
}
&.female-text {
color: #EC4899;
font-weight: 600;
}
&.male-text {
color: #38BDF8;
font-weight: 600;
}
}
.copy-val {
@@ -2194,6 +2234,14 @@ const handleJoin = () => {
&.green-text {
color: #059669;
}
&.female-text {
color: #DB2777;
font-weight: 600;
}
&.male-text {
color: #2563EB;
font-weight: 600;
}
}
.copy-val .copy-btn {
color: #EA580C;

查看文件

@@ -104,6 +104,12 @@
<view class="mode-tag" :class="act.payType === 'AA' ? 'aa-mode' : 'project-mode'">
{{ act.payType === 'AA' ? 'AA制平摊' : '房主请客' }}
</view>
<view class="gender-req-tag female" v-if="act.genderRequirement === 'FEMALE_ONLY' || act.genderRequirement === '仅女生'">
仅限女生
</view>
<view class="gender-req-tag male" v-else-if="act.genderRequirement === 'MALE_ONLY' || act.genderRequirement === '仅男生'">
仅限男生
</view>
<view class="status-pill" :class="act.status || 'RECRUITING'">
{{ getStatusText(act.status) }}
</view>
@@ -705,6 +711,25 @@ const createActivity = () => {
}
}
.gender-req-tag {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 12rpx;
font-weight: 600;
&.female {
background: rgba(236, 72, 153, 0.14);
border: 1px solid rgba(236, 72, 153, 0.35);
color: #F472B6;
}
&.male {
background: rgba(59, 130, 246, 0.14);
border: 1px solid rgba(59, 130, 246, 0.35);
color: #60A5FA;
}
}
.status-pill {
margin-left: auto;
font-size: 20rpx;
@@ -952,6 +977,19 @@ const createActivity = () => {
border-color: #F0ECE4;
box-shadow: 0 6rpx 20rpx rgba(28, 25, 23, 0.05);
.gender-req-tag {
&.female {
background: #FDF2F8;
border-color: #FBCFE8;
color: #DB2777;
}
&.male {
background: #EFF6FF;
border-color: #BFDBFE;
color: #2563EB;
}
}
.activity-title {
color: #1C1917;
}