feat(activity&admin): 优化组局起止时段与性别限制,PC后台核销看板与登录页体验升级
这个提交包含在:
@@ -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;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户