This commit is contained in:
@jyuhsin
2025-10-24 17:18:32 +08:00
parent b4db19e6b1
commit 5f8de5e32b
20 changed files with 2218 additions and 23 deletions
@@ -37,7 +37,7 @@ public class GuildHallController {
@At("")
@SaCheckPermission("guildHall.management")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/guildhall/index.html")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/guildhall/manage/index.html")
public void index() {
}
@@ -90,10 +90,17 @@ public class GuildHallController {
return Result.success(dao.fetch(GuildHall.class, id));
}
@At
@SaCheckPermission("guildHall.management")
@ApiOperation("查询会议厅多个")
public Result selectList() {
return Result.success(dao.query(GuildHall.class, Cnd.NEW()));
}
@At
@SaCheckPermission("guildHall.management")
@ApiOperation("查询座位")
public Result selectSeat(String hallId) {
public Result selectSeatList(String hallId) {
List<GuildHallSeat> list = dao.query(GuildHallSeat.class, Cnd.where(GuildHallSeat::getHallId, "=", hallId)
.asc(GuildHallSeat::getRowNumber).asc(GuildHallSeat::getColNumber));
@@ -1,21 +1,33 @@
package com.budwk.app.zhgh.democratic.teachercongress.guildhall.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHall;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallMeeting;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallSeat;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallSeatSelect;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.service.GuildHallMeetingService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.ArrayList;
import java.util.List;
@Api("会议厅会议")
@IocBean
@At("/platform/guildHall/meeting")
@@ -50,17 +62,32 @@ public class GuildHallMeetingController {
@SaCheckPermission("guildHall.meeting")
@SLog(tag = "会议厅会议", msg = "保存会议厅会议")
@ApiOperation("保存会议厅会议")
@Aop(TransAop.READ_COMMITTED)
public Result save(GuildHallMeeting meeting) {
dao.insert(meeting);
return Result.success();
List<GuildHallSeat> seats = dao.query(GuildHallSeat.class, Cnd.where("hallId", "=", meeting.getHallId()));
List<GuildHallSeatSelect> selects = BeanUtil.copyToList(seats, GuildHallSeatSelect.class);
for (GuildHallSeatSelect select : selects) {
select.setMeetingId(meeting.getId());
}
dao.insert(selects);
return Result.success();
}
@At
@SaCheckPermission("guildHall.meeting")
@SLog(tag = "会议厅会议", msg = "修改会议厅会议")
@ApiOperation("修改会议厅会议")
@Aop(TransAop.READ_COMMITTED)
public Result update(GuildHallMeeting meeting) {
dao.update(meeting);
dao.clear(GuildHallSeatSelect.class, Cnd.where("hallId", "=", meeting.getHallId()));
List<GuildHallSeat> seats = dao.query(GuildHallSeat.class, Cnd.where("hallId", "=", meeting.getHallId()));
List<GuildHallSeatSelect> selects = BeanUtil.copyToList(seats, GuildHallSeatSelect.class);
for (GuildHallSeatSelect select : selects) {
select.setMeetingId(meeting.getId());
}
dao.insert(selects);
return Result.success();
}
@@ -73,4 +100,21 @@ public class GuildHallMeetingController {
return Result.success();
}
@At
@SaCheckPermission("guildHall")
@ApiOperation("查询会议")
public Result selectList() {
List<NutMap> results = new ArrayList<>();
List<GuildHallMeeting> meetings = dao.query(GuildHallMeeting.class, Cnd.NEW().desc("startTime"));
for (GuildHallMeeting meeting : meetings) {
if(DateUtil.compare(DateUtil.date(), meeting.getEndTime()) > 0) {
continue;
}
NutMap nutMap = Lang.obj2nutmap(meeting);
GuildHall hall = dao.fetch(GuildHall.class, meeting.getHallId());
nutMap.put("address", hall.getAddress());
results.add(nutMap);
}
return Result.success(results);
}
}
@@ -0,0 +1,140 @@
package com.budwk.app.zhgh.democratic.teachercongress.guildhall.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHall;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallMeeting;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.service.GuildHallService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName GuildHallSeatSummaryController
* @Author JyuHsin
* @Date 2025/10/24 16:25
* @Version 1.0
* @Description TODO
*/
@Api("会议选座统计")
@IocBean
@At("/platform/guildHall/seatSummary")
@Ok("json:full")
public class GuildHallSeatSummaryController {
@Inject
private Dao dao;
@Inject
private GuildHallService guildHallService;
@At("")
@SaCheckPermission("guildHall.seatSummary")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/guildhall/seatSummary/index.html")
public void index() {
}
@At
@SaCheckPermission("guildHall.seatSummary")
@ApiOperation("分页查询")
public Result pageData(PageForm pageForm,
@Param(value = "meetingId") String meetingId) {
Sql sql = Sqls.create("""
SELECT
u.userName,
u.loginName,
u.unitName as unitName,
u.unionName as unionName,
u.mobile,
u.sex,
t1.rowNumber,
t1.colNumber
FROM
`guild_hall_seat_select` t1
LEFT JOIN `vw_user` u ON u.id = t1.userId
WHERE
t1.userId IS NOT NULL AND t1.meetingId = @meetingId
order by t1.rowNumber ASC,t1.colNumber ASC
""");
sql.setParam("meetingId", meetingId);
Pagination pagination = guildHallService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@SaCheckPermission("guildHall.seatSummary")
@ApiOperation("查询会议")
public Result listMeeting() {
List<GuildHallMeeting> meetings = dao.query(GuildHallMeeting.class, Cnd.NEW().desc("startTime"));
return Result.success(meetings);
}
@At
@Ok("void")
@SaCheckPermission("guildHall.seatSummary")
public void exportExcel(String meetingId, HttpServletResponse response) {
Sql sql = Sqls.create("""
SELECT
u.userName,
u.loginName,
u.unitName as unitName,
u.unionName as unionName,
u.mobile,
u.sex,
t1.rowNumber,
t1.colNumber,
CONCAT(t1.rowNumber,'排',t1.colNumber,'列') seat
FROM
`guild_hall_seat_select` t1
LEFT JOIN `vw_user` u ON u.id = t1.userId
WHERE
t1.userId IS NOT NULL AND t1.meetingId = @meetingId
order by t1.rowNumber ASC,t1.colNumber ASC
""");
sql.setParam("meetingId",meetingId);
List<NutMap> list = guildHallService.listMap(sql);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("姓名", "userName", 20));
exportEntities.add(new ExcelExportEntity("工号", "loginName", 20));
exportEntities.add(new ExcelExportEntity("性别", "sex", 20));
exportEntities.add(new ExcelExportEntity("单位", "unitName", 20));
exportEntities.add(new ExcelExportEntity("分工会", "unionName", 20));
exportEntities.add(new ExcelExportEntity("手机号", "mobile", 20));
exportEntities.add(new ExcelExportEntity("座位", "seat", 20));
try {
ExportParams exportParams = new ExportParams();
exportParams.setSheetName("选座人员");
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("选座人员.xlsx", workbook, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,97 @@
package com.budwk.app.zhgh.democratic.teachercongress.guildhall.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallMeeting;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallSeat;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.models.GuildHallSeatSelect;
import com.budwk.app.zhgh.democratic.teachercongress.guildhall.service.GuildHallService;
import io.swagger.annotations.Api;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Date;
import java.util.List;
/**
* @ClassName GuildHallSelectSeatController
* @Author JyuHsin
* @Date 2025/10/24 15:22
* @Version 1.0
* @Description TODO
*/
@Api("会议选座")
@IocBean
@At("/platform/guildHall/selectSeat")
@Ok("json:full")
public class GuildHallSelectSeatController {
@Inject
private Dao dao;
@Inject
private GuildHallService guildHallService;
@At("")
@SaCheckPermission("guildHall.selectSeat")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/guildhall/selectSeat/index.html")
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.guildHall.selectSeat")
@Ok("beetl:/platform/zhghh5/democratic/teachercongress/guildhall/selectSeat/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("guildHall.selectSeat")
public Result loadSeats(@Param("meetingId") String meetingId) {
// 已选择情况
List<GuildHallSeatSelect> selectedSeats = dao.query(GuildHallSeatSelect.class, Cnd.where("meetingId", "=", meetingId));
for (GuildHallSeatSelect selectedSeat : selectedSeats) {
if (selectedSeat.getUserId() != null) {
selectedSeat.setIsOccupied(true);
selectedSeat.setOccupiedBy(selectedSeat.getUserId());
selectedSeat.setIsMine(selectedSeat.getUserId().equals(SecurityUtil.getUserId()));
} else {
selectedSeat.setIsOccupied(false);
selectedSeat.setIsMine(false);
}
}
// 座位排序 首先按seatRowNum 再按seatColNum
selectedSeats.sort((o1, o2) -> {
int compare = o1.getRowNumber().compareTo(o2.getRowNumber());
if (compare == 0) {
return o1.getColNumber().compareTo(o2.getColNumber());
}
return compare;
});
return Result.success(selectedSeats);
}
@At
@SaCheckPermission("guildHall.selectSeat")
public Result doSelect(@Param("meetingId") String meetingId, Integer rowNumber, Integer colNumber) {
GuildHallSeatSelect oldSeat = dao.fetch(GuildHallSeatSelect.class, Cnd.where("meetingId", "=", meetingId).and("userId", "=", SecurityUtil.getUserId()));
if(oldSeat != null){
oldSeat.setUserId(null);
oldSeat.setSelectTime(null);
dao.update(oldSeat);
}
GuildHallSeatSelect seat = dao.fetch(GuildHallSeatSelect.class, Cnd.where("meetingId", "=", meetingId).and("rowNumber", "=", rowNumber).and("colNumber", "=", colNumber));
seat.setUserId(SecurityUtil.getUserId());
seat.setSelectTime(new Date());
dao.update(seat);
return Result.success();
}
}
@@ -58,4 +58,9 @@ public class GuildHallMeeting extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer groupId;
@Column
@Comment("会厅Id")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String hallId;
}
@@ -7,6 +7,8 @@ import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("guild_hall_seat")
@@ -39,9 +41,15 @@ public class GuildHallSeat extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer colNumber;
@Column
@Comment("座位编号")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String seatNumber;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enable;
}
@@ -0,0 +1,53 @@
package com.budwk.app.zhgh.democratic.teachercongress.guildhall.models;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
/**
* @ClassName GuildHallSeatSelect
* @Author JyuHsin
* @Date 2025/10/24 15:55
* @Version 1.0
* @Description TODO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("guild_hall_seat_select")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("会议厅座位")
@Accessors(chain = true)
@TableIndexes(value = {
@Index(name = "INDEX_GUILD_HALL_SEAT_HALL_ID", fields = "hallId",unique = false)
})
public class GuildHallSeatSelect extends GuildHallSeat{
@Name
@PrevInsert(uu32 = true)
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("会议id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String meetingId;
@Column
@Comment("用户id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("选座时间")
@ColDefine(type = ColType.DATETIME)
private Date selectTime;
private Boolean isOccupied;
private String occupiedBy;
private Boolean isMine;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -48,6 +48,7 @@ module.exports = {
<el-select placeholder="参加人员范围"
v-model="internalGroupId"
clearable
style="width: 100%"
filterable>
<el-option v-for="item in groupOptions"
:label="item.groupName"
@@ -55,7 +56,7 @@ module.exports = {
:key="item.groupId"
></el-option>
</el-select>
<el-button @click="$refs.drawerUserScope.userScopeDialog = true" type="primary" icon="el-icon-setting">设置
<el-button @click="$refs.drawerUserScope.userScopeDialog = true" type="primary" icon="el-icon-setting" class="ml10">设置
</el-button>
</div>
@@ -8,10 +8,11 @@ const jcdt = {
<div class="jcdt-content">
<div class="jcdt-list">
<div class="jcdt-item" v-for="item in list" :key="item.title" @click="onLink(item)">
<div class="date">{{item.date}}</div>
<div class="date" v-if="item.date">{{item.date}}</div>
<div class="title">{{item.title}}</div>
<div class="image">
<img :src="item.image" alt="">
<img v-if="item.image" :src="item.image" alt="">
<img v-else src="/assets/platform/img/v4/not-image.png" alt="">
</div>
<div class="summary">{{item.summary}}</div>
</div>
@@ -38,7 +39,7 @@ const jcdt = {
style: /*language=CSS*/ `
.jcdt-wrapper {
width: 100%;
background-image: url(https://cdgh.ncu.edu.cn/images/index_bg2.png);
background-image: url(/assets/platform/img/v4/home-bg.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
@@ -32,7 +32,7 @@ layout("/layouts/platform.html"){
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
@@ -33,7 +33,7 @@ layout("/layouts/platform.html"){
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
@@ -34,7 +34,7 @@ layout("/layouts/platform.html"){
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
@@ -123,7 +123,7 @@ const seat = {
loadSeats() {
// 加载现有座位数据
this.$axios.post('/platform/guildHall/selectSeat', {hallId: this.row.id}).then(res => {
this.$axios.post('/platform/guildHall/selectSeatList', {hallId: this.row.id}).then(res => {
if (res.code === 0 && res.data && res.data.length > 0) {
this.seats = res.data
// 根据现有数据计算行列数
@@ -9,7 +9,7 @@ layout("/layouts/platform.html"){
<el-input placeholder="请输入名称" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
<search-item label="年度">
<el-date-picker v-model="pageForm.year" type="year" placeholder="请选择年度"></el-date-picker>
<el-date-picker v-model="pageForm.year" type="year" placeholder="请选择年度" style="width: 100%;"></el-date-picker>
</search-item>
</search>
</el-card>
@@ -31,6 +31,7 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="70%">
@@ -40,7 +41,7 @@ layout("/layouts/platform.html"){
</el-form-item>
<el-form-item label="年度" prop="year">
<el-date-picker v-model="formData.year" type="year" value-format="yyyy"
placeholder="选择年度"></el-date-picker>
placeholder="选择年度" style="width: 100%;"></el-date-picker>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
@@ -60,6 +61,11 @@ layout("/layouts/platform.html"){
style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item label="会厅选择" prop="hallId">
<el-select v-model="formData.hallId" placeholder="请选择会厅" style="width: 100%">
<el-option v-for="item in hallList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="内容" prop="content">
<text-editor v-model="formData.content"></text-editor>
</el-form-item>
@@ -83,12 +89,14 @@ layout("/layouts/platform.html"){
return {
dialogVisible: false,
formRules: {
name: [{required: true, message: '请输入名称', trigger: 'blur'}],
year: [{required: true, message: '请选择年度', trigger: 'change'}],
startTime: [{required: true, message: '请选择开始时间', trigger: 'change'}],
endTime: [{required: true, message: '请选择结束时间', trigger: 'change'}],
content: [{required: true, message: '请输入内容', trigger: 'blur'}]
}
name: [{required: true, message: '必填', trigger: 'blur'}],
year: [{required: true, message: '必填', trigger: 'change'}],
startTime: [{required: true, message: '必填', trigger: 'change'}],
endTime: [{required: true, message: '必填', trigger: 'change'}],
hallId: [{required: true, message: '必填', trigger: 'blur'}],
content: [{required: true, message: '必填', trigger: 'blur'}],
},
hallList: [],
}
},
methods: {
@@ -106,6 +114,7 @@ layout("/layouts/platform.html"){
},
onEdit(row) {
this.dialogVisible = true;
row.year = row.year.toString()
this.formData = {...row}
},
onSave() {
@@ -139,13 +148,16 @@ layout("/layouts/platform.html"){
this.$message.success(res.msg)
})
})
}
},
selectList() {
this.$axios.post('/platform/guildHall/selectList').then(res => {
this.hallList = res.data
})
},
},
created() {
this.selectList();
this.doSearch();
setTimeout(() => {
console.log(this.$refs.tableRef.columns)
},2000)
}
})
</script>
@@ -0,0 +1,81 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="会议">
<el-select clearable v-model="pageForm.meetingId" filterable placeholder="请选择会议">
<el-option
v-for="item in meetings"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="选座人员">
<el-button size="small" type="primary" @click="exportExcel">导出选座人员</el-button>
</table-tool>
<el-table :data="tableData" @sort-change='pageOrder'>
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column align="center" header-align="center" label="姓名" prop="userName"></el-table-column>
<el-table-column align="center" header-align="center" label="工号" prop="loginName"></el-table-column>
<el-table-column align="center" header-align="center" label="性别" prop="sex"></el-table-column>
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName"></el-table-column>
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName"></el-table-column>
<el-table-column align="center" header-align="center" label="联系方式" prop="mobile"></el-table-column>
<el-table-column align="center" header-align="center" label="座位" prop="seat">
<template scope="{row}">
{{row.rowNumber + '排' + row.colNumber + '列'}}
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
</div>
<script>
let vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
meetings: []
}
},
methods: {
// 加载会议列表
loadMeetings() {
$.post('/platform/guildHall/seatSummary/listMeeting').then(res => {
if (res.code === 0) {
this.meetings = res.data
if (this.meetings.length > 0) {
this.pageForm.meetingId = this.meetings[0].id
}
this.pageData()
}
})
},
exportExcel() {
this.$downLoad(loc() + "/exportExcel", {
meetingId: this.pageForm.meetingId
})
}
},
async created() {
this.loadMeetings()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,837 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.container-seat {
margin: 0 auto;
background: white;
min-height: 100vh;
}
.header-seat {
background-color: #ffffff;
color: #000000;
padding: 10px 20px;
border-bottom: 1px solid #ddd;
}
.header-seat h1 {
font-size: 24px;
margin-bottom: 5px;
font-weight: 500;
}
.header-seat p {
opacity: 0.9;
font-size: 14px;
}
.main-content-seat {
display: flex;
min-height: calc(100vh - 80px);
}
.seat-area {
flex: 1;
padding: 30px;
text-align: center;
}
.stage {
text-align: center;
padding: 15px;
background-color: #1867b0;
color: white;
margin-bottom: 30px;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
}
.legend {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
font-size: 14px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-seat-svg {
width: 24px;
height: 24px;
margin-right: 4px;
}
.seat-grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 8px;
margin: 0 auto;
padding: 20px;
background-color: #fafafa;
border-radius: 8px;
border: 1px solid #e0e0e0;
width: fit-content;
justify-self: center;
}
.seat {
width: 60px;
height: 60px;
border-radius: 4px;
border: 1px solid transparent;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.seat-icon {
width: 40px;
height: 40px;
margin-bottom: 2px;
}
.seat-number {
font-size: 10px;
font-weight: 600;
text-align: center;
}
.seat-back, .seat-cushion, .seat-armrest {
transition: all 0.2s ease;
}
.seat.available .seat-back {
fill: #f8f9fa;
stroke: #dee2e6;
stroke-width: 1;
}
.seat.available .seat-cushion {
fill: #ffffff;
stroke: #dee2e6;
stroke-width: 1;
}
.seat.available .seat-armrest {
fill: #e9ecef;
stroke: #dee2e6;
stroke-width: 1;
}
.seat.available .seat-number {
color: #666;
}
.seat.available:hover .seat-back {
fill: #e3f2fd;
stroke: #1867b0;
}
.seat.available:hover .seat-cushion {
fill: #f0f8ff;
stroke: #1867b0;
}
.seat.available:hover .seat-armrest {
fill: #e3f2fd;
stroke: #1867b0;
}
.seat.available:hover .seat-number {
color: #1867b0;
}
.seat.occupied .seat-back {
fill: #adb5bd;
stroke: #6c757d;
stroke-width: 1;
}
.seat.occupied .seat-cushion {
fill: #d0d0d0;
stroke: #6c757d;
stroke-width: 1;
}
.seat.occupied .seat-armrest {
fill: #adb5bd;
stroke: #6c757d;
stroke-width: 1;
}
.seat.occupied .seat-number {
color: #666;
}
.seat.occupied {
cursor: not-allowed;
}
.seat.selected .seat-back {
fill: #0d47a1;
stroke: #1867b0;
stroke-width: 2;
}
.seat.selected .seat-cushion {
fill: #1867b0;
stroke: #0d47a1;
stroke-width: 2;
}
.seat.selected .seat-armrest {
fill: #0d47a1;
stroke: #1867b0;
stroke-width: 2;
}
.seat.selected .seat-number {
/*color: white;*/
font-weight: 700;
}
.seat.my-seat .seat-back {
fill: #c62828;
stroke: #ff3742;
stroke-width: 2;
}
.seat.my-seat .seat-cushion {
fill: #ff4757;
stroke: #c62828;
stroke-width: 2;
}
.seat.my-seat .seat-armrest {
fill: #c62828;
stroke: #ff3742;
stroke-width: 2;
}
.seat.my-seat .seat-number {
/*color: white;*/
font-weight: 700;
}
.seat.disabled .seat-back {
fill: #f5f5f5;
stroke: #cccccc;
stroke-width: 1;
}
.seat.disabled .seat-cushion {
fill: #f0f0f0;
stroke: #cccccc;
stroke-width: 1;
}
.seat.disabled .seat-armrest {
fill: #f5f5f5;
stroke: #cccccc;
stroke-width: 1;
}
.seat.disabled .seat-number {
color: #999;
}
.seat.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.sidebar-seat {
width: 300px;
background-color: #fafafa;
border-left: 1px solid #e0e0e0;
}
.section {
background: white;
padding: 20px;
margin-bottom: 20px;
}
.section h3 {
color: #333;
margin-bottom: 15px;
font-size: 16px;
font-weight: 600;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #1867b0;
}
.btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: 10px;
}
.btn-primary {
background-color: #1867b0;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #155a8a;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #5a6268;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.my-booking {
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #1867b0;
}
.my-booking p {
margin-bottom: 8px;
font-size: 14px;
color: #666;
}
.my-booking strong {
color: #333;
}
.alert {
padding: 12px;
border-radius: 4px;
margin-bottom: 15px;
font-size: 14px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
@media (max-width: 768px) {
.main-content-seat {
flex-direction: column;
}
.sidebar-seat {
width: 100%;
border-left: none;
border-top: 1px solid #e0e0e0;
}
.seat-grid {
gap: 6px;
padding: 15px;
}
.seat {
width: 40px;
height: 40px;
font-size: 11px;
}
.header-seat {
padding: 15px 20px;
}
.seat-area, .sidebar-seat {
padding: 20px;
}
}
/* 会议选择弹窗样式 */
.meeting-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.meeting-modal-content {
background: white;
border-radius: 8px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.meeting-modal h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
text-align: center;
}
.meeting-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.meeting-item {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
background: #fafafa;
}
.meeting-item:hover {
border-color: #1867b0;
background: #f0f8ff;
}
.meeting-item.selected {
border-color: #1867b0;
background: #e8f4fd;
}
.meeting-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.meeting-info {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.meeting-time {
font-size: 13px;
color: #999;
}
.modal-buttons {
display: flex;
gap: 15px;
margin-top: 25px;
justify-content: center;
}
.modal-btn {
padding: 10px 25px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.modal-btn-primary {
background-color: #1867b0;
color: white;
}
.modal-btn-primary:hover:not(:disabled) {
background-color: #155a8a;
}
.modal-btn-secondary {
background-color: #6c757d;
color: white;
}
.modal-btn-secondary:hover:not(:disabled) {
background-color: #5a6268;
}
.modal-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
<div id="app" v-cloak>
<guava>
<!-- 会议选择弹窗 -->
<div v-if="showMeetingModal" class="meeting-modal">
<div class="meeting-modal-content">
<h2>请选择会议</h2>
<div class="meeting-list">
<div
v-for="meeting in meetings"
:key="meeting.id"
:class="['meeting-item', { selected: selectedMeetingId === meeting.id }]"
@click="selectMeeting(meeting)"
>
<div class="meeting-name">{{ meeting.name }}</div>
<div class="meeting-info">地点:{{ meeting.address }}</div>
<div class="meeting-time">时间:{{ meeting.startTime }} 至 {{ meeting.endTime }}</div>
</div>
</div>
<div class="modal-buttons">
<button
class="modal-btn"
@click="showMeetingModal = false"
>
取消选择
</button>
<button
class="modal-btn modal-btn-primary"
@click="confirmMeetingSelection"
:disabled="!selectedMeetingId"
>
确认选择
</button>
</div>
</div>
</div>
<div class="container-seat">
<div class="header-seat" v-if="meeting">
<h1>{{ meeting.name }}</h1>
<p>{{ meeting.startTime + ' 至 ' + meeting.endTime}}</p>
</div>
<div class="main-content-seat">
<div class="seat-area">
<!-- <div class="stage">主席台</div>-->
<div class="legend">
<div class="legend-item">
<svg class="legend-seat-svg" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#e9ecef" stroke="#dee2e6" stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#e9ecef" stroke="#dee2e6" stroke-width="1"/>
</svg>
<span>可选座位</span>
</div>
<div class="legend-item">
<svg class="legend-seat-svg" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#adb5bd" stroke="#6c757d" stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#d0d0d0" stroke="#6c757d" stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#adb5bd" stroke="#6c757d" stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#adb5bd" stroke="#6c757d" stroke-width="1"/>
</svg>
<span>已被占用</span>
</div>
<div class="legend-item">
<svg class="legend-seat-svg" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#0d47a1" stroke="#1867b0" stroke-width="2"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#1867b0" stroke="#0d47a1" stroke-width="2"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#0d47a1" stroke="#1867b0" stroke-width="2"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#0d47a1" stroke="#1867b0" stroke-width="2"/>
</svg>
<span>当前选择</span>
</div>
<div class="legend-item">
<svg class="legend-seat-svg" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#c62828" stroke="#ff3742" stroke-width="2"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#ff4757" stroke="#c62828" stroke-width="2"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#c62828" stroke="#ff3742" stroke-width="2"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#c62828" stroke="#ff3742" stroke-width="2"/>
</svg>
<span>我的座位</span>
</div>
<div class="legend-item">
<svg class="legend-seat-svg" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#f5f5f5" stroke="#cccccc" stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#f0f0f0" stroke="#cccccc" stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#f5f5f5" stroke="#cccccc" stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#f5f5f5" stroke="#cccccc" stroke-width="1"/>
</svg>
<span>禁用座位</span>
</div>
</div>
<div v-if="loading" class="loading">
<p>正在加载座位信息...</p>
</div>
<div v-else class="seat-grid" :style="gridStyle">
<div
v-for="seat in seats"
:key="seat.id"
:class="getSeatClass(seat)"
class="seat"
@click="selectSeat(seat)"
:title="getSeatTitle(seat)"
>
<svg class="seat-icon" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<!-- 座位靠背 -->
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" class="seat-back"/>
<!-- 座位坐垫 -->
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" class="seat-cushion"/>
<!-- 扶手 -->
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" class="seat-armrest"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" class="seat-armrest"/>
</svg>
<span class="seat-number">{{ seat.rowNumber }}-{{ seat.colNumber }}</span>
</div>
</div>
</div>
<div class="sidebar-seat">
<div class="section">
<h3>操作列</h3>
<button
class="btn btn-primary mt10"
@click="showMeetingModal = true"
>
切换会议
</button>
</div>
<div class="section">
<h3>座位选择</h3>
<div v-if="selectedSeat" class="my-booking" style="border-left-color: #28a745;">
<p><strong>选中座位:</strong> {{ selectedSeat.seatNumber }}</p>
<p><strong>位置:</strong> 第{{ selectedSeat.rowNumber }}排第{{ selectedSeat.colNumber }}列</p>
</div>
<div v-else class="my-booking">
<p style="color: #999;">请点击座位进行选择</p>
</div>
<div v-if="alertMessage" :class="'alert alert-' + alertType">
{{ alertMessage }}
</div>
<button
class="btn btn-primary mt10"
@click="confirmSelection"
:disabled="!selectedSeat || submitting"
>
{{ submitting ? '提交中...' : '提交选座' }}
</button>
</div>
<div class="section">
<h3>我的座位</h3>
<div v-if="myCurrentSeat" class="my-booking">
<p><strong>座位号:</strong> {{ myCurrentSeat.rowNumber }}-{{ myCurrentSeat.colNumber }} </p>
<p><strong>位置:</strong> 第{{ myCurrentSeat.rowNumber }}排第{{ myCurrentSeat.colNumber }}列</p>
<p><strong>选择时间:</strong> {{ formatTime(myCurrentSeat.selectTime) }}</p>
</div>
<div v-else class="my-booking">
<p style="color: #999;">暂未选择座位</p>
</div>
</div>
</div>
</div>
</div>
</guava>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
meetings: [], // 会议列表
selectedMeetingId: null, // 选中的会议ID
showMeetingModal: true, // 显示会议选择弹窗
meeting: null,
seats: [],
selectedSeat: null,
myCurrentSeat: null,
loading: false,
submitting: false,
alertMessage: '',
alertType: 'success',
seatConfig: {
rows: 8,
cols: 12
},
}
},
mounted() {
this.loadMeetings();
},
computed: {
gridStyle() {
return {
'grid-template-columns': 'repeat(' + this.seatConfig.cols + ', 1fr)'
}
}
},
methods: {
// 加载会议列表
loadMeetings() {
$.post('/platform/guildHall/meeting/selectList').then(res => {
if (res.code === 0) {
this.meetings = res.data
}
})
},
// 选择会议
selectMeeting(meeting) {
this.selectedMeetingId = meeting.id;
},
// 确认会议选择
confirmMeetingSelection() {
if (!this.selectedMeetingId) {
this.$message.error('请选择一个会议');
return;
}
const selectedMeeting = this.meetings.find(m => m.id === this.selectedMeetingId);
if (selectedMeeting) {
this.meeting = selectedMeeting
this.loadSeats();
}
},
// 查询选座情况
async loadSeats() {
if (!this.meeting.id) {
return;
}
this.loading = true;
const {code,data,msg} = await $.post('/platform/guildHall/selectSeat/loadSeats', {meetingId: this.meeting.id})
if (code === 0) {
if(data.length === 0) {
this.$message.warning('该会议暂未设置座位')
return
}
this.seats = data;
this.myCurrentSeat = this.seats.find(seat => seat.isMine);
// 根据现有数据计算行列数
const maxRow = Math.max(...this.seats.map(s => parseInt(s.rowNumber)))
const maxCol = Math.max(...this.seats.map(s => parseInt(s.colNumber)))
this.seatConfig.rows = maxRow
this.seatConfig.cols = maxCol
}
this.loading = false
this.showMeetingModal = false;
},
selectSeat(seat) {
if (!seat.enable) {
this.$message.error('该座位已被禁用');
return;
}
if (seat.isOccupied && !seat.isMine) {
this.$message.error('该座位已被占用');
return;
}
if (seat.isMine) {
this.$message.success('这是您当前的座位');
return;
}
this.selectedSeat = seat;
this.$set(this.selectedSeat, 'seatNumber', seat.rowNumber + '-' + seat.colNumber)
},
async confirmSelection() {
if (!this.selectedSeat) {
this.$message.error('请先选择座位');
return;
}
this.submitting = true;
const {code,data,msg} = await $.post('/platform/guildHall/selectSeat/doSelect', {
meetingId: this.meeting.id,
seatNumber: this.selectedSeat.seatNumber,
rowNumber: this.selectedSeat.rowNumber,
colNumber: this.selectedSeat.colNumber
})
this.submitting = false
if (code === 0) {
this.$message.success("选座成功")
this.loadSeats();
}
},
getSeatClass(seat) {
if (!seat.enable) return 'disabled';
if (seat.isMine) return 'my-seat';
if (seat === this.selectedSeat) return 'selected';
if (seat.isOccupied) return 'occupied';
return 'available';
},
getSeatTitle(seat) {
if (seat.isMine) return '我的座位';
if (seat === this.selectedSeat) return '当前选择';
if (seat.status === 1) return '已被占用';
return '点击选择此座位';
},
formatTime(time) {
if (!time) return '';
return new Date(time).toLocaleString('zh-CN');
},
showAlert(message, type) {
this.alertMessage = message;
this.alertType = type;
setTimeout(() => {
this.alertMessage = '';
}, 3000);
}
}
})
</script>
<!--#
}
#-->
@@ -234,6 +234,7 @@ const apply = {
selectTime(item) {
if((item.dateStr + ' ' + item.startTime + '-' + item.endTime) !== (this.selected.dateStr + ' ' + this.selected.startTime + '-' + this.selected.endTime)) {
this.$set(this.selected, 'day', item.dateStr)
this.$set(this.selected, "dateStr", item.dateStr)
this.$set(this.selected, 'startTime', item.startTime)
this.$set(this.selected, 'endTime', item.endTime)
} else {
@@ -0,0 +1,908 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
*{
box-sizing: border-box;
}
.mobile-seat-container {
background: #f7f8fa;
min-height: 100vh;
padding-bottom: 160px;
}
.mobile-header {
background: white;
padding: 16px;
border-bottom: 1px solid #ebedf0;
/*position: sticky;*/
/*top: 0;*/
z-index: 10;
}
.mobile-header .meeting-title {
font-size: 18px;
font-weight: 600;
color: #323233;
margin-bottom: 4px;
}
.mobile-header .meeting-time {
font-size: 14px;
color: #969799;
}
.mobile-stage {
text-align: center;
padding: 8px;
background: transparent;
margin: 16px;
border-radius: 16px;
position: relative;
overflow: hidden;
}
.stage-svg {
width: 100%;
height: 80px;
margin-bottom: 8px;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
}
.stage-text {
font-size: 18px;
font-weight: 700;
color: #1565c0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
background: linear-gradient(45deg, #1976d2, #42a5f5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-top: -4px;
}
.mobile-legend {
display: flex;
justify-content: space-around;
padding: 12px 16px;
background: white;
margin: 0 16px 16px;
border-radius: 8px;
font-size: 12px;
}
.mobile-legend-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.mobile-legend-seat {
width: 20px;
height: 20px;
}
.mobile-seat-area {
padding: 0;
}
.zoom-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
padding: 8px 16px;
margin-bottom: 8px;
/*background: rgba(128, 128, 128, 0.1);*/
border-radius: 8px;
}
.zoom-controls .van-button {
background: rgba(128, 128, 128, 0.2) !important;
border: 1px solid rgba(128, 128, 128, 0.3) !important;
color: #666 !important;
font-size: 11px;
padding: 2px 6px !important;
height: 24px !important;
min-width: auto !important;
}
.zoom-controls .van-button:disabled {
background: rgba(128, 128, 128, 0.1) !important;
color: #ccc !important;
}
.zoom-level {
display: flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-weight: 500;
color: #666;
padding: 3px 6px;
background: rgba(128, 128, 128, 0.15);
border-radius: 6px;
min-width: 45px;
justify-content: center;
}
.seat-grid-container {
width: 100%;
min-height: 300px;
position: relative;
padding: 16px;
}
.mobile-seat-grid {
display: grid;
/*grid-template-columns: repeat(10, 1fr);*/
gap: 4px;
background: white;
padding: 12px;
border-radius: 12px;
width: 100%;
user-select: none;
-webkit-user-select: none;
overflow: auto;
max-height: 60vh;
-webkit-overflow-scrolling: touch;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
.mobile-seat-grid::-webkit-scrollbar {
display: none;
}
.mobile-seat {
width: 32px;
height: 32px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
touch-action: manipulation;
transform-origin: center;
}
.mobile-seat-icon {
width: 24px;
height: 24px;
margin-bottom: 1px;
}
.mobile-seat-number {
font-size: 8px;
font-weight: 600;
text-align: center;
line-height: 1;
}
/* 座位状态样式 */
.mobile-seat.available .seat-back {
fill: #f8f9fa;
stroke: #dee2e6;
stroke-width: 1;
}
.mobile-seat.available .seat-cushion {
fill: #ffffff;
stroke: #dee2e6;
stroke-width: 1;
}
.mobile-seat.available .seat-armrest {
fill: #e9ecef;
stroke: #dee2e6;
stroke-width: 1;
}
.mobile-seat.available .mobile-seat-number {
color: #666;
}
.mobile-seat.available:active .seat-back {
fill: #e3f2fd;
stroke: #1867b0;
}
.mobile-seat.available:active .seat-cushion {
fill: #f0f8ff;
stroke: #1867b0;
}
.mobile-seat.available:active .seat-armrest {
fill: #e3f2fd;
stroke: #1867b0;
}
.mobile-seat.occupied .seat-back {
fill: #adb5bd;
stroke: #6c757d;
stroke-width: 1;
}
.mobile-seat.occupied .seat-cushion {
fill: #d0d0d0;
stroke: #6c757d;
stroke-width: 1;
}
.mobile-seat.occupied .seat-armrest {
fill: #adb5bd;
stroke: #6c757d;
stroke-width: 1;
}
.mobile-seat.occupied .mobile-seat-number {
color: #666;
}
.mobile-seat.occupied {
cursor: not-allowed;
}
.mobile-seat.selected .seat-back {
fill: #0d47a1;
stroke: #1867b0;
stroke-width: 2;
}
.mobile-seat.selected .seat-cushion {
fill: #1867b0;
stroke: #0d47a1;
stroke-width: 2;
}
.mobile-seat.selected .seat-armrest {
fill: #0d47a1;
stroke: #1867b0;
stroke-width: 2;
}
.mobile-seat.selected .mobile-seat-number {
font-weight: 700;
}
.mobile-seat.my-seat .seat-back {
fill: #c62828;
stroke: #ff3742;
stroke-width: 2;
}
.mobile-seat.my-seat .seat-cushion {
fill: #ff4757;
stroke: #c62828;
stroke-width: 2;
}
.mobile-seat.my-seat .seat-armrest {
fill: #c62828;
stroke: #ff3742;
stroke-width: 2;
}
.mobile-seat.my-seat .mobile-seat-number {
font-weight: 700;
}
.mobile-seat.disabled .seat-back {
fill: #f5f5f5;
stroke: #cccccc;
stroke-width: 1;
}
.mobile-seat.disabled .seat-cushion {
fill: #f0f0f0;
stroke: #cccccc;
stroke-width: 1;
}
.mobile-seat.disabled .seat-armrest {
fill: #f5f5f5;
stroke: #cccccc;
stroke-width: 1;
}
.mobile-seat.disabled .mobile-seat-number {
color: #999;
}
.mobile-seat.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.mobile-bottom-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #ebedf0;
padding: 16px;
z-index: 200;
}
.mobile-selected-info {
background: #f0f8ff;
border: 1px solid #1989fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
font-size: 14px;
}
.mobile-selected-info .seat-info {
color: #1989fa;
font-weight: 600;
}
.mobile-my-seat-info {
background: #fff2f0;
border: 1px solid #ff4757;
border-radius: 8px;
padding: 12px;
margin-top: 16px;
font-size: 14px;
}
.mobile-my-seat-info .seat-info {
color: #ff4757;
font-weight: 600;
}
.mobile-loading {
text-align: center;
padding: 60px 20px;
color: #969799;
}
/* 会议选择弹窗样式 */
.meeting-modal-content {
max-height: 70vh;
overflow-y: auto;
}
.meeting-modal-content .van-nav-bar__text {
display: none;
}
.meeting-item {
border: 1px solid #ebedf0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
background: white;
transition: all 0.2s ease;
}
.meeting-item:active {
background: #f0f8ff;
border-color: #1989fa;
}
.meeting-item.selected {
background: #f0f8ff;
border-color: #1989fa;
}
.meeting-name {
font-size: 16px;
font-weight: 600;
color: #323233;
margin-bottom: 8px;
}
.meeting-info {
font-size: 14px;
color: #646566;
margin-bottom: 4px;
}
.meeting-time {
font-size: 12px;
color: #969799;
}
@media (max-width: 375px) {
.mobile-seat-grid {
grid-template-columns: repeat(8, 28px);
gap: 3px;
padding: 10px;
}
.mobile-seat {
width: 28px;
height: 28px;
}
.mobile-seat-icon {
width: 20px;
height: 20px;
}
.mobile-seat-number {
font-size: 8px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="会议选座" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<!-- 会议选择弹窗 -->
<van-popup v-model="showMeetingModal" position="bottom" :close-on-click-overlay="false">
<div class="meeting-modal-content">
<van-nav-bar title="选择会议"></van-nav-bar>
<div style="padding: 16px;">
<div v-for="meeting in meetings" :key="meeting.id"
:class="['meeting-item', { selected: selectedMeetingId === meeting.id }]"
@click="selectMeeting(meeting)">
<div class="meeting-name">{{ meeting.name }}</div>
<div class="meeting-info">地点:{{ meeting.address }}</div>
<div class="meeting-time">时间:{{ meeting.startTime }} 至 {{ meeting.endTime }}</div>
</div>
<div style="display: flex; gap: 22px">
<van-button block @click="showMeetingModal = false" style="margin-top: 16px;">
取消选择
</van-button>
<van-button block type="info" :disabled="!selectedMeetingId"
@click="confirmMeetingSelection" style="margin-top: 16px;">
确认选择
</van-button>
</div>
</div>
</div>
</van-popup>
<div class="mobile-seat-container">
<!-- 头部信息 -->
<div class="mobile-header" v-if="meeting">
<div class="meeting-title">{{ meeting.name }}</div>
<div class="meeting-time">{{ meeting.startTime }} 至 {{ meeting.endTime }}</div>
</div>
<!-- 主席台 -->
<div class="mobile-stage">
<svg class="stage-svg" viewBox="0 0 320 100" xmlns="http://www.w3.org/2000/svg">
<!-- 背景装饰 -->
<rect x="0" y="0" width="320" height="100" fill="url(#bgGradient)" rx="12" ry="12"/>
<!-- 主舞台基座 -->
<ellipse cx="160" cy="75" rx="140" ry="15" fill="url(#baseGradient)" opacity="0.8"/>
<!-- 主舞台平台 -->
<path d="M 30 45 Q 30 35 40 35 L 280 35 Q 290 35 290 45 L 285 65 Q 285 70 280 70 L 40 70 Q 35 70 35 65 Z"
fill="url(#stageGradient)" stroke="url(#stageStroke)" stroke-width="2"/>
<!-- 台阶层次 -->
<path d="M 45 50 Q 45 45 50 45 L 270 45 Q 275 45 275 50 L 272 62 Q 272 65 270 65 L 50 65 Q 47 65 47 62 Z"
fill="url(#stepGradient)" stroke="#1976d2" stroke-width="1"/>
<!-- 装饰花纹 -->
<path d="M 60 52 Q 65 50 70 52 Q 75 54 80 52" stroke="#ffffff" stroke-width="1.5" fill="none" opacity="0.7"/>
<path d="M 240 52 Q 245 50 250 52 Q 255 54 260 52" stroke="#ffffff" stroke-width="1.5" fill="none" opacity="0.7"/>
<!-- 中央装饰 -->
<circle cx="160" cy="52" r="8" fill="url(#centerGradient)" stroke="#ffffff" stroke-width="1" opacity="0.9"/>
<circle cx="160" cy="52" r="4" fill="#ffd700" opacity="0.8"/>
<!-- 聚光灯光束 -->
<path d="M 80 20 L 85 35 L 75 35 Z" fill="url(#lightGradient)" opacity="0.6"/>
<path d="M 160 15 L 167 35 L 153 35 Z" fill="url(#lightGradient)" opacity="0.8"/>
<path d="M 240 20 L 245 35 L 235 35 Z" fill="url(#lightGradient)" opacity="0.6"/>
<!-- 聚光灯 -->
<circle cx="80" cy="18" r="4" fill="#ffd700" stroke="#ffb300" stroke-width="1"/>
<circle cx="160" cy="13" r="5" fill="#ffd700" stroke="#ffb300" stroke-width="1"/>
<circle cx="240" cy="18" r="4" fill="#ffd700" stroke="#ffb300" stroke-width="1"/>
<!-- 星光效果 -->
<g opacity="0.8">
<path d="M 100 25 L 102 30 L 107 30 L 103 33 L 105 38 L 100 35 L 95 38 L 97 33 L 93 30 L 98 30 Z" fill="#ffd700"/>
<path d="M 220 28 L 222 32 L 226 32 L 223 35 L 225 39 L 220 36 L 215 39 L 217 35 L 214 32 L 218 32 Z" fill="#ffd700"/>
</g>
<!-- 渐变定义 -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#e3f2fd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#bbdefb;stop-opacity:1" />
</linearGradient>
<linearGradient id="stageGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1e88e5;stop-opacity:1" />
<stop offset="30%" style="stop-color:#1976d2;stop-opacity:1" />
<stop offset="70%" style="stop-color:#1565c0;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0d47a1;stop-opacity:1" />
</linearGradient>
<linearGradient id="stageStroke" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#42a5f5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1565c0;stop-opacity:1" />
</linearGradient>
<linearGradient id="stepGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#42a5f5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976d2;stop-opacity:1" />
</linearGradient>
<linearGradient id="baseGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#90caf9;stop-opacity:0.6" />
<stop offset="100%" style="stop-color:#1976d2;stop-opacity:0.8" />
</linearGradient>
<radialGradient id="centerGradient" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976d2;stop-opacity:1" />
</radialGradient>
<linearGradient id="lightGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#fff9c4;stop-opacity:0.9" />
<stop offset="100%" style="stop-color:#fff59d;stop-opacity:0.3" />
</linearGradient>
</defs>
</svg>
<div class="stage-text">主席台</div>
</div>
<!-- 图例 -->
<div class="mobile-legend">
<div class="mobile-legend-item">
<svg class="mobile-legend-seat" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#f8f9fa" stroke="#dee2e6"
stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#ffffff" stroke="#dee2e6"
stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#e9ecef" stroke="#dee2e6"
stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#e9ecef" stroke="#dee2e6"
stroke-width="1"/>
</svg>
<span>可选</span>
</div>
<div class="mobile-legend-item">
<svg class="mobile-legend-seat" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#adb5bd" stroke="#6c757d"
stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#d0d0d0" stroke="#6c757d"
stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#adb5bd" stroke="#6c757d"
stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#adb5bd" stroke="#6c757d"
stroke-width="1"/>
</svg>
<span>占用</span>
</div>
<div class="mobile-legend-item">
<svg class="mobile-legend-seat" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#0d47a1" stroke="#1867b0"
stroke-width="2"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#1867b0" stroke="#0d47a1"
stroke-width="2"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#0d47a1" stroke="#1867b0"
stroke-width="2"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#0d47a1" stroke="#1867b0"
stroke-width="2"/>
</svg>
<span>选中</span>
</div>
<div class="mobile-legend-item">
<svg class="mobile-legend-seat" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#c62828" stroke="#ff3742"
stroke-width="2"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#ff4757" stroke="#c62828"
stroke-width="2"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#c62828" stroke="#ff3742"
stroke-width="2"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#c62828" stroke="#ff3742"
stroke-width="2"/>
</svg>
<span>我的</span>
</div>
<div class="mobile-legend-item">
<svg class="mobile-legend-seat" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" fill="#f5f5f5" stroke="#cccccc"
stroke-width="1"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" fill="#f0f0f0" stroke="#cccccc"
stroke-width="1"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" fill="#f5f5f5" stroke="#cccccc"
stroke-width="1"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" fill="#f5f5f5" stroke="#cccccc"
stroke-width="1"/>
</svg>
<span>禁用</span>
</div>
</div>
<!-- 座位区域 -->
<div class="mobile-seat-area">
<van-loading v-if="loading" type="spinner" color="#1989fa" class="mobile-loading">
正在加载座位信息...
</van-loading>
<div v-else class="seat-grid-container">
<div class="mobile-seat-grid" id="seatGrid" :style="gridStyle">
<div v-for="seat in seats" :key="seat.id"
:class="getSeatClass(seat)"
class="mobile-seat"
@click="selectSeat(seat)">
<svg class="mobile-seat-icon" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="5" width="34" height="20" rx="3" ry="3" class="seat-back"/>
<rect x="5" y="22" width="40" height="18" rx="4" ry="4" class="seat-cushion"/>
<rect x="2" y="20" width="6" height="15" rx="2" ry="2" class="seat-armrest"/>
<rect x="42" y="20" width="6" height="15" rx="2" ry="2" class="seat-armrest"/>
</svg>
<span class="mobile-seat-number">{{ seat.rowNumber }}-{{ seat.colNumber }}</span>
</div>
</div>
</div>
</div>
<!-- 缩放控制按钮 -->
<div class="zoom-controls">
<van-button type="default" size="mini" @click="zoomOut" :disabled="currentScale <= 0.5">
<van-icon name="minus" size="12" />
缩小
</van-button>
<div class="zoom-level">
<van-icon name="search" size="12" />
{{ Math.round(currentScale * 100) }}%
</div>
<van-button type="default" size="mini" @click="resetZoom">
<van-icon name="replay" size="12" />
重置
</van-button>
<van-button type="default" size="mini" @click="zoomIn" :disabled="currentScale >= 3">
<van-icon name="plus" size="12" />
放大
</van-button>
</div>
<!-- 我的座位信息 -->
<div v-if="myCurrentSeat" class="mobile-my-seat-info" style="margin: 16px;">
<div style="margin-bottom: 4px;">我的座位</div>
<div class="seat-info">{{ myCurrentSeat.rowNumber }}-{{ myCurrentSeat.colNumber }} (第{{ myCurrentSeat.rowNumber }}排第{{
myCurrentSeat.colNumber }}列)
</div>
</div>
</div>
<!-- 底部操作面板 -->
<div class="mobile-bottom-panel">
<div v-if="selectedSeat" class="mobile-selected-info">
<div style="margin-bottom: 4px;">已选择座位</div>
<div class="seat-info">{{ selectedSeat.rowNumber }}-{{ selectedSeat.colNumber }} (第{{ selectedSeat.rowNumber }}排第{{
selectedSeat.colNumber }}列)
</div>
</div>
<div style="display: flex; gap: 22px">
<van-button type="info" block @click="showMeetingModal = true">
切换会议
</van-button>
<van-button type="info" block :disabled="!selectedSeat || submitting"
:loading="submitting" @click="confirmSelection">
{{ submitting ? '提交中...' : '确认选座' }}
</van-button>
</div>
</div>
</div>
<script>
const vue = new Vue({
el: '#app',
data() {
return {
meetings: [], // 会议列表
selectedMeetingId: null, // 选中的会议ID
showMeetingModal: true, // 显示会议选择弹窗
meeting: null,
seats: [],
selectedSeat: null,
myCurrentSeat: null,
loading: false,
submitting: false,
currentScale: 1, // 当前缩放比例
seatConfig: {
rows: 8,
cols: 12
},
}
},
mounted() {
this.loadMeetings();
this.$nextTick(() => {
this.initPinchZoom();
});
},
computed: {
gridStyle() {
return {
'grid-template-columns': 'repeat(' + this.seatConfig.cols + ', 1fr)'
}
}
},
methods: {
// 加载会议列表
loadMeetings() {
$.post('/platform/guildHall/meeting/selectList').then(res => {
if (res.code === 0) {
this.meetings = res.data
}
})
},
// 选择会议
selectMeeting(meeting) {
this.selectedMeetingId = meeting.id;
},
// 确认会议选择
confirmMeetingSelection() {
if (!this.selectedMeetingId) {
this.$toast('请选择一个会议');
return;
}
const selectedMeeting = this.meetings.find(m => m.id === this.selectedMeetingId);
if (selectedMeeting) {
this.meeting = selectedMeeting
this.loadSeats();
}
},
// 查询选座情况
async loadSeats() {
if (!this.meeting.id) {
return;
}
this.loading = true;
const {
code,
data,
msg
} = await $.post('/platform/guildHall/selectSeat/loadSeats', {meetingId: this.meeting.id})
if (code === 0) {
this.seats = data;
this.myCurrentSeat = this.seats.find(seat => seat.isMine);
// 根据现有数据计算行列数
const maxRow = Math.max(...this.seats.map(s => parseInt(s.rowNumber)))
const maxCol = Math.max(...this.seats.map(s => parseInt(s.colNumber)))
this.seatConfig.rows = maxRow
this.seatConfig.cols = maxCol
}
this.loading = false
this.showMeetingModal = false;
},
selectSeat(seat) {
if (!seat.enable) {
this.$toast('该座位已被禁用');
return;
}
if (seat.isOccupied && !seat.isMine) {
this.$toast('该座位已被占用');
return;
}
if (seat.isMine) {
this.$toast('这是您当前的座位');
return;
}
this.selectedSeat = seat;
// 添加触觉反馈
if (navigator.vibrate) {
navigator.vibrate(50);
}
},
async confirmSelection() {
if (!this.selectedSeat) {
this.$toast('请先选择座位');
return;
}
this.submitting = true;
const {code, data, msg} = await $.post('/platform/guildHall/selectSeat/doSelect', {
meetingId: this.meeting.id,
rowNumber: this.selectedSeat.rowNumber,
colNumber: this.selectedSeat.colNumber
})
this.submitting = false
if (code === 0) {
this.$toast.success("选座成功")
this.selectedSeat = null;
this.loadSeats();
} else {
this.$toast.fail(msg || '选座失败');
}
},
getSeatClass(seat) {
if (!seat.enable) {
return 'disabled';
}
if (seat.isMine) return 'my-seat';
if (seat === this.selectedSeat) return 'selected';
if (seat.isOccupied) return 'occupied';
return 'available';
},
getSeatTitle(seat) {
if (seat.isMine) return '我的座位';
if (seat === this.selectedSeat) return '当前选择';
if (seat.status === 1) return '已被占用';
return '点击选择此座位';
},
formatTime(time) {
if (!time) return '';
return new Date(time).toLocaleString('zh-CN');
},
// 放大
zoomIn() {
if (this.currentScale < 3) {
this.currentScale = Math.min(3, this.currentScale + 0.2);
this.applyZoom();
}
},
// 缩小
zoomOut() {
if (this.currentScale > 0.5) {
this.currentScale = Math.max(0.5, this.currentScale - 0.2);
this.applyZoom();
}
},
// 重置缩放
resetZoom() {
this.currentScale = 1;
this.applyZoom();
},
// 应用缩放
applyZoom() {
const seats = document.querySelectorAll('.mobile-seat');
const seatGrid = document.querySelector('.mobile-seat-grid');
seats.forEach(seat => {
seat.style.transform = 'scale(' + this.currentScale + ')';
seat.style.transformOrigin = 'center center';
seat.style.transition = 'transform 0.2s ease';
});
// 动态调整网格间距
if (seatGrid) {
const baseGap = 4;
const adjustedGap = baseGap * this.currentScale * 5;
seatGrid.style.gap = adjustedGap + 'px';
}
},
// 初始化双指缩放功能(已替换为按钮控制)
initPinchZoom() {
// 双指缩放功能已被按钮控制替代,保留此方法以避免错误
console.log('双指缩放功能已替换为按钮控制');
}
}
})
</script>
<!--#
}
#-->