主席信箱

This commit is contained in:
=
2026-04-21 15:27:15 +08:00
parent c98c309d82
commit 83f84d78d9
17 changed files with 2668 additions and 0 deletions
@@ -0,0 +1,30 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.chairmanMailbox.models.ChairmanMailbox;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@IocBean
@At("/platform/chairmanMailbox/common")
@Ok("json:full")
@Api("主席信箱公共接口")
public class ChairmanMailboxCommonController {
@Inject
private Dao dao;
@At
@SaCheckLogin
@ApiOperation("获取详情")
public Result info(@Param("id") String id) {
return Result.success(dao.fetch(ChairmanMailbox.class, id));
}
}
@@ -0,0 +1,20 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import io.swagger.annotations.Api;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@IocBean
@At("/platform/chairmanMailbox")
@Api("主席信箱入口")
public class ChairmanMailboxIndexController {
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/chairmanMailbox/index.html")
@SaCheckLogin
public void h5() {
}
}
@@ -0,0 +1,53 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.chairmanMailbox.service.ChairmanMailboxService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@IocBean
@At("/platform/chairmanMailbox/mine")
@Ok("json:full")
@Api("主席信箱-我的")
public class ChairmanMailboxMineController {
@Inject
private ChairmanMailboxService chairmanMailboxService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/chairmanMailbox/mine/index.html")
@SaCheckPermission("chairmanMailbox.mine")
public void index() {
}
@At
@Ok("beetl:/platform/zhghh5/democratic/chairmanMailbox/mine/index.html")
@SaCheckPermission("h5.chairmanMailbox")
public void h5() {
}
@At
@SaCheckPermission(value = {"chairmanMailbox.mine", "h5.chairmanMailbox"}, mode = SaMode.OR)
public Result pageData(Integer pageNumber, Integer pageSize, String searchKeyword) {
return Result.success(chairmanMailboxService.pageMyData(pageNumber, pageSize, searchKeyword));
}
@At
@SaCheckPermission(value = {"chairmanMailbox.mine", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("删除")
@SLog(tag = "主席信箱-我的", msg = "删除")
public Result delete(@Param("id") String id) {
chairmanMailboxService.delete(id);
return Result.success();
}
}
@@ -0,0 +1,70 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.chairmanMailbox.models.ChairmanMailbox;
import com.budwk.app.zhgh.democratic.chairmanMailbox.service.ChairmanMailboxService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@IocBean
@At("/platform/chairmanMailbox/query")
@Ok("json:full")
@Api("主席信箱管理")
public class ChairmanMailboxQueryController {
@Inject
private ChairmanMailboxService chairmanMailboxService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/chairmanMailbox/query/index.html")
@SaCheckPermission("chairmanMailbox.query")
public void index() {
}
@At
@Ok("beetl:/platform/zhghh5/democratic/chairmanMailbox/query/index.html")
@SaCheckPermission("h5.chairmanMailbox")
public void h5() {
}
@At
@SaCheckPermission(value = {"chairmanMailbox.query", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("分页查询")
public Result pageData(Integer pageNumber, Integer pageSize, String searchKeyword, Integer isReply, String timeRange) {
return Result.success(chairmanMailboxService.pageQueryData(pageNumber, pageSize, searchKeyword, isReply, timeRange));
}
@At
@SaCheckPermission(value = {"chairmanMailbox.query", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("统计数据")
public Result getStats() {
return Result.success(chairmanMailboxService.getQueryStats());
}
@At
@SaCheckPermission(value = {"chairmanMailbox.query", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("回复信件")
@SLog(tag = "主席信箱-管理", msg = "回复信件")
public Result reply(@Param("data") ChairmanMailbox mailbox) {
chairmanMailboxService.reply(mailbox);
return Result.success();
}
@At
@SaCheckPermission(value = {"chairmanMailbox.query", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("删除")
public Result delete(@Param("id") String id) {
chairmanMailboxService.delete(id);
return Result.success();
}
}
@@ -0,0 +1,74 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.chairmanMailbox.models.ChairmanMailbox;
import com.budwk.app.zhgh.democratic.chairmanMailbox.service.ChairmanMailboxService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@IocBean
@At("/platform/chairmanMailbox/write")
@Ok("json:full")
@Api("主席信箱填写")
public class ChairmanMailboxWriteController {
@Inject
private Dao dao;
@Inject
private ChairmanMailboxService chairmanMailboxService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/chairmanMailbox/write/index.html")
@SaCheckPermission("chairmanMailbox.write")
public void index() {
}
@At
@Ok("beetl:/platform/zhghh5/democratic/chairmanMailbox/write/index.html")
@SaCheckPermission("h5.chairmanMailbox")
public void h5() {
}
@At
@SaCheckPermission(value = {"chairmanMailbox.write", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("获取当前登录人默认信息")
public Result currentUserInfo() {
return Result.success(chairmanMailboxService.getCurrentUserMailboxInfo());
}
@At
@SaCheckPermission(value = {"chairmanMailbox.write", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("保存草稿")
@SLog(tag = "主席信箱-填写", msg = "保存草稿")
public Result save(@Param("data") ChairmanMailbox mailbox) {
dao.insertOrUpdate(chairmanMailboxService.prepareSubmissionData(mailbox, 0));
return Result.success();
}
@At
@SaCheckPermission(value = {"chairmanMailbox.write", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("提交")
@SLog(tag = "主席信箱-填写", msg = "提交")
public Result submit(@Param("data") ChairmanMailbox mailbox) {
dao.insertOrUpdate(chairmanMailboxService.prepareSubmissionData(mailbox, 1));
return Result.success();
}
@At
@SaCheckPermission(value = {"chairmanMailbox.write", "h5.chairmanMailbox"}, mode = SaMode.OR)
@ApiOperation("获取详情")
public Result info(@Param("id") String id) {
return Result.success(dao.fetch(ChairmanMailbox.class, id));
}
}
@@ -0,0 +1,124 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.models;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
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.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Table("chairman_mailbox")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("主席信箱")
@Data
public class ChairmanMailbox extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("提交人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("提交人姓名")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String userName;
@Column
@Comment("提交人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String loginName;
@Column
@Comment("提交人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("提交人单位名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("提交人工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("提交人工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("提交时间")
@ColDefine(type = ColType.DATETIME)
private Date submitTime;
@Column
@Comment("标题")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String title;
@Column
@Comment("内容")
@ColDefine(type = ColType.TEXT)
private String content;
@Column
@Comment("联系方式")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String concat;
@Column
@Comment("附件")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> attachments;
@Column
@Comment("状态 0草稿 1已提交")
@ColDefine(type = ColType.INT)
@Default("1")
private Integer status;
@Column
@Comment("是否回复")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isReply;
@Column
@Comment("回复内容")
@ColDefine(type = ColType.TEXT)
private String replyContent;
@Column
@Comment("回复时间")
@ColDefine(type = ColType.DATETIME)
private Date replyTime;
@Column
@Comment("回复人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String replyUserId;
@Column
@Comment("回复人姓名")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String replyUserName;
}
@@ -0,0 +1,64 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.chairmanMailbox.models.ChairmanMailbox;
import org.nutz.lang.util.NutMap;
public interface ChairmanMailboxService extends BaseService<ChairmanMailbox> {
/**
* 获取当前登录人的默认写信信息,统一回显姓名、工号、单位、工会和联系方式。
*
* @return 当前登录人的默认写信信息
*/
ChairmanMailbox getCurrentUserMailboxInfo();
/**
* 组装信箱提交数据,统一补齐当前登录人信息、提交时间和提交状态。
*
* @param mailbox 信件数据
* @param status 状态,0表示草稿,1表示已提交
* @return 处理后的信件数据
*/
ChairmanMailbox prepareSubmissionData(ChairmanMailbox mailbox, int status);
/**
* 查询当前登录人已提交的信箱数据。
*
* @param pageNumber 页码
* @param pageSize 每页条数
* @param searchKeyword 关键字
* @return 分页数据
*/
Pagination<NutMap> pageMyData(Integer pageNumber, Integer pageSize, String searchKeyword);
/**
* 查询信箱管理列表。
*
* @param pageNumber 页码
* @param pageSize 每页条数
* @param searchKeyword 关键字
* @param isReply 回复状态
* @param timeRange 时间范围
* @return 分页数据
*/
Pagination<NutMap> pageQueryData(Integer pageNumber,
Integer pageSize,
String searchKeyword,
Integer isReply,
String timeRange);
/**
* 获取信箱管理统计数据。
*
* @return 统计结果
*/
NutMap getQueryStats();
/**
* 回复主席信箱。
*
* @param mailbox 回复内容
*/
void reply(ChairmanMailbox mailbox);
}
@@ -0,0 +1,161 @@
package com.budwk.app.zhgh.democratic.chairmanMailbox.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.chairmanMailbox.models.ChairmanMailbox;
import com.budwk.app.zhgh.democratic.chairmanMailbox.service.ChairmanMailboxService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.Date;
@IocBean(args = {"refer:dao"})
public class ChairmanMailboxServiceImpl extends BaseServiceImpl<ChairmanMailbox> implements ChairmanMailboxService {
public ChairmanMailboxServiceImpl(Dao dao) {
super(dao);
}
@Override
public ChairmanMailbox getCurrentUserMailboxInfo() {
ChairmanMailbox mailbox = new ChairmanMailbox();
View_user loginUser = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId()));
if (loginUser != null) {
mailbox.setUserId(loginUser.getId());
mailbox.setUserName(loginUser.getUsername());
mailbox.setLoginName(loginUser.getLoginname());
mailbox.setUnitId(loginUser.getUnitId());
mailbox.setUnitName(loginUser.getUnitName());
mailbox.setUnionId(loginUser.getUnionId());
mailbox.setUnionName(loginUser.getUnionName());
mailbox.setConcat(loginUser.getMobile());
}
mailbox.setAttachments(null);
return mailbox;
}
@Override
public ChairmanMailbox prepareSubmissionData(ChairmanMailbox mailbox, int status) {
ChairmanMailbox result = ObjectUtil.defaultIfNull(mailbox, new ChairmanMailbox());
View_user loginUser = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId()));
if (loginUser != null) {
result.setUserId(loginUser.getId());
result.setUserName(loginUser.getUsername());
result.setLoginName(loginUser.getLoginname());
result.setUnitId(loginUser.getUnitId());
result.setUnitName(loginUser.getUnitName());
result.setUnionId(loginUser.getUnionId());
result.setUnionName(loginUser.getUnionName());
if (StrUtil.isBlank(result.getConcat())) {
result.setConcat(loginUser.getMobile());
}
}
if (result.getSubmitTime() == null) {
result.setSubmitTime(new Date());
}
if (result.getIsReply() == null) {
result.setIsReply(false);
}
result.setStatus(status);
return result;
}
@Override
public Pagination<NutMap> pageMyData(Integer pageNumber, Integer pageSize, String searchKeyword) {
Sql sql = Sqls.create("""
SELECT id, title, content, userName, loginName, unitName, unionName, concat, submitTime, isReply, replyTime
FROM chairman_mailbox
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("createdBy", "=", SecurityUtil.getUserId());
cnd.and("status", "=", 1);
if (StrUtil.isNotBlank(searchKeyword)) {
cnd.where().andLike("title", searchKeyword);
}
cnd.desc("submitTime");
sql.setCondition(cnd);
return listPageMap(pageNumber, pageSize, sql);
}
@Override
public Pagination<NutMap> pageQueryData(Integer pageNumber,
Integer pageSize,
String searchKeyword,
Integer isReply,
String timeRange) {
Sql sql = Sqls.create("""
SELECT id, title, content, userName, loginName, unitName, unionName, concat, submitTime, isReply, replyTime, replyUserName
FROM chairman_mailbox
$condition
""");
Cnd cnd = buildQueryCondition(searchKeyword, isReply, timeRange);
cnd.desc("submitTime");
sql.setCondition(cnd);
return listPageMap(pageNumber, pageSize, sql);
}
@Override
public NutMap getQueryStats() {
return NutMap.NEW()
.addv("total", dao().count(ChairmanMailbox.class, buildQueryCondition(null, null, null)))
.addv("pending", dao().count(ChairmanMailbox.class, buildQueryCondition(null, 0, null)))
.addv("replied", dao().count(ChairmanMailbox.class, buildQueryCondition(null, 1, null)))
.addv("today", dao().count(ChairmanMailbox.class, buildQueryCondition(null, null, "today")));
}
@Override
public void reply(ChairmanMailbox mailbox) {
if (mailbox == null || StrUtil.isBlank(mailbox.getId())) {
return;
}
ChairmanMailbox data = dao().fetch(ChairmanMailbox.class, mailbox.getId());
if (data == null) {
return;
}
View_user loginUser = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId()));
data.setIsReply(true);
data.setReplyContent(mailbox.getReplyContent());
data.setReplyTime(new Date());
data.setReplyUserId(SecurityUtil.getUserId());
if (loginUser != null) {
data.setReplyUserName(loginUser.getUsername());
}
dao().updateIgnoreNull(data);
}
/**
* 统一构建信箱管理查询条件,保证手机端和PC端逻辑一致。
*/
private Cnd buildQueryCondition(String searchKeyword, Integer isReply, String timeRange) {
Cnd cnd = Cnd.NEW();
cnd.and("status", "=", 1);
if (StrUtil.isNotBlank(searchKeyword)) {
cnd.and(new org.nutz.dao.util.cri.SqlExpressionGroup()
.orLike("title", searchKeyword)
.orLike("content", searchKeyword)
.orLike("userName", searchKeyword));
}
if (isReply != null) {
cnd.and("isReply", "=", isReply == 1);
}
if (StrUtil.isNotBlank(timeRange)) {
if ("today".equals(timeRange)) {
cnd.and(new Static("DATE(submitTime) = CURDATE()"));
} else if ("week".equals(timeRange)) {
cnd.and(new Static("YEARWEEK(submitTime, 1) = YEARWEEK(CURDATE(), 1)"));
} else if ("month".equals(timeRange)) {
cnd.and(new Static("DATE_FORMAT(submitTime, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')"));
}
}
return cnd;
}
}
@@ -0,0 +1,82 @@
const CHAIRMAN_MAILBOX_INFO = {
template:
"<div>" +
" <div class='process-title'>信件信息</div>" +
" <el-descriptions :column='2' border class='flow-task-form'>" +
" <el-descriptions-item :label='labels.userName'>{{viewData.userName}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.loginName'>{{viewData.loginName}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.unitName'>{{viewData.unitName}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.unionName'>{{viewData.unionName}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.concat'>{{viewData.concat}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.submitTime'>{{viewData.submitTime}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.replyStatus'>{{getReplyText(viewData.isReply)}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.title' :span='2'>{{viewData.title}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.content' :span='2'>" +
" <span style='white-space: pre-line;'>{{viewData.content}}</span>" +
" </el-descriptions-item>" +
" <el-descriptions-item :label='labels.attachments' :span='2'>" +
" <file-preview v-if='viewData.attachments && viewData.attachments.length > 0' :files='viewData.attachments' complete_result></file-preview>" +
" <span v-else>-</span>" +
" </el-descriptions-item>" +
" </el-descriptions>" +
" <div class='mt10' v-if='viewData.isReply'>" +
" <div class='process-title'>回复内容</div>" +
" <el-descriptions :column='2' border class='flow-task-form'>" +
" <el-descriptions-item :label='labels.replyUserName'>{{viewData.replyUserName || '-'}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.replyTime'>{{viewData.replyTime || '-'}}</el-descriptions-item>" +
" <el-descriptions-item :label='labels.replyContent' :span='2'>" +
" <span style='white-space: pre-line;'>{{viewData.replyContent || '-'}}</span>" +
" </el-descriptions-item>" +
" </el-descriptions>" +
" </div>" +
" <slot></slot>" +
"</div>",
data() {
return {
labels: {
userName: '姓名',
loginName: '工号',
unitName: '单位',
unionName: '工会',
concat: '联系方式',
submitTime: '提交时间',
replyStatus: '回复状态',
title: '标题',
content: '信件内容',
attachments: '附件',
replyUserName: '回复人',
replyTime: '回复时间',
replyContent: '回复详情'
},
viewData: {},
row: null
}
},
methods: {
onOpen(row) {
this.$set(this, 'row', row)
this.getInfo()
},
getInfo() {
this.$axios.post('/platform/chairmanMailbox/common/info', {id: this.row.id}).then((res) => {
if (res.code === 0) {
var data = res.data || {}
if (data.attachments && typeof data.attachments === 'string') {
try {
data.attachments = JSON.parse(data.attachments)
} catch (e) {
data.attachments = []
}
}
if (!data.attachments) {
data.attachments = []
}
this.$set(this, 'viewData', data)
}
})
},
getReplyText(isReply) {
return isReply ? '已回复' : '待回复'
}
}
}
@@ -0,0 +1,62 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="标题">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入标题关键字" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="title" label="标题" min-width="280px"></el-table-column>
<el-table-column prop="submitTime" label="提交时间" min-width="180px"></el-table-column>
<el-table-column label="回复状态" width="120px">
<template slot-scope="{row}">
<el-tag :type="row.isReply ? 'success' : 'warning'" size="small">{{ row.isReply ? '已回复' : '待回复' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="120px">
<template slot-scope="{row}">
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<chairman-mailbox-info ref="infoRef"></chairman-mailbox-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'chairman-mailbox-info': CHAIRMAN_MAILBOX_INFO
},
methods: {
openView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,190 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never" class="mb10">
<div class="stats-panel">
<div class="stats-item">
<div class="stats-value">{{ stats.total }}</div>
<div class="stats-label">总信件数</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.pending }}</div>
<div class="stats-label">待回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.replied }}</div>
<div class="stats-label">已回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.today }}</div>
<div class="stats-label">今日新增</div>
</div>
</div>
</el-card>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="关键字">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入标题、内容或提交人" clearable></el-input>
</search-item>
<search-item label="回复状态">
<el-select v-model="pageForm.isReply" placeholder="请选择" clearable>
<el-option label="待回复" :value="0"></el-option>
<el-option label="已回复" :value="1"></el-option>
</el-select>
</search-item>
<search-item label="时间范围">
<el-select v-model="pageForm.timeRange" placeholder="请选择" clearable>
<el-option label="今日" value="today"></el-option>
<el-option label="本周" value="week"></el-option>
<el-option label="本月" value="month"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="title" label="标题" min-width="260px"></el-table-column>
<el-table-column prop="userName" label="提交人"></el-table-column>
<el-table-column prop="unitName" label="单位"></el-table-column>
<el-table-column prop="submitTime" label="提交时间" min-width="180px"></el-table-column>
<el-table-column label="回复状态" width="120px">
<template slot-scope="{row}">
<el-tag :type="row.isReply ? 'success' : 'warning'" size="small">{{ row.isReply ? '已回复' : '待回复' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="160px">
<template slot-scope="{row}">
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="!row.isReply" @click="openReply(row)" size="mini" type="success">回复</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<chairman-mailbox-info ref="infoRef">
<div v-if="showReplyForm" class="mt10">
<div class="process-title">回复信件</div>
<el-form :model="replyData" label-width="90px">
<el-form-item label="回复内容">
<el-input v-model="replyData.replyContent" type="textarea" :rows="5" maxlength="500" show-word-limit placeholder="请输入回复内容"></el-input>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()">取消</el-button>
<el-button type="primary" @click="submitReply">提交回复</el-button>
</el-row>
</div>
</chairman-mailbox-info>
</template>
</guava>
</div>
<style>
.stats-panel {
display: flex;
}
.stats-item {
flex: 1;
text-align: center;
}
.stats-value {
font-size: 24px;
font-weight: 700;
color: rgb(0, 78, 100);
}
.stats-label {
margin-top: 8px;
color: #666666;
font-size: 13px;
}
</style>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'chairman-mailbox-info': CHAIRMAN_MAILBOX_INFO
},
data() {
return {
stats: {
total: 0,
pending: 0,
replied: 0,
today: 0
},
showReplyForm: false,
replyData: {
id: '',
replyContent: ''
}
}
},
methods: {
openView(row) {
this.$set(this, 'showReplyForm', false)
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
openReply(row) {
this.$set(this, 'showReplyForm', true)
this.$set(this, 'replyData', {
id: row.id,
replyContent: row.replyContent || ''
})
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
submitReply() {
if (!this.replyData.replyContent) {
this.$message.warning('请输入回复内容')
return
}
this.$confirm('确认提交回复吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/chairmanMailbox/query/reply', {
data: JSON.stringify(this.replyData)
}).then((res) => {
if (res.code === 0) {
this.$message.success('回复成功')
this.$refs.guava.index()
this.loadStats()
this.pageData()
}
})
})
},
loadStats() {
this.$axios.post('/platform/chairmanMailbox/query/getStats').then((res) => {
if (res.code === 0) {
this.$set(this, 'stats', res.data || this.stats)
}
})
}
},
created() {
this.loadStats()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,157 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<custom-card>
<h3 style="color: var(--color-primary)" slot="header">主席信箱</h3>
<el-form :model="formData" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="labels.userName">
<el-input v-model="formData.userName" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="labels.loginName">
<el-input v-model="formData.loginName" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="labels.unitName">
<el-input v-model="formData.unitName" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="labels.unionName">
<el-input v-model="formData.unionName" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="labels.concat">
<el-input v-model="formData.concat" maxlength="20"></el-input>
</el-form-item>
<el-form-item :label="labels.title">
<el-input v-model="formData.title" maxlength="100" show-word-limit></el-input>
</el-form-item>
<el-form-item :label="labels.content">
<el-input
v-model="formData.content"
type="textarea"
maxlength="500"
show-word-limit
:rows="6"
></el-input>
</el-form-item>
<el-form-item :label="labels.attachments">
<file-upload
:value.sync="formData.attachments"
:upload_number="15"
upload_result_category="array"
complete_result
upload_mode="drag"
></file-upload>
</el-form-item>
</el-form>
<template slot="footer">
<el-button type="primary" @click="submitForm">提交信件</el-button>
</template>
</custom-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
store,
data() {
return {
bizId: GetQueryString('bizId'),
labels: {
userName: '姓名',
loginName: '工号',
unitName: '单位',
unionName: '工会',
concat: '联系方式',
title: '标题',
content: '信件内容',
attachments: '附件上传'
},
formData: {
attachments: []
}
}
},
created() {
this.init()
},
methods: {
init() {
if (this.bizId) {
this.$axios.post('/platform/chairmanMailbox/write/info', {id: this.bizId}).then((res) => {
if (res.code === 0) {
var data = res.data || {attachments: []}
if (!data.attachments) {
data.attachments = []
}
this.$set(this, 'formData', data)
}
})
} else {
this.$axios.post('/platform/chairmanMailbox/write/currentUserInfo').then((res) => {
if (res.code === 0) {
var data = res.data || {attachments: []}
if (!data.attachments) {
data.attachments = []
}
this.$set(this, 'formData', data)
}
})
}
},
submitForm() {
if (!this.formData.concat) {
this.$message.warning('请输入联系方式')
return
}
if (!/^1[3-9]\d{9}$/.test(this.formData.concat)) {
this.$message.warning('请输入正确的手机号')
return
}
if (!this.formData.title) {
this.$message.warning('请输入标题')
return
}
if (!this.formData.content) {
this.$message.warning('请输入信件内容')
return
}
this.$confirm('确认提交信件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/chairmanMailbox/write/submit', {
data: JSON.stringify(this.formData)
}).then((res) => {
if (res.code === 0) {
this.$message.success('提交成功')
commonUtil.pjaxPush('/platform/chairmanMailbox/mine')
}
})
})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,422 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="主席信箱" fixed placeholder left-text="返回" left-arrow @click-left="history.go(-1)"></van-nav-bar>
<div class="banner-section">
<div class="banner-content">
<div class="banner-text">
<h2>我们重视您的来信</h2>
<p>每一封信件都会认真阅读、及时处理。</p>
</div>
<div class="banner-image">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48cGF0aCBkPSJNMTI1LjYgMTAyLjdsLTE5LTE2LjhtLTQuNy0yMy43bDExLjUtNi44TTU3LjQgMTZsMTEuNSAxMS43TTMxLjIgNTMuOGwyMy42IDcuOSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2ZmZjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtvcGFjaXR5Oi40Ii8+PHBhdGggZD0iTTY0LjEgMzEuN0w0NyA0OS45Yy0zIDMuMi0zLjEgOC4xLS4xIDExLjJsMjQuNCAyNWMzIDMuMSA3LjkgMy4yIDExIC4xTDk5IDY5YzMtMy4yIDMuMS04LjEuMS0xMS4yTDc0LjcgMzIuOGMtMyAzLjEtNy45IDMuMS0xMC42LTEuMXoiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiNmZmY7c3Ryb2tlLW1pdGVybGltaXQ6MTAiLz48cGF0aCBkPSJNMTA4LjkgOTUuN2wtMTUuOC0xMi0xMi4yIDEzIDE0LjQgMTMuN2MuMyAyLjUgMi4zIDQuNSA0LjggNC41aDEzLjdjMi43IDAgNC45LTIuMiA0LjktNC45VjkzLjVjMC0yLjgtMi4xLTUtNC45LTV2OC40cy4xLTEuMi00LjkgMi44LjEtMyAuMS0zeiIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6I2ZmZjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMCIvPjwvc3ZnPg==" alt="主席信箱">
</div>
</div>
</div>
<div class="cards-container">
<div class="entry-card" @click="goPage('/platform/chairmanMailbox/write/h5')">
<div class="card-icon submit-icon">
<van-icon name="edit"></van-icon>
</div>
<div class="card-info">
<div class="entry-title">我要写信</div>
<div class="entry-desc">提交您的来信、诉求或建议</div>
</div>
<div class="card-arrow">
<van-icon name="arrow"></van-icon>
</div>
</div>
<div class="entry-card" @click="goPage('/platform/chairmanMailbox/mine/h5')">
<div class="card-icon my-icon">
<van-icon name="records"></van-icon>
</div>
<div class="card-info">
<div class="entry-title">我的信件</div>
<div class="entry-desc">查看我提交的信件和回复情况</div>
</div>
<div class="card-arrow">
<van-icon name="arrow"></van-icon>
</div>
</div>
<div class="entry-card" v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_CHAIRMAN'])" @click="goPage('/platform/chairmanMailbox/query/h5')">
<div class="card-icon admin-icon">
<van-icon name="manager"></van-icon>
</div>
<div class="card-info">
<div class="entry-title">信箱管理</div>
<div class="entry-desc">统一查看和回复职工提交的来信</div>
</div>
<div class="card-arrow">
<van-icon name="arrow"></van-icon>
</div>
</div>
</div>
<div class="guide-container">
<div class="guide-header">
<h3>使用指南</h3>
</div>
<div class="guide-steps">
<div class="guide-step">
<div class="step-number">1</div>
<div class="step-content">
<h4>填写信件</h4>
<p>点击“我要写信”,填写您的问题、意见或建议。</p>
</div>
<div class="step-icon">
<van-icon name="edit"></van-icon>
</div>
</div>
<div class="step-divider"></div>
<div class="guide-step">
<div class="step-number">2</div>
<div class="step-content">
<h4>等待处理</h4>
<p>提交后可在“我的信件”中查看办理进度和状态。</p>
</div>
<div class="step-icon">
<van-icon name="underway-o"></van-icon>
</div>
</div>
<div class="step-divider"></div>
<div class="guide-step">
<div class="step-number">3</div>
<div class="step-content">
<h4>查看回复</h4>
<p>信件处理完成后,可在详情中查看回复内容。</p>
</div>
<div class="step-icon">
<van-icon name="comment-o"></van-icon>
</div>
</div>
</div>
</div>
<div class="faq-container">
<div class="faq-header">
<h3>常见问题</h3>
<span class="faq-subtitle">帮助您快速了解使用方式</span>
</div>
<div class="faq-list">
<div class="faq-item" :class="{'faq-active': activeFaq === 1}" @click="toggleFaq(1)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o"></van-icon></span>
<span>如何提交带附件的信件?</span>
<span class="faq-arrow"><van-icon name="arrow-down"></van-icon></span>
</div>
<div class="faq-answer" v-show="activeFaq === 1">
<p>在“我要写信”页面可以上传附件,支持图片等常见文件,提交时会和信件内容一并保存。</p>
</div>
</div>
<div class="faq-item" :class="{'faq-active': activeFaq === 2}" @click="toggleFaq(2)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o"></van-icon></span>
<span>提交后在哪里查看回复?</span>
<span class="faq-arrow"><van-icon name="arrow-down"></van-icon></span>
</div>
<div class="faq-answer" v-show="activeFaq === 2">
<p>您可以进入“我的信件”,点击对应信件查看详情。若已处理,会显示回复人、回复时间和回复内容。</p>
</div>
</div>
<div class="faq-item" :class="{'faq-active': activeFaq === 3}" @click="toggleFaq(3)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o"></van-icon></span>
<span>提交后的信件可以修改吗?</span>
<span class="faq-arrow"><van-icon name="arrow-down"></van-icon></span>
</div>
<div class="faq-answer" v-show="activeFaq === 3">
<p>已提交信件默认不再修改。如确需补充内容,建议重新提交,并在标题或内容中说明是对前一次来信的补充。</p>
</div>
</div>
</div>
</div>
</div>
<style>
.banner-section {
padding: 0;
height: 180px;
background: linear-gradient(135deg, rgb(0, 78, 100) 0%, #37a6bd 100%);
position: relative;
overflow: visible;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 20px 0;
height: 100%;
}
.banner-text {
color: #ffffff;
z-index: 2;
}
.banner-text h2 {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.banner-text p {
font-size: 14px;
opacity: 0.9;
line-height: 1.7;
}
.banner-image {
width: 100px;
height: 100px;
z-index: 2;
}
.banner-image img {
width: 100%;
height: 100%;
object-fit: contain;
}
.cards-container {
padding: 20px 16px 16px;
margin-top: -20px;
position: relative;
z-index: 10;
background-color: #f6f6f6;
border-radius: 20px 20px 0 0;
}
.entry-card {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 16px;
}
.card-icon .van-icon {
font-size: 24px;
color: #ffffff;
}
.submit-icon {
background: linear-gradient(135deg, #1989fa 0%, #39b9f9 100%);
}
.my-icon {
background: linear-gradient(135deg, #07c160 0%, #10d878 100%);
}
.admin-icon {
background: linear-gradient(135deg, #ff6b6b 0%, #ffaa7f 100%);
}
.card-info {
flex: 1;
}
.entry-title {
color: #333333;
font-size: 16px;
font-weight: 600;
margin-bottom: 6px;
}
.entry-desc {
color: #999999;
font-size: 13px;
line-height: 1.7;
}
.card-arrow {
color: #cccccc;
}
.guide-container {
padding: 0 16px 16px;
background: #f6f6f6;
}
.guide-header {
margin-bottom: 16px;
}
.guide-header h3 {
font-size: 18px;
font-weight: 600;
color: #333333;
}
.guide-steps {
background: #ffffff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.guide-step {
display: flex;
align-items: center;
position: relative;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgb(0, 78, 100);
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-content h4 {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
color: #333333;
}
.step-content p {
font-size: 13px;
color: #666666;
margin: 0;
line-height: 1.7;
}
.step-icon {
margin-left: 12px;
color: rgb(0, 78, 100);
}
.step-divider {
height: 24px;
width: 1px;
background: #e8e8e8;
margin: 8px 0 8px 15px;
}
.faq-container {
padding: 0 16px 16px;
background: #f6f6f6;
}
.faq-header {
margin-bottom: 16px;
display: flex;
align-items: baseline;
}
.faq-header h3 {
font-size: 18px;
font-weight: 600;
color: #333333;
margin-right: 8px;
}
.faq-subtitle {
font-size: 12px;
color: #999999;
}
.faq-list {
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.faq-item {
border-bottom: 1px solid #eeeeee;
}
.faq-item:last-child {
border-bottom: none;
}
.faq-question {
display: flex;
align-items: center;
padding: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.faq-active .faq-question {
background-color: rgba(0, 78, 100, 0.1);
}
.faq-icon {
color: rgb(0, 78, 100);
margin-right: 12px;
}
.faq-arrow {
margin-left: auto;
color: #999999;
}
.faq-active .faq-arrow .van-icon {
transform: rotate(180deg);
}
.faq-answer {
padding: 0 16px 16px 44px;
color: #666666;
font-size: 13px;
line-height: 1.7;
border-top: 1px dashed #eeeeee;
background-color: rgba(0, 0, 0, 0.02);
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
store,
data() {
return {
activeFaq: null
}
},
methods: {
goPage(url) {
this.$pjaxReplace(url)
},
toggleFaq(id) {
this.activeFaq = this.activeFaq === id ? null : id
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,64 @@
const CHAIRMAN_MAILBOX_INFO = {
template:
'<van-action-sheet v-model="visible" title="查看详情">' +
' <div class="detail-container">' +
' <van-cell-group title="提交信息">' +
' <van-cell title="姓名">{{viewData.userName}}</van-cell>' +
' <van-cell title="工号">{{viewData.loginName}}</van-cell>' +
' <van-cell title="单位">{{viewData.unitName}}</van-cell>' +
' <van-cell title="工会">{{viewData.unionName}}</van-cell>' +
' <van-cell title="联系方式">{{viewData.concat}}</van-cell>' +
' <van-cell title="提交时间">{{viewData.submitTime}}</van-cell>' +
' <van-cell title="回复状态">{{getReplyText(viewData.isReply)}}</van-cell>' +
' <van-cell title="标题">{{viewData.title}}</van-cell>' +
' <van-cell title="信件内容" class="direction-column-cell">{{viewData.content}}</van-cell>' +
' </van-cell-group>' +
' <van-cell-group title="附件" v-if="viewData.attachments && viewData.attachments.length > 0">' +
' <div class="detail-upload-wrap">' +
' <file-preview :files="viewData.attachments" complete_result></file-preview>' +
' </div>' +
' </van-cell-group>' +
' <van-cell-group title="回复内容" v-if="viewData.isReply">' +
' <van-cell title="回复人">{{viewData.replyUserName}}</van-cell>' +
' <van-cell title="回复时间">{{viewData.replyTime}}</van-cell>' +
' <van-cell title="回复详情" class="direction-column-cell">{{viewData.replyContent}}</van-cell>' +
' </van-cell-group>' +
' <slot></slot>' +
' </div>' +
'</van-action-sheet>',
data() {
return {
visible: false,
viewData: {},
row: null
}
},
methods: {
onOpen(row) {
this.$set(this, 'row', row)
this.$set(this, 'visible', true)
this.getInfo()
},
onClose() {
this.$set(this, 'visible', false)
},
getInfo() {
this.$axios.post('/platform/chairmanMailbox/common/info', { id: this.row.id }).then((res) => {
if (res.code === 0) {
var data = res.data || {}
if (data.attachments && typeof data.attachments === 'string') {
try {
data.attachments = JSON.parse(data.attachments)
} catch (e) {
data.attachments = []
}
}
this.$set(this, 'viewData', data)
}
})
},
getReplyText(isReply) {
return isReply ? '已回复' : '待回复'
}
}
}
@@ -0,0 +1,279 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
:root {
--primary-color: rgb(0, 78, 100);
--primary-light: rgba(0, 78, 100, 0.1);
--secondary-color: #37A6BD;
--text-color: #333333;
--text-secondary: #666666;
--text-light: #999999;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--border-color: #eeeeee;
}
#app {
min-height: 100vh;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
}
.mail-list {
padding: 16px;
background-color: var(--bg-color);
flex: 1;
display: flex;
flex-direction: column;
}
.van-pull-refresh,
.van-list {
flex: 1;
display: flex;
flex-direction: column;
}
.mail-card {
background-color: var(--card-bg);
border-radius: 12px;
margin-bottom: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
border: 1px solid rgba(0, 0, 0, 0.02);
}
.mail-card:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.mail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.mail-title {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mail-status {
font-size: 12px;
padding: 3px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: 500;
}
.status-pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-replied {
background-color: #f6ffed;
color: #52c41a;
}
.mail-content {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
background-color: var(--bg-color);
padding: 10px;
border-radius: 8px;
word-break: break-all;
white-space: pre-line;
max-height: 3.2em;
text-overflow: ellipsis;
display: block;
}
.mail-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--text-light);
border-top: 1px solid #f5f5f5;
padding-top: 12px;
}
.mail-time {
display: flex;
align-items: center;
}
.mail-time .van-icon {
font-size: 14px;
margin-right: 4px;
}
.mail-action {
color: var(--primary-color);
display: flex;
align-items: center;
font-weight: 500;
}
.mail-action .van-icon {
font-size: 14px;
margin-left: 2px;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 16px;
flex: 1;
}
.empty-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 16px;
text-align: center;
}
.empty-text {
font-size: 15px;
color: var(--text-light);
text-align: center;
margin-bottom: 20px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="我的信件" left-arrow @click-left="historyBack" fixed placeholder left-text="返回"></van-nav-bar>
<div class="mail-list">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="loadMore">
<div class="empty-list" v-if="mailList.length === 0 && !loading">
<van-icon name="comment-circle-o" class="empty-icon"></van-icon>
<div class="empty-text">您还没有提交过信件</div>
<van-button type="primary" size="normal" round @click="goWrite">去写信</van-button>
</div>
<div class="mail-card" v-for="item in mailList" :key="item.id" @click="openDetail(item)">
<div class="mail-header">
<div class="mail-title">{{ item.title || '信件反馈' }}</div>
<div class="mail-status" :class="item.isReply ? 'status-replied' : 'status-pending'">
{{ item.isReply ? '已回复' : '待回复' }}
</div>
</div>
<div class="mail-content">{{ item.content }}</div>
<div class="mail-footer">
<div class="mail-time">
<van-icon name="clock-o"></van-icon>
<span>{{ formatDate(item.submitTime) }}</span>
</div>
<div class="mail-action">
查看详情
<van-icon name="arrow"></van-icon>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<chairman-mailbox-info ref="infoRef"></chairman-mailbox-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
var chairmanMailboxMineVue = new Vue({
el: '#app',
store,
components: {
'chairman-mailbox-info': CHAIRMAN_MAILBOX_INFO
},
data() {
return {
loading: false,
refreshing: false,
finished: false,
pageNumber: 1,
pageSize: 10,
mailList: []
}
},
created() {
this.loadData()
},
methods: {
historyBack,
loadData() {
this.$set(this, 'loading', true)
$.post('/platform/chairmanMailbox/mine/pageData', {
pageNumber: this.pageNumber,
pageSize: this.pageSize,
searchKeyword: ''
}).done((res) => {
var list = res && res.data ? (res.data.list || []) : []
if (this.pageNumber === 1) {
this.$set(this, 'mailList', list)
} else {
this.$set(this, 'mailList', this.mailList.concat(list))
}
this.$set(this, 'finished', !list || list.length < this.pageSize)
this.$set(this, 'loading', false)
this.$set(this, 'refreshing', false)
}).fail(() => {
this.$set(this, 'loading', false)
this.$set(this, 'refreshing', false)
this.$set(this, 'finished', true)
})
},
onRefresh() {
this.$set(this, 'pageNumber', 1)
this.$set(this, 'finished', false)
this.loadData()
},
loadMore() {
this.$set(this, 'pageNumber', this.pageNumber + 1)
this.loadData()
},
openDetail(row) {
this.$refs.infoRef.onOpen(row)
},
goWrite() {
this.$pjaxReplace('/platform/chairmanMailbox/write/h5')
},
formatDate(dateStr) {
if (!dateStr) {
return ''
}
return String(dateStr).replace('T', ' ').substring(0, 16)
}
}
})
window.addEventListener('pageshow', function (e) {
if (e.persisted || (window.performance && window.performance.navigation.type === 2)) {
chairmanMailboxMineVue.onRefresh()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,561 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
:root {
--primary-color: rgb(0, 78, 100);
--primary-light: rgba(0, 78, 100, 0.1);
--secondary-color: #37A6BD;
--text-color: #333333;
--text-secondary: #666666;
--text-light: #999999;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--border-color: #eeeeee;
}
#app {
min-height: 100vh;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
}
.stats-panel {
background-color: var(--card-bg);
padding: 16px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.stats-item {
flex: 1;
text-align: center;
}
.stats-value {
font-size: 20px;
font-weight: bold;
color: var(--primary-color);
}
.stats-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
.filter-section {
background-color: var(--card-bg);
padding: 12px 16px;
margin-bottom: 10px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.filter-section.collapsed {
max-height: 84px;
}
.filter-section.expanded {
max-height: 500px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.filter-title {
font-size: 15px;
font-weight: 500;
color: var(--text-color);
}
.filter-toggle {
color: var(--text-secondary);
display: flex;
align-items: center;
}
.filter-toggle .van-icon {
transition: transform 0.3s;
margin-left: 4px;
}
.filter-toggle .van-icon.rotate {
transform: rotate(180deg);
}
.filter-content {
transition: opacity 0.3s;
}
.filter-content.hidden {
opacity: 0;
height: 0;
overflow: hidden;
}
.filter-row {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-label {
font-size: 13px;
color: var(--text-secondary);
margin-right: 10px;
min-width: 60px;
padding-top: 4px;
}
.filter-options {
display: flex;
flex-wrap: wrap;
flex: 1;
}
.filter-tag {
padding: 4px 10px;
border-radius: 16px;
font-size: 12px;
margin-right: 8px;
margin-bottom: 6px;
background-color: var(--bg-color);
color: var(--text-secondary);
}
.filter-tag.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.filter-search {
padding: 8px 0;
}
.filter-search .van-search {
padding: 0;
}
.filter-search .van-search__content {
background-color: var(--bg-color);
}
.mail-list {
padding: 16px;
background-color: var(--bg-color);
flex: 1;
display: flex;
flex-direction: column;
}
.van-pull-refresh,
.van-list {
flex: 1;
display: flex;
flex-direction: column;
}
.mail-card {
background-color: var(--card-bg);
border-radius: 12px;
margin-bottom: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.02);
}
.mail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.mail-title {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mail-status {
font-size: 12px;
padding: 3px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: 500;
}
.status-pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-replied {
background-color: #fff7e6;
color: #fa8c16;
}
.submitter-row {
display: flex;
align-items: center;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.submitter-row .van-icon {
margin-right: 5px;
font-size: 14px;
}
.unit-name {
margin-left: 15px;
}
.mail-content {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
background-color: var(--bg-color);
padding: 10px;
border-radius: 8px;
word-break: break-all;
white-space: pre-line;
max-height: 3.2em;
text-overflow: ellipsis;
display: block;
}
.mail-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--text-light);
border-top: 1px solid #f5f5f5;
padding-top: 12px;
}
.mail-time {
display: flex;
align-items: center;
}
.mail-time .van-icon {
font-size: 14px;
margin-right: 4px;
}
.mail-action {
color: var(--primary-color);
display: flex;
align-items: center;
font-weight: 500;
}
.mail-action .van-icon {
font-size: 14px;
margin-left: 2px;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 16px;
flex: 1;
}
.empty-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 16px;
text-align: center;
}
.empty-text {
font-size: 15px;
color: var(--text-light);
text-align: center;
margin-bottom: 20px;
}
.reply-box {
padding: 12px 16px 16px;
}
.reply-actions {
padding-top: 12px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="信箱管理" left-arrow @click-left="historyBack" fixed placeholder left-text="返回"></van-nav-bar>
<div class="stats-panel">
<div class="stats-item">
<div class="stats-value">{{ stats.total }}</div>
<div class="stats-label">总信件数</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.pending }}</div>
<div class="stats-label">待回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.replied }}</div>
<div class="stats-label">已回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.today }}</div>
<div class="stats-label">今日新增</div>
</div>
</div>
<div class="filter-section" :class="filters.isCollapsed ? 'collapsed' : 'expanded'">
<div class="filter-header">
<div class="filter-title">筛选条件</div>
<div class="filter-toggle" @click="toggleFilterCollapse">
<span>{{ filters.isCollapsed ? '展开' : '收起' }}</span>
<van-icon :name="filters.isCollapsed ? 'arrow-down' : 'arrow-up'" :class="{ rotate: !filters.isCollapsed }"></van-icon>
</div>
</div>
<div class="filter-search">
<van-search v-model="filters.keyword" placeholder="搜索标题、内容或提交人" @search="onSearch"></van-search>
</div>
<div class="filter-content" :class="{ hidden: filters.isCollapsed }">
<div class="filter-row">
<div class="filter-label">状态</div>
<div class="filter-options">
<div class="filter-tag" :class="{ active: filters.status === '' }" @click="setFilter('status', '')">全部</div>
<div class="filter-tag" :class="{ active: filters.status === '0' }" @click="setFilter('status', '0')">待回复</div>
<div class="filter-tag" :class="{ active: filters.status === '1' }" @click="setFilter('status', '1')">已回复</div>
</div>
</div>
<div class="filter-row">
<div class="filter-label">时间</div>
<div class="filter-options">
<div class="filter-tag" :class="{ active: filters.time === '' }" @click="setFilter('time', '')">全部</div>
<div class="filter-tag" :class="{ active: filters.time === 'today' }" @click="setFilter('time', 'today')">今日</div>
<div class="filter-tag" :class="{ active: filters.time === 'week' }" @click="setFilter('time', 'week')">本周</div>
<div class="filter-tag" :class="{ active: filters.time === 'month' }" @click="setFilter('time', 'month')">本月</div>
</div>
</div>
</div>
</div>
<div class="mail-list">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="loadMore">
<div class="empty-list" v-if="mailList.length === 0 && !loading">
<van-icon name="comment-circle-o" class="empty-icon"></van-icon>
<div class="empty-text">暂无符合条件的信件</div>
</div>
<div class="mail-card" v-for="item in mailList" :key="item.id" @click="openDetail(item)">
<div class="mail-header">
<div class="mail-title">{{ item.title || '信件反馈' }}</div>
<div class="mail-status" :class="item.isReply ? 'status-replied' : 'status-pending'">
{{ item.isReply ? '已回复' : '待回复' }}
</div>
</div>
<div class="submitter-row">
<van-icon name="contact"></van-icon>
<span>{{ item.userName }}</span>
<span class="unit-name">{{ item.unitName }}</span>
</div>
<div class="mail-content">{{ item.content }}</div>
<div class="mail-footer">
<div class="mail-time">
<van-icon name="clock-o"></van-icon>
<span>{{ formatDate(item.submitTime) }}</span>
</div>
<div class="mail-action">
{{ item.isReply ? '查看详情' : '去回复' }}
<van-icon name="arrow"></van-icon>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<chairman-mailbox-info ref="infoRef">
<div v-if="showReplyForm" class="reply-box">
<van-field
v-model="replyData.replyContent"
label="回复内容"
type="textarea"
rows="4"
maxlength="500"
show-word-limit
placeholder="请输入回复内容">
</van-field>
<div class="reply-actions">
<van-button block type="primary" @click="submitReply">提交回复</van-button>
</div>
</div>
</chairman-mailbox-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
new Vue({
el: '#app',
store,
components: {
'chairman-mailbox-info': CHAIRMAN_MAILBOX_INFO
},
data() {
return {
refreshing: false,
loading: false,
finished: false,
pageNumber: 1,
pageSize: 10,
mailList: [],
showReplyForm: false,
replyData: {
id: '',
replyContent: ''
},
filters: {
status: '',
time: '',
keyword: '',
isCollapsed: true
},
stats: {
total: 0,
pending: 0,
replied: 0,
today: 0
}
}
},
created() {
this.loadStats()
this.loadData()
},
methods: {
historyBack,
loadStats() {
$.post('/platform/chairmanMailbox/query/getStats').done((res) => {
if (res.code === 0 && res.data) {
this.$set(this, 'stats', res.data)
}
})
},
loadData() {
this.$set(this, 'loading', true)
$.post('/platform/chairmanMailbox/query/pageData', {
pageNumber: this.pageNumber,
pageSize: this.pageSize,
searchKeyword: this.filters.keyword,
isReply: this.filters.status === '' ? null : parseInt(this.filters.status),
timeRange: this.filters.time
}).done((res) => {
var list = res && res.data ? (res.data.list || []) : []
if (this.pageNumber === 1) {
this.$set(this, 'mailList', list)
} else {
this.$set(this, 'mailList', this.mailList.concat(list))
}
this.$set(this, 'finished', !list || list.length < this.pageSize)
this.$set(this, 'loading', false)
this.$set(this, 'refreshing', false)
}).fail(() => {
this.$set(this, 'loading', false)
this.$set(this, 'refreshing', false)
this.$set(this, 'finished', true)
})
},
onRefresh() {
this.$set(this, 'pageNumber', 1)
this.$set(this, 'finished', false)
this.loadStats()
this.loadData()
},
loadMore() {
this.$set(this, 'pageNumber', this.pageNumber + 1)
this.loadData()
},
setFilter(type, value) {
this.$set(this.filters, type, value)
this.$set(this, 'pageNumber', 1)
this.$set(this, 'finished', false)
this.loadData()
},
onSearch() {
this.$set(this, 'pageNumber', 1)
this.$set(this, 'finished', false)
this.loadData()
},
toggleFilterCollapse() {
this.$set(this.filters, 'isCollapsed', !this.filters.isCollapsed)
},
openDetail(row) {
this.$set(this, 'showReplyForm', !row.isReply)
this.$set(this, 'replyData', {
id: row.id,
replyContent: row.replyContent || ''
})
this.$refs.infoRef.onOpen(row)
},
submitReply() {
if (!this.replyData.replyContent) {
this.$toast('请输入回复内容')
return
}
this.$dialog.confirm({
title: '提示',
message: '确认提交回复吗?'
}).then(() => {
this.$axios.post('/platform/chairmanMailbox/query/reply', {
data: JSON.stringify(this.replyData)
}).then((res) => {
if (res.code === 0) {
this.$toast.success('回复成功')
this.$refs.infoRef.onClose()
this.onRefresh()
}
})
}).catch(() => {
})
},
formatDate(dateStr) {
if (!dateStr) {
return ''
}
return String(dateStr).replace('T', ' ').substring(0, 16)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,255 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="我要写信" left-arrow left-text="返回" fixed placeholder @click-left="historyBack"></van-nav-bar>
<div class="form-container">
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>来信人信息</span>
</div>
<div class="input-row">
<div class="input-label">姓名</div>
<div class="input-control">
<input type="text" v-model="formData.userName" readonly>
</div>
</div>
<div class="input-row">
<div class="input-label">工号</div>
<div class="input-control">
<input type="text" v-model="formData.loginName" readonly>
</div>
</div>
<div class="input-row">
<div class="input-label">联系方式</div>
<div class="input-control">
<input type="tel" v-model="formData.concat" placeholder="请输入手机号">
</div>
</div>
<div class="input-row">
<div class="input-label">所在单位</div>
<div class="input-control">
<input type="text" v-model="formData.unitName" readonly>
</div>
</div>
<div class="input-row">
<div class="input-label">所属工会</div>
<div class="input-control">
<input type="text" v-model="formData.unionName" readonly>
</div>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>信件标题</span>
</div>
<div class="input-row">
<div class="input-label">标题</div>
<div class="input-control">
<input type="text" v-model="formData.title" placeholder="请输入信件标题">
</div>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>信件内容</span>
</div>
<div class="textarea-container">
<textarea v-model="formData.content" placeholder="请输入您想反映的问题、意见或建议"></textarea>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>附件上传</span>
</div>
<div class="upload-area">
<h5-file-upload
:value.sync="formData.attachments"
:upload_number="15"
upload_mode="file"
upload_result_category="array"
upload_result_type="url"
complete_result
></h5-file-upload>
</div>
</div>
<div class="submit-area">
<van-button block type="info" round @click="submitForm">提交信件</van-button>
</div>
</div>
</div>
<style>
.form-container {
min-height: calc(100vh - 46px);
background: #f6f6f6;
}
.form-section {
margin-bottom: 12px;
}
.section-title {
display: flex;
align-items: center;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #f6f6f6;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgb(0, 78, 100);
margin-right: 8px;
}
.section-title span {
color: #333333;
font-weight: 500;
}
.input-row {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f5f5f5;
background: #ffffff;
}
.input-label {
width: 82px;
color: #333333;
padding-right: 8px;
}
.input-control {
flex: 1;
}
.input-control input {
width: 100%;
border: 0;
outline: none;
text-align: right;
color: #666666;
font-size: 14px;
background: transparent;
}
.textarea-container {
padding: 10px 16px;
background: #ffffff;
}
.textarea-container textarea {
width: 100%;
height: 140px;
border: 0;
outline: none;
resize: none;
color: #333333;
font-size: 14px;
line-height: 1.7;
}
.upload-area {
padding: 16px;
background: #ffffff;
}
.submit-area {
padding: 20px 16px;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
store,
data() {
return {
bizId: GetQueryString('bizId'),
formData: {
attachments: []
}
}
},
created() {
this.init()
},
methods: {
historyBack,
init() {
if (this.bizId) {
this.$axios.post('/platform/chairmanMailbox/write/info', { id: this.bizId }).then((res) => {
if (res.code === 0) {
var data = res.data || {}
if (!data.attachments) {
data.attachments = []
}
this.$set(this, 'formData', data)
}
})
} else {
this.$axios.post('/platform/chairmanMailbox/write/currentUserInfo').then((res) => {
if (res.code === 0) {
var data = res.data || {}
if (!data.attachments) {
data.attachments = []
}
this.$set(this, 'formData', data)
}
})
}
},
submitForm() {
if (!this.formData.concat) {
this.$toast('请输入联系方式')
return
}
if (!/^1[3-9]\d{9}$/.test(this.formData.concat)) {
this.$toast('请输入正确的手机号')
return
}
if (!this.formData.title) {
this.$toast('请输入标题')
return
}
if (!this.formData.content) {
this.$toast('请输入信件内容')
return
}
this.$dialog.confirm({
title: '提示',
message: '确认提交信件吗?'
}).then(() => {
this.$axios.post('/platform/chairmanMailbox/write/submit', {
data: JSON.stringify(this.formData)
}).then((res) => {
if (res.code === 0) {
this.$toast.success('提交成功')
this.$pjaxReplace('/platform/chairmanMailbox/mine/h5')
}
})
}).catch(() => {
})
}
}
})
</script>
<!--#
}
#-->