This commit is contained in:
=
2026-06-17 14:15:32 +08:00
11 changed files with 464 additions and 92 deletions
@@ -15,6 +15,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid; import javax.validation.Valid;
@@ -49,6 +50,7 @@ public class CoffeeTicketActivityController {
if (StrUtil.isNotBlank(title)) { if (StrUtil.isNotBlank(title)) {
cnd.and(Cnd.likeEX("title", title)); cnd.and(Cnd.likeEX("title", title));
} }
cnd.desc("createdAt");
Pagination pagination = coffeeTicketActivityService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); Pagination pagination = coffeeTicketActivityService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination); return Result.success(pagination);
} }
@@ -67,4 +69,23 @@ public class CoffeeTicketActivityController {
coffeeTicketActivityService.deleteActivity(id); coffeeTicketActivityService.deleteActivity(id);
return Result.success(); return Result.success();
} }
/**
* 开启或关闭咖啡抢票活动。
*
* @param id 活动ID,传 coffee_ticket_activity 表主键
* @param enabled true 表示开启报名并推送首页,false 表示关闭报名并移除首页入口
* @return Result,成功后前端刷新列表即可看到最新状态
*/
@At
@SaCheckPermission("coffeeTicket.activity")
@Aop(TransAop.READ_COMMITTED)
public Result updateEnabled(@Param("id") @Valid String id, @Param("enabled") Boolean enabled) {
try {
coffeeTicketActivityService.updateEnabled(id, enabled);
return Result.success(Boolean.TRUE.equals(enabled) ? "开启成功" : "关闭成功");
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
}
} }
@@ -35,7 +35,7 @@ public class CoffeeTicketNewController {
/** /**
* 保存咖啡抢票活动。 * 保存咖啡抢票活动。
* *
* @param activity 页面提交的活动配置,包含标题、封面、描述、活动开始/结束日期、可预约星期、票数、每日预约时间段、预约方式、活动地点和参与须知 * @param activity 页面提交的活动配置,包含标题、封面、描述、活动开始/结束日期、可预约星期、票数、每日预约时间段、可报名人员范围、预约方式、活动地点和参与须知
* @return Result,成功时 data 为 CoffeeTicketActivity,可通过 data.id 获取活动ID * @return Result,成功时 data 为 CoffeeTicketActivity,可通过 data.id 获取活动ID
*/ */
@At @At
@@ -46,8 +46,12 @@ public class CoffeeTicketNewController {
if (validateResult != null) { if (validateResult != null) {
return validateResult; return validateResult;
} }
try {
CoffeeTicketActivity savedActivity = coffeeTicketActivityService.saveActivity(activity); CoffeeTicketActivity savedActivity = coffeeTicketActivityService.saveActivity(activity);
return Result.success(savedActivity); return Result.success(savedActivity);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
} }
/** /**
@@ -87,6 +91,9 @@ public class CoffeeTicketNewController {
if (StrUtil.isBlank(activity.getReserveMode())) { if (StrUtil.isBlank(activity.getReserveMode())) {
return Result.error("请选择预约方式"); return Result.error("请选择预约方式");
} }
if (activity.getActivityGroupId() == null) {
return Result.error("请选择可报名人员范围");
}
return null; return null;
} }
} }
@@ -45,6 +45,13 @@ public class H5CoffeeTicketDetailController {
if (activity == null) { if (activity == null) {
return Result.error("活动不存在"); return Result.error("活动不存在");
} }
if (!Boolean.TRUE.equals(activity.getEnabled())) {
return Result.error("活动未开启");
}
String scopeMsg = coffeeTicketActivityService.checkH5ActivityUserScope(activity, SecurityUtil.getUserId());
if (scopeMsg != null) {
return Result.error(scopeMsg);
}
CoffeeTicketActivityH5DetailVO detailVO = BeanUtil.copyProperties(activity, CoffeeTicketActivityH5DetailVO.class); CoffeeTicketActivityH5DetailVO detailVO = BeanUtil.copyProperties(activity, CoffeeTicketActivityH5DetailVO.class);
CoffeeTicketRecord userRecord = coffeeTicketRecordService.fetchLatestUserRecord(id, SecurityUtil.getUserId()); CoffeeTicketRecord userRecord = coffeeTicketRecordService.fetchLatestUserRecord(id, SecurityUtil.getUserId());
detailVO.setHasGrabbed(userRecord != null); detailVO.setHasGrabbed(userRecord != null);
@@ -1,12 +1,15 @@
package com.budwk.app.zhgh.dayofficework.coffeeTicket.models; package com.budwk.app.zhgh.dayofficework.coffeeTicket.models;
import com.budwk.app.base.model.BaseModel; import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.services.SysHomeConvert;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine; import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType; import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column; import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment; import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name; import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table; import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta; import org.nutz.dao.entity.annotation.TableMeta;
@@ -21,7 +24,7 @@ import java.util.List;
@Table("coffee_ticket_activity") @Table("coffee_ticket_activity")
@TableMeta("{'mysql-charset':'utf8mb4'}") @TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("咖啡抢票活动表") @Comment("咖啡抢票活动表")
public class CoffeeTicketActivity extends BaseModel implements Serializable { public class CoffeeTicketActivity extends BaseModel implements Serializable, SysHomeConvert {
@Name @Name
@Comment("ID") @Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32) @ColDefine(type = ColType.VARCHAR, width = 32)
@@ -78,6 +81,17 @@ public class CoffeeTicketActivity extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
private String reserveMode; private String reserveMode;
@Column
@Comment("可报名人员范围分组ID,对应 activity_user_scope.groupId")
@ColDefine(type = ColType.INT, width = 32)
private Integer activityGroupId;
@Column
@Comment("是否开启,开启后手机端可报名并推送首页")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean enabled;
@Column @Column
@Comment("活动地点") @Comment("活动地点")
@ColDefine(type = ColType.VARCHAR, width = 100) @ColDefine(type = ColType.VARCHAR, width = 100)
@@ -87,4 +101,21 @@ public class CoffeeTicketActivity extends BaseModel implements Serializable {
@Comment("参与须知") @Comment("参与须知")
@ColDefine(type = ColType.TEXT) @ColDefine(type = ColType.TEXT)
private String notice; private String notice;
@Override
public Sys_home_activity covertToSysHomeActivity() {
Sys_home_activity sysHomeActivity = new Sys_home_activity();
sysHomeActivity.setId(this.getId());
sysHomeActivity.setName(this.getTitle());
sysHomeActivity.setCover(this.getCover());
sysHomeActivity.setContent(this.getDescription());
sysHomeActivity.setUrl("/platform/coffeeTicket/activity");
sysHomeActivity.setH5Url("/platform/h5/coffeeTicket/detail?id=" + this.getId());
sysHomeActivity.setStartDate(this.getActivityStartDate());
sysHomeActivity.setEndDate(this.getActivityEndDate());
sysHomeActivity.setAllowUserGroupId(this.getActivityGroupId());
sysHomeActivity.setEnable(Boolean.TRUE.equals(this.getEnabled()));
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
return sysHomeActivity;
}
} }
@@ -25,6 +25,14 @@ public interface CoffeeTicketActivityService extends BaseService<CoffeeTicketAct
*/ */
void deleteActivity(String id); void deleteActivity(String id);
/**
* 开启或关闭咖啡抢票活动,并同步移动端首页入口。
*
* @param id 活动ID
* @param enabled true 表示开启报名并推送首页,false 表示关闭报名并移除首页入口
*/
void updateEnabled(String id, Boolean enabled);
/** /**
* 手机端分页查询可展示的咖啡抢票活动。 * 手机端分页查询可展示的咖啡抢票活动。
* *
@@ -51,6 +59,15 @@ public interface CoffeeTicketActivityService extends BaseService<CoffeeTicketAct
*/ */
String checkH5GrabTicket(String id, Integer ticketNum); String checkH5GrabTicket(String id, Integer ticketNum);
/**
* 校验当前用户是否在活动可报名人员范围内。
*
* @param activity 活动实体,必须包含 activityGroupId
* @param userId 当前登录用户ID
* @return String,返回 null 表示在范围内;返回非空字符串表示不可参与原因
*/
String checkH5ActivityUserScope(CoffeeTicketActivity activity, String userId);
/** /**
* 手机端立即抢票并保存抢票记录。 * 手机端立即抢票并保存抢票记录。
* *
@@ -6,7 +6,9 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm; import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.dayofficework.coffeeTicket.models.CoffeeTicketActivity; import com.budwk.app.zhgh.dayofficework.coffeeTicket.models.CoffeeTicketActivity;
import com.budwk.app.zhgh.dayofficework.coffeeTicket.models.CoffeeTicketRecord; import com.budwk.app.zhgh.dayofficework.coffeeTicket.models.CoffeeTicketRecord;
import com.budwk.app.zhgh.dayofficework.coffeeTicket.service.CoffeeTicketActivityService; import com.budwk.app.zhgh.dayofficework.coffeeTicket.service.CoffeeTicketActivityService;
@@ -30,6 +32,9 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
@Inject @Inject
private CoffeeTicketRecordService coffeeTicketRecordService; private CoffeeTicketRecordService coffeeTicketRecordService;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
public CoffeeTicketActivityServiceImpl(Dao dao) { public CoffeeTicketActivityServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -37,7 +42,14 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public CoffeeTicketActivity saveActivity(CoffeeTicketActivity activity) { public CoffeeTicketActivity saveActivity(CoffeeTicketActivity activity) {
if (activity.getActivityGroupId() == null || activityBasicScopeService.getGroupInfo(activity.getActivityGroupId()) == null) {
throw new IllegalStateException("请选择有效的可报名人员范围");
}
if (activity.getEnabled() == null) {
activity.setEnabled(false);
}
dao().insertOrUpdate(activity); dao().insertOrUpdate(activity);
syncHomeActivity(activity);
return activity; return activity;
} }
@@ -45,11 +57,25 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void deleteActivity(String id) { public void deleteActivity(String id) {
dao().delete(CoffeeTicketActivity.class, id); dao().delete(CoffeeTicketActivity.class, id);
dao().delete(Sys_home_activity.class, id);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateEnabled(String id, Boolean enabled) {
CoffeeTicketActivity activity = fetch(id);
if (activity == null) {
throw new IllegalStateException("活动不存在");
}
activity.setEnabled(Boolean.TRUE.equals(enabled));
dao().updateIgnoreNull(activity);
syncHomeActivity(activity);
} }
@Override @Override
public Pagination h5PageData(PageForm pageForm, String status) { public Pagination h5PageData(PageForm pageForm, String status) {
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("enabled", "=", true);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
String keyword = "%" + pageForm.getSearchKeyword() + "%"; String keyword = "%" + pageForm.getSearchKeyword() + "%";
cnd.and(Cnd.exps("title", "like", keyword).or("address", "like", keyword)); cnd.and(Cnd.exps("title", "like", keyword).or("address", "like", keyword));
@@ -64,6 +90,12 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
} else if ("ended".equals(status)) { } else if ("ended".equals(status)) {
cnd.and("activityEndDate", "<", todayStart); cnd.and("activityEndDate", "<", todayStart);
} }
List<Integer> visibleGroupIds = buildCurrentUserVisibleGroupIds(cnd);
if (visibleGroupIds.isEmpty()) {
cnd.and("id", "=", "__none__");
} else {
cnd.and("activityGroupId", "in", visibleGroupIds);
}
cnd.desc("activityStartDate"); cnd.desc("activityStartDate");
Pagination pagination = listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); Pagination pagination = listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
List<CoffeeTicketActivity> activityList = pagination.getList(CoffeeTicketActivity.class); List<CoffeeTicketActivity> activityList = pagination.getList(CoffeeTicketActivity.class);
@@ -93,6 +125,13 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
if (activity == null) { if (activity == null) {
return "活动不存在"; return "活动不存在";
} }
if (!Boolean.TRUE.equals(activity.getEnabled())) {
return "活动未开启";
}
String scopeMsg = checkH5ActivityUserScope(activity, SecurityUtil.getUserId());
if (scopeMsg != null) {
return scopeMsg;
}
Date now = new Date(); Date now = new Date();
if (activity.getActivityStartDate() == null || activity.getActivityEndDate() == null) { if (activity.getActivityStartDate() == null || activity.getActivityEndDate() == null) {
return "活动周期未配置"; return "活动周期未配置";
@@ -126,6 +165,20 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
return null; return null;
} }
@Override
public String checkH5ActivityUserScope(CoffeeTicketActivity activity, String userId) {
if (activity == null) {
return "活动不存在";
}
if (activity.getActivityGroupId() == null) {
return "活动未配置可报名人员范围";
}
if (!activityBasicScopeService.isUserInGroup(activity.getActivityGroupId(), userId)) {
return "您不在本活动可报名人员范围内";
}
return null;
}
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public CoffeeTicketRecord grabH5Ticket(String id, Integer ticketNum) { public CoffeeTicketRecord grabH5Ticket(String id, Integer ticketNum) {
@@ -169,6 +222,35 @@ public class CoffeeTicketActivityServiceImpl extends BaseServiceImpl<CoffeeTicke
return vo; return vo;
} }
/**
* 按当前查询条件先收集活动分组,再复用分组服务过滤出当前用户可参与的分组ID。
*
* @param cnd H5列表搜索条件,包含关键字和活动状态筛选
* @return List<Integer>,当前登录用户在人员范围内的活动分组ID列表
*/
private List<Integer> buildCurrentUserVisibleGroupIds(Cnd cnd) {
List<Integer> groupIds = dao().query(CoffeeTicketActivity.class, cnd)
.stream()
.map(CoffeeTicketActivity::getActivityGroupId)
.filter(groupId -> groupId != null)
.distinct()
.toList();
return activityBasicScopeService.filterGroupIdsByUser(groupIds, SecurityUtil.getUserId());
}
/**
* 同步移动端首页入口。活动开启时写入首页,关闭时删除首页入口。
*
* @param activity 咖啡抢票活动,包含首页展示和跳转所需字段
*/
private void syncHomeActivity(CoffeeTicketActivity activity) {
if (Boolean.TRUE.equals(activity.getEnabled())) {
dao().insertOrUpdate(activity.covertToSysHomeActivity());
return;
}
dao().delete(Sys_home_activity.class, activity.getId());
}
/** /**
* 将 Java 星期值转换为页面保存的 1-7 格式,其中 1 表示周一、7 表示周日。 * 将 Java 星期值转换为页面保存的 1-7 格式,其中 1 表示周一、7 表示周日。
* *
@@ -32,6 +32,11 @@ layout("/layouts/platform.html"){
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder"> <el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="60" type="index" :index="indexMethod"></el-table-column> <el-table-column label="序号" width="60" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="活动标题" prop="title" sortable show-overflow-tooltip></el-table-column> <el-table-column label="活动标题" prop="title" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="创建时间" prop="createdAt" width="170" sortable>
<template scope="{row}">
<span>{{formatDateTime(row.createdAt)}}</span>
</template>
</el-table-column>
<el-table-column label="活动周期" width="220"> <el-table-column label="活动周期" width="220">
<template scope="{row}"> <template scope="{row}">
<span>{{formatDate(row.activityStartDate)}} - {{formatDate(row.activityEndDate)}}</span> <span>{{formatDate(row.activityStartDate)}} - {{formatDate(row.activityEndDate)}}</span>
@@ -48,14 +53,22 @@ layout("/layouts/platform.html"){
<span>{{row.reserveStartTime || '--'}} - {{row.reserveEndTime || '--'}}</span> <span>{{row.reserveStartTime || '--'}} - {{row.reserveEndTime || '--'}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" prop="createdAt" width="170" sortable> <el-table-column label="可报名人员范围" width="180" show-overflow-tooltip>
<template scope="{row}"> <template scope="{row}">
<span>{{formatDateTime(row.createdAt)}}</span> <span>{{formatActivityGroup(row.activityGroupId)}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" fixed="right" width="150"> <el-table-column label="状态" width="100" align="center">
<template scope="{row}">
<el-tag v-if="row.enabled" type="success" size="small">已开启</el-tag>
<el-tag v-else type="info" size="small">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="220">
<template scope="{row}"> <template scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button> <el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button v-if="row.enabled" size="mini" type="warning" @click="updateEnabled(row, false)">关闭</el-button>
<el-button v-else size="mini" type="success" @click="updateEnabled(row, true)">开启</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button> <el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -79,7 +92,8 @@ layout("/layouts/platform.html"){
"5": "周五", "5": "周五",
"6": "周六", "6": "周六",
"7": "周日" "7": "周日"
} },
groupList: []
} }
}, },
methods: { methods: {
@@ -101,6 +115,21 @@ layout("/layouts/platform.html"){
} }
return weekDays.map(item => this.weekMap[item] || item).join("、") return weekDays.map(item => this.weekMap[item] || item).join("、")
}, },
formatActivityGroup(activityGroupId) {
if (!activityGroupId) {
return "--"
}
const group = this.groupList.find(item => String(item.groupId) === String(activityGroupId))
return group ? group.groupName : "--"
},
loadGroupList() {
this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup", {})
.then((res) => {
if (res.code === 0) {
this.groupList = res.data || []
}
})
},
pageData() { pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm) this.$axios.post(loc() + "/pageData", this.pageForm)
.then((res) => { .then((res) => {
@@ -110,6 +139,27 @@ layout("/layouts/platform.html"){
} }
}) })
}, },
updateEnabled(row, enabled) {
const actionText = enabled ? "开启" : "关闭"
this.$confirm("确定" + actionText + "该活动吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/coffeeTicket/activity/updateEnabled", {
id: row.id,
enabled: enabled
})
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg || actionText + "成功")
this.pageData()
} else {
this.$message.warning(res.msg)
}
})
}).catch(() => {})
},
onDelete(id) { onDelete(id) {
this.$confirm("删除后该活动配置不可恢复,确定删除吗?", "提示", { this.$confirm("删除后该活动配置不可恢复,确定删除吗?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
@@ -129,6 +179,7 @@ layout("/layouts/platform.html"){
} }
}, },
created() { created() {
this.loadGroupList()
this.pageData() this.pageData()
} }
}) })
@@ -110,7 +110,8 @@ layout("/layouts/platform.html"){
<custom-card> <custom-card>
<h2 slot="header">创建咖啡抢票活动</h2> <h2 slot="header">创建咖啡抢票活动</h2>
<template> <template>
<el-form class="coffee-ticket-form" :model="formData" ref="formRef" :rules="formRules" label-width="150px" v-loading="formLoading"> <el-form class="coffee-ticket-form" :model="formData" ref="formRef" :rules="formRules" label-width="150px"
v-loading="formLoading">
<section class="coffee-section"> <section class="coffee-section">
<div class="coffee-section-index">1</div> <div class="coffee-section-index">1</div>
<h3 class="coffee-section-title">基础信息</h3> <h3 class="coffee-section-title">基础信息</h3>
@@ -178,21 +179,27 @@ layout("/layouts/platform.html"){
<section class="coffee-section"> <section class="coffee-section">
<div class="coffee-section-index">3</div> <div class="coffee-section-index">3</div>
<h3 class="coffee-section-title">预约规则</h3> <h3 class="coffee-section-title">预约规则</h3>
<el-form-item label="每次可预约票数" prop="ticketCount"> <el-form-item label="每次可抢票票数" prop="ticketCount">
<div class="coffee-ticket-number"> <div class="coffee-ticket-number">
<el-input-number v-model="formData.ticketCount" :min="1" :max="999" :precision="0"></el-input-number> <el-input-number v-model="formData.ticketCount" :min="1" :max="999"
:precision="0"></el-input-number>
<span>张</span> <span>张</span>
<span class="coffee-inline-note">每次可预约的票数上限</span> <span class="coffee-inline-note">每次可抢票的票数上限</span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="每日可预约时间段" required> <el-form-item label="可报名人员范围" prop="activityGroupId">
<permission-group :value.sync="formData.activityGroupId"></permission-group>
<div class="coffee-form-item-tip">只有该人员范围内的用户可以在手机端看到并参与本活动</div>
</el-form-item>
<el-form-item label="每日可抢票时间段" required>
<div class="coffee-time-range"> <div class="coffee-time-range">
<el-form-item prop="reserveStartTime"> <el-form-item prop="reserveStartTime">
<el-time-select <el-time-select
v-model="formData.reserveStartTime" v-model="formData.reserveStartTime"
placeholder="开始时间" placeholder="开始时间"
:picker-options="{ start: '00:00', step: '00:30', end: '23:30' }"> :picker-options="{ start: '07:00', step: '00:10', end: '23:30' }">
</el-time-select> </el-time-select>
</el-form-item> </el-form-item>
<span>-</span> <span>-</span>
@@ -268,6 +275,8 @@ layout("/layouts/platform.html"){
reserveStartTime: "", reserveStartTime: "",
reserveEndTime: "", reserveEndTime: "",
reserveMode: "WEEKLY", reserveMode: "WEEKLY",
activityGroupId: null,
enabled: false,
address: "", address: "",
notice: "" notice: ""
}, },
@@ -295,7 +304,8 @@ layout("/layouts/platform.html"){
{required: true, message: "请选择结束时间", trigger: "change"}, {required: true, message: "请选择结束时间", trigger: "change"},
{validator: validateTimeRange, trigger: "change"} {validator: validateTimeRange, trigger: "change"}
], ],
reserveMode: [{ required: true, message: "请选择预约方式", trigger: "change" }] reserveMode: [{required: true, message: "请选择预约方式", trigger: "change"}],
activityGroupId: [{required: true, message: "请选择可报名人员范围", trigger: ["blur", "change"]}]
} }
} }
}, },
@@ -319,6 +329,8 @@ layout("/layouts/platform.html"){
reserveStartTime: this.formData.reserveStartTime, reserveStartTime: this.formData.reserveStartTime,
reserveEndTime: this.formData.reserveEndTime, reserveEndTime: this.formData.reserveEndTime,
reserveMode: this.formData.reserveMode, reserveMode: this.formData.reserveMode,
activityGroupId: this.formData.activityGroupId,
enabled: this.formData.enabled,
address: this.formData.address, address: this.formData.address,
notice: this.formData.notice notice: this.formData.notice
} }
@@ -334,9 +346,7 @@ layout("/layouts/platform.html"){
.then((res) => { .then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$message.success(res.msg || "保存成功") this.$message.success(res.msg || "保存成功")
if (res.data && res.data.id) { pjaxReplace("/platform/coffeeTicket/activity")
this.$set(this.formData, "id", res.data.id)
}
} else { } else {
this.$message.warning(res.msg) this.$message.warning(res.msg)
} }
@@ -7,18 +7,35 @@ layout("/layouts/platform_h5.html"){
} }
.detail-page { .detail-page {
min-height: 100vh; height: 100vh;
padding-bottom: 74px; overflow: hidden;
background: #f5f7fb; background: #f5f7fb;
} }
.detail-cover { .detail-cover {
position: relative; position: fixed;
left: 12px;
right: 12px;
top: 58px;
z-index: 8;
height: 170px; height: 170px;
overflow: hidden; overflow: hidden;
border-radius: 14px;
background: linear-gradient(135deg, #dfeeff, #f8fbff); background: linear-gradient(135deg, #dfeeff, #f8fbff);
} }
.detail-cover::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1;
background: linear-gradient(to top, rgba(0, 0, 0, .58), rgba(0, 0, 0, .08) 58%, rgba(0, 0, 0, 0));
pointer-events: none;
}
.detail-cover img { .detail-cover img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -35,8 +52,48 @@ layout("/layouts/platform_h5.html"){
font-size: 13px; font-size: 13px;
} }
.detail-cover-text {
position: absolute;
left: 16px;
right: 16px;
bottom: 16px;
z-index: 2;
color: #ffffff;
}
.detail-cover-title {
margin-bottom: 4px;
color: #ffffff;
font-size: 21px;
line-height: 28px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-cover-subtitle {
color: rgba(255, 255, 255, .9);
font-size: 14px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-scroll {
position: fixed;
left: 0;
right: 0;
top: 240px;
bottom: 66px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.detail-card { .detail-card {
margin: -10px 12px 12px; margin: 0 12px 12px;
position: relative; position: relative;
z-index: 1; z-index: 1;
border-radius: 10px; border-radius: 10px;
@@ -179,7 +236,7 @@ layout("/layouts/platform_h5.html"){
</style> </style>
<div id="app" v-cloak class="detail-page"> <div id="app" v-cloak class="detail-page">
<van-nav-bar title="活动详情" left-arrow placeholder fixed @click-left="historyBack"> <van-nav-bar title="活动详情" left-text="返回" left-arrow placeholder fixed @click-left="historyBack">
</van-nav-bar> </van-nav-bar>
<div class="detail-cover"> <div class="detail-cover">
@@ -187,13 +244,13 @@ layout("/layouts/platform_h5.html"){
<div v-else class="detail-cover-empty">暂无封面</div> <div v-else class="detail-cover-empty">暂无封面</div>
</div> </div>
<div class="detail-scroll">
<div class="detail-card"> <div class="detail-card">
<div class="detail-main"> <div class="detail-main">
<div class="detail-title-row"> <div class="detail-title-row">
<div class="detail-title">{{formData.title || '--'}}</div> <div class="detail-title">{{formData.title || '--'}}</div>
<div class="detail-status" :class="statusInfo.className">{{statusInfo.text}}</div> <div class="detail-status" :class="statusInfo.className">{{statusInfo.text}}</div>
</div> </div>
<div class="detail-desc">{{formData.description || '暂无活动描述'}}</div>
<div class="detail-info"> <div class="detail-info">
<div class="detail-info-row"> <div class="detail-info-row">
<i class="van-icon van-icon-calendar-o"></i> <i class="van-icon van-icon-calendar-o"></i>
@@ -229,6 +286,13 @@ layout("/layouts/platform_h5.html"){
</div> </div>
</div> </div>
<div class="notice-card">
<div class="notice-title">
<i class="van-icon van-icon-bookmark-o"></i>
<span>活动描述</span>
</div>
<div class="notice-content">{{formData.description || '暂无活动描述'}}</div>
</div>
<div class="notice-card"> <div class="notice-card">
<div class="notice-title"> <div class="notice-title">
<i class="van-icon van-icon-bookmark-o"></i> <i class="van-icon van-icon-bookmark-o"></i>
@@ -236,9 +300,20 @@ layout("/layouts/platform_h5.html"){
</div> </div>
<div class="notice-content">{{noticeText}}</div> <div class="notice-content">{{noticeText}}</div>
</div> </div>
</div>
<div class="detail-footer"> <div class="detail-footer">
<van-button class="detail-action" type="info" block :disabled="formData.hasGrabbed || ticketLoading" :loading="ticketLoading" loading-text="校验中..." @click="onTicket">{{formData.hasGrabbed ? '已抢票' : '立即抢票'}}</van-button> <van-button
v-if="ticketActionInfo.visible"
class="detail-action"
type="info"
block
:disabled="ticketActionInfo.disabled || ticketLoading"
:loading="ticketLoading"
loading-text="校验中..."
@click="onTicket">
{{formData.hasGrabbed ? '已抢票' : ticketActionInfo.text}}
</van-button>
</div> </div>
</div> </div>
@@ -249,6 +324,8 @@ layout("/layouts/platform_h5.html"){
return { return {
formLoading: false, formLoading: false,
ticketLoading: false, ticketLoading: false,
nowTimestamp: Date.now(),
ticketTimer: null,
formData: {}, formData: {},
weekMap: { weekMap: {
"1": "周一", "1": "周一",
@@ -259,7 +336,7 @@ layout("/layouts/platform_h5.html"){
"6": "周六", "6": "周六",
"7": "周日" "7": "周日"
}, },
defaultNotice: "1. 请提前到达活动地点,活动开始后15分钟停止入场。\n2. 每人每次最多可预约40张,请合理安排名额。\n3. 预约成功后如需取消,请提前2小时操作。\n4. 如有疑问,请联系客服或关注活动公告。" defaultNotice: "如有疑问,请联系校工会或关注活动公告。"
} }
}, },
computed: { computed: {
@@ -275,6 +352,44 @@ layout("/layouts/platform_h5.html"){
}, },
noticeText() { noticeText() {
return this.formData.notice || this.defaultNotice return this.formData.notice || this.defaultNotice
},
ticketActionInfo() {
const now = this.$moment(this.nowTimestamp)
const today = now.format("YYYY-MM-DD")
if (this.formData.hasGrabbed) {
return { visible: true, disabled: true, text: "已抢票" }
}
if (!this.formData.activityStartDate || !this.formData.activityEndDate) {
return { visible: false, disabled: true, text: "" }
}
if (now.isBefore(this.$moment(this.formData.activityStartDate).startOf("day"))) {
return { visible: false, disabled: true, text: "" }
}
if (now.isAfter(this.$moment(this.formData.activityEndDate).endOf("day"))) {
return { visible: true, disabled: true, text: "活动已结束" }
}
if (!this.formData.weekDays || this.formData.weekDays.indexOf(this.getTodayWeekValue()) === -1) {
return { visible: true, disabled: true, text: "今日不可抢票" }
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return { visible: true, disabled: true, text: "未配置抢票时间" }
}
const startTime = this.$moment(today + " " + this.formData.reserveStartTime, "YYYY-MM-DD HH:mm")
const endTime = this.$moment(today + " " + this.formData.reserveEndTime, "YYYY-MM-DD HH:mm")
const showTime = startTime.clone().subtract(10, "minutes")
if (now.isBefore(showTime)) {
return { visible: false, disabled: true, text: "" }
}
if (now.isBefore(startTime)) {
const seconds = Math.max(startTime.diff(now, "seconds"), 0)
const minutes = Math.floor(seconds / 60)
const leftSeconds = seconds % 60
return { visible: true, disabled: true, text: "距抢票开始 " + minutes + "分" + leftSeconds + "秒" }
}
if (now.isAfter(endTime)) {
return { visible: true, disabled: true, text: "今日抢票已结束" }
}
return { visible: true, disabled: false, text: "立即抢票" }
} }
}, },
methods: { methods: {
@@ -293,7 +408,13 @@ layout("/layouts/platform_h5.html"){
if (!weekDays || weekDays.length === 0) { if (!weekDays || weekDays.length === 0) {
return "--" return "--"
} }
return weekDays.map(item => this.weekMap[item] || item).join("、") return weekDays.slice().sort((a, b) => {
return Number(a) - Number(b)
}).map(item => this.weekMap[item] || item).join("、")
},
getTodayWeekValue() {
const day = this.$moment(this.nowTimestamp).day()
return day === 0 ? "7" : String(day)
}, },
loadData() { loadData() {
const id = this.getQueryString("id") const id = this.getQueryString("id")
@@ -318,6 +439,9 @@ layout("/layouts/platform_h5.html"){
if (this.ticketLoading) { if (this.ticketLoading) {
return return
} }
if (this.ticketActionInfo.disabled) {
return
}
if (!this.formData.id) { if (!this.formData.id) {
this.$toast.fail("活动ID不能为空") this.$toast.fail("活动ID不能为空")
return return
@@ -365,6 +489,15 @@ layout("/layouts/platform_h5.html"){
}, },
mounted() { mounted() {
this.loadData() this.loadData()
this.ticketTimer = setInterval(() => {
this.nowTimestamp = Date.now()
}, 1000)
},
beforeDestroy() {
if (this.ticketTimer) {
clearInterval(this.ticketTimer)
this.ticketTimer = null
}
} }
}) })
</script> </script>
@@ -4,12 +4,16 @@ layout("/layouts/platform_h5.html"){
<style scoped> <style scoped>
body { body {
background: #f5f7fb; background: #f5f7fb;
overflow: hidden;
} }
.ticket-page { .ticket-page {
min-height: 100vh; height: 100vh;
overflow-y: auto;
box-sizing: border-box;
padding-bottom: 62px; padding-bottom: 62px;
background: #f5f7fb; background: #f5f7fb;
-webkit-overflow-scrolling: touch;
} }
.ticket-sticky { .ticket-sticky {
@@ -216,7 +220,7 @@ layout("/layouts/platform_h5.html"){
</style> </style>
<div id="app" v-cloak class="ticket-page"> <div id="app" v-cloak class="ticket-page">
<van-nav-bar title="抢票活动" left-text="返回" left-arrow placeholder fixed @click-left="historyBack"></van-nav-bar> <van-nav-bar title="抢票活动" left-text="返回" left-arrow placeholder fixed @click-left="()=>this.$pjaxReplace('/platform/h5/home')"></van-nav-bar>
<van-sticky offset-top="46px" class="ticket-sticky"> <van-sticky offset-top="46px" class="ticket-sticky">
<div class="ticket-search"> <div class="ticket-search">
@@ -261,7 +265,7 @@ layout("/layouts/platform_h5.html"){
<i class="van-icon van-icon-clock-o"></i> <i class="van-icon van-icon-clock-o"></i>
<span class="ticket-info-label">可抢票</span> <span class="ticket-info-label">可抢票</span>
<span class="ticket-info-value ticket-week-tags" v-if="item.weekDays && item.weekDays.length > 0"> <span class="ticket-info-value ticket-week-tags" v-if="item.weekDays && item.weekDays.length > 0">
<span class="ticket-week-tag" v-for="week in item.weekDays" :key="week">{{weekMap[week] || week}}</span> <span class="ticket-week-tag" v-for="week in sortWeekDays(item.weekDays)" :key="week">{{weekMap[week] || week}}</span>
</span> </span>
<span class="ticket-info-value" v-else>--</span> <span class="ticket-info-value" v-else>--</span>
</div> </div>
@@ -331,6 +335,11 @@ layout("/layouts/platform_h5.html"){
} }
return weekDays.map(item => this.weekMap[item] || item).join(" / ") return weekDays.map(item => this.weekMap[item] || item).join(" / ")
}, },
sortWeekDays(weekDays) {
return weekDays.slice().sort((a, b) => {
return Number(a) - Number(b)
})
},
getStatusInfo(row) { getStatusInfo(row) {
const now = this.$moment() const now = this.$moment()
if (row.activityEndDate && now.isAfter(this.$moment(row.activityEndDate).endOf("day"))) { if (row.activityEndDate && now.isAfter(this.$moment(row.activityEndDate).endOf("day"))) {
@@ -4,12 +4,16 @@ layout("/layouts/platform_h5.html"){
<style scoped> <style scoped>
body { body {
background: #f5f7fb; background: #f5f7fb;
overflow: hidden;
} }
.mine-page { .mine-page {
min-height: 100vh; height: 100vh;
overflow-y: auto;
box-sizing: border-box;
padding-bottom: 62px; padding-bottom: 62px;
background: #f5f7fb; background: #f5f7fb;
-webkit-overflow-scrolling: touch;
} }
.mine-filter-wrap { .mine-filter-wrap {
@@ -181,7 +185,7 @@ layout("/layouts/platform_h5.html"){
</style> </style>
<div id="app" v-cloak class="mine-page"> <div id="app" v-cloak class="mine-page">
<van-nav-bar title="我的抢票" left-arrow placeholder fixed @click-left="historyBack"></van-nav-bar> <van-nav-bar title="我的抢票" left-arrow left-text="返回" placeholder fixed @click-left="()=>this.$pjaxReplace('/platform/h5/home')"></van-nav-bar>
<van-sticky offset-top="46px" class="mine-filter-wrap"> <van-sticky offset-top="46px" class="mine-filter-wrap">
<div class="mine-filter"> <div class="mine-filter">