会议纪要功能修复与改进
后端: - 待办同步改走 ISysOaTaskService.insertByBo/updateByBo,新任务带操作日志和IM通知 - 任务状态映射修正:done→2执行完成,其余→0执行中(原 progress→1 会显示成"等待验收") - 无负责人/无内容的待办仅作纪要记录,不再生成无主任务 - 更新时可清空字段改用显式 set(原来解绑项目、清空内容不生效) - 新增接口返回纪要ID,前端据此进入编辑态,避免重复保存生成多条 - 会议编号加3位随机数防同秒撞唯一键;异常改 ServiceException;同步失败记日志 - enrich 为待办条目注入 assigneeName,列表/详情/导出可显示负责人姓名 - SysOaTaskServiceImpl.insertByBo 回填 taskId 供调用方关联 前端: - 主持人/待办负责人改用人员单选弹窗(原多选组件取首位的方式易误操作) - 会议类型、待办状态接入 sys_dict 字典(oa_meeting_type / oa_meeting_task_status) - 新建保存后切换为编辑态;默认日期用本地时区(原 UTC 凌晨会差一天) - 导出/打印带主持人、参会人、待办负责人姓名(原来只有用户ID) - 删除已同步待办时提示任务不会被删除 SQL(已直接应用到生产库): - 字典数据补全并修复 dict_id=0 脏数据(sys_dict_* 主键为雪花ID须显式指定) - 菜单 2063809716454174722 icon 修为 documentation,授权10个角色 - 脚本改为幂等,去掉 DROP TABLE,del_flag 注释修正为逻辑删除值2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,17 @@ public class OaMeetingMinutesController extends BaseController {
|
||||
return R.ok(service.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增,返回新纪要ID(前端据此切换为编辑态,避免重复保存生成多条)
|
||||
*/
|
||||
@Log(title = "会议纪要", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit
|
||||
@PostMapping
|
||||
public R<Void> add(@RequestBody OaMeetingMinutesBo bo) {
|
||||
return toAjax(service.insertByBo(bo));
|
||||
public R<Long> add(@RequestBody OaMeetingMinutesBo bo) {
|
||||
if (!service.insertByBo(bo)) {
|
||||
return R.fail("保存失败");
|
||||
}
|
||||
return R.ok(bo.getId());
|
||||
}
|
||||
|
||||
@Log(title = "会议纪要", businessType = BusinessType.UPDATE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.oa.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -12,19 +14,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.oa.domain.OaMeetingMinutes;
|
||||
import com.ruoyi.oa.domain.SysOaProject;
|
||||
import com.ruoyi.oa.domain.SysOaTask;
|
||||
import com.ruoyi.oa.domain.bo.OaMeetingMinutesBo;
|
||||
import com.ruoyi.oa.domain.bo.SysOaTaskBo;
|
||||
import com.ruoyi.oa.domain.vo.OaMeetingMinutesVo;
|
||||
import com.ruoyi.oa.mapper.OaMeetingMinutesMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaProjectMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaTaskMapper;
|
||||
import com.ruoyi.oa.service.IOaMeetingMinutesService;
|
||||
import com.ruoyi.oa.service.ISysOaTaskService;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -32,6 +37,13 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会议纪要
|
||||
*
|
||||
* 待办同步说明:tasks_json 中"有负责人且有内容"的条目会通过 ISysOaTaskService
|
||||
* 生成/更新 sys_oa_task(复用任务模块的操作日志、IM 通知逻辑),任务ID回写进 JSON。
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@@ -39,12 +51,20 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
private final OaMeetingMinutesMapper baseMapper;
|
||||
private final SysOaProjectMapper projectMapper;
|
||||
private final SysOaTaskMapper taskMapper;
|
||||
private final ISysOaTaskService taskService;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ObjectMapper json = new ObjectMapper();
|
||||
|
||||
/** sys_oa_task.state:0执行中 2执行完成(1等待验收/15延期申请由任务模块流转,会议页不使用) */
|
||||
private static final Long TASK_STATE_DOING = 0L;
|
||||
private static final Long TASK_STATE_DONE = 2L;
|
||||
|
||||
@Override
|
||||
public OaMeetingMinutesVo queryById(Long id) {
|
||||
OaMeetingMinutesVo vo = baseMapper.selectVoById(id);
|
||||
if (vo == null) {
|
||||
throw new ServiceException("会议纪要不存在或已删除");
|
||||
}
|
||||
enrich(Collections.singletonList(vo));
|
||||
return vo;
|
||||
}
|
||||
@@ -72,7 +92,8 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
if (StringUtils.isNotBlank(bo.getKeyword())) {
|
||||
String kw = bo.getKeyword().trim();
|
||||
lqw.and(w -> w.like(OaMeetingMinutes::getSubject, kw)
|
||||
.or().like(OaMeetingMinutes::getLocation, kw));
|
||||
.or().like(OaMeetingMinutes::getLocation, kw)
|
||||
.or().like(OaMeetingMinutes::getMeetingCode, kw));
|
||||
}
|
||||
lqw.ge(bo.getDateFrom() != null, OaMeetingMinutes::getMeetingDate, bo.getDateFrom());
|
||||
lqw.le(bo.getDateTo() != null, OaMeetingMinutes::getMeetingDate, bo.getDateTo());
|
||||
@@ -82,16 +103,25 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/** 给列表填上 项目名/编号、主持人/参会人员 昵称 */
|
||||
/** 填充 项目名/编号、主持人/参会人/待办负责人 昵称 */
|
||||
private void enrich(List<OaMeetingMinutesVo> list) {
|
||||
if (list == null || list.isEmpty()) return;
|
||||
Set<Long> projectIds = new HashSet<>();
|
||||
Set<Long> userIds = new HashSet<>();
|
||||
Map<Long, ArrayNode> taskNodes = new HashMap<>();
|
||||
for (OaMeetingMinutesVo v : list) {
|
||||
if (v == null) continue;
|
||||
if (v.getProjectId() != null) projectIds.add(v.getProjectId());
|
||||
if (v.getHostUserId() != null) userIds.add(v.getHostUserId());
|
||||
for (Long uid : parseLongCsv(v.getAttendeeUserIds())) userIds.add(uid);
|
||||
userIds.addAll(parseLongCsv(v.getAttendeeUserIds()));
|
||||
ArrayNode arr = parseTaskArray(v.getTasksJson());
|
||||
if (arr != null) {
|
||||
taskNodes.put(v.getId(), arr);
|
||||
for (JsonNode n : arr) {
|
||||
Long uid = longOf(n, "assigneeUserId");
|
||||
if (uid != null) userIds.add(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Long, SysOaProject> pMap = projectIds.isEmpty() ? Collections.emptyMap()
|
||||
: projectMapper.selectList(new QueryWrapper<SysOaProject>().in("project_id", projectIds))
|
||||
@@ -113,14 +143,26 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
SysUser u = uMap.get(v.getHostUserId());
|
||||
if (u != null) v.setHostUserName(u.getNickName());
|
||||
}
|
||||
List<Long> attIds = parseLongCsv(v.getAttendeeUserIds());
|
||||
if (!attIds.isEmpty()) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Long uid : attIds) {
|
||||
SysUser u = uMap.get(uid);
|
||||
if (u != null) names.add(u.getNickName());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Long uid : parseLongCsv(v.getAttendeeUserIds())) {
|
||||
SysUser u = uMap.get(uid);
|
||||
if (u != null) names.add(u.getNickName());
|
||||
}
|
||||
if (!names.isEmpty()) v.setAttendeeUserNames(String.join(",", names));
|
||||
// 待办条目写入 assigneeName,前端展示/导出用
|
||||
ArrayNode arr = taskNodes.get(v.getId());
|
||||
if (arr != null) {
|
||||
for (JsonNode n : arr) {
|
||||
if (!n.isObject()) continue;
|
||||
Long uid = longOf(n, "assigneeUserId");
|
||||
SysUser u = uid == null ? null : uMap.get(uid);
|
||||
((ObjectNode) n).put("assigneeName", u == null ? null : u.getNickName());
|
||||
}
|
||||
try {
|
||||
v.setTasksJson(json.writeValueAsString(arr));
|
||||
} catch (Exception e) {
|
||||
log.warn("会议纪要[{}] tasksJson 序列化失败", v.getId(), e);
|
||||
}
|
||||
v.setAttendeeUserNames(String.join(",", names));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,9 +171,10 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean insertByBo(OaMeetingMinutesBo bo) {
|
||||
OaMeetingMinutes add = BeanUtil.toBean(bo, OaMeetingMinutes.class);
|
||||
if (StringUtils.isBlank(add.getMeetingCode())) {
|
||||
add.setMeetingCode("MT-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
|
||||
}
|
||||
add.setId(null);
|
||||
// 时间戳+3位随机数,避免同一秒并发保存撞唯一键
|
||||
add.setMeetingCode("MT-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
|
||||
+ RandomUtil.randomNumbers(3));
|
||||
if (StringUtils.isBlank(add.getMeetingType())) add.setMeetingType("other");
|
||||
if (add.getSyncTask() == null) add.setSyncTask(1);
|
||||
validBeforeSave(add);
|
||||
@@ -141,8 +184,7 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
if (Integer.valueOf(1).equals(add.getSyncTask())) {
|
||||
String updated = syncTasks(add);
|
||||
if (updated != null && !updated.equals(add.getTasksJson())) {
|
||||
add.setTasksJson(updated);
|
||||
baseMapper.updateById(add);
|
||||
patchTasksJson(add.getId(), updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,28 +194,50 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(OaMeetingMinutesBo bo) {
|
||||
if (bo.getId() == null) throw new ServiceException("缺少纪要ID");
|
||||
OaMeetingMinutes old = baseMapper.selectById(bo.getId());
|
||||
if (old == null) throw new ServiceException("会议纪要不存在或已删除");
|
||||
OaMeetingMinutes upd = BeanUtil.toBean(bo, OaMeetingMinutes.class);
|
||||
if (StringUtils.isBlank(upd.getMeetingType())) upd.setMeetingType("other");
|
||||
validBeforeSave(upd);
|
||||
boolean ok = baseMapper.updateById(upd) > 0;
|
||||
if (ok && Integer.valueOf(1).equals(upd.getSyncTask())) {
|
||||
String updated = syncTasks(upd);
|
||||
if (updated != null && !updated.equals(upd.getTasksJson())) {
|
||||
upd.setTasksJson(updated);
|
||||
baseMapper.updateById(upd);
|
||||
}
|
||||
if (Integer.valueOf(1).equals(upd.getSyncTask())) {
|
||||
upd.setTasksJson(syncTasks(upd));
|
||||
}
|
||||
return ok;
|
||||
// 可清空字段(解绑项目、清空人员/内容等)显式 set,避免 MyBatis-Plus 忽略 null 导致清空不生效
|
||||
LambdaUpdateWrapper<OaMeetingMinutes> luw = Wrappers.<OaMeetingMinutes>lambdaUpdate()
|
||||
.set(OaMeetingMinutes::getProjectId, upd.getProjectId())
|
||||
.set(OaMeetingMinutes::getHostUserId, upd.getHostUserId())
|
||||
.set(OaMeetingMinutes::getAttendeeUserIds, upd.getAttendeeUserIds())
|
||||
.set(OaMeetingMinutes::getLocation, upd.getLocation())
|
||||
.set(OaMeetingMinutes::getTopic, upd.getTopic())
|
||||
.set(OaMeetingMinutes::getDiscussion, upd.getDiscussion())
|
||||
.set(OaMeetingMinutes::getDecision, upd.getDecision())
|
||||
.set(OaMeetingMinutes::getTasksJson, upd.getTasksJson())
|
||||
.eq(OaMeetingMinutes::getId, upd.getId());
|
||||
OaMeetingMinutes entity = new OaMeetingMinutes();
|
||||
entity.setMeetingDate(upd.getMeetingDate());
|
||||
entity.setMeetingType(upd.getMeetingType());
|
||||
entity.setSubject(upd.getSubject());
|
||||
entity.setSyncTask(upd.getSyncTask());
|
||||
return baseMapper.update(entity, luw) > 0;
|
||||
}
|
||||
|
||||
private void patchTasksJson(Long id, String tasksJson) {
|
||||
baseMapper.update(null, Wrappers.<OaMeetingMinutes>lambdaUpdate()
|
||||
.set(OaMeetingMinutes::getTasksJson, tasksJson)
|
||||
.eq(OaMeetingMinutes::getId, id));
|
||||
}
|
||||
|
||||
private void validBeforeSave(OaMeetingMinutes e) {
|
||||
if (e.getMeetingDate() == null) throw new IllegalArgumentException("请选择会议日期");
|
||||
if (StringUtils.isBlank(e.getSubject())) throw new IllegalArgumentException("请输入会议主题");
|
||||
if (e.getMeetingDate() == null) throw new ServiceException("请选择会议日期");
|
||||
if (StringUtils.isBlank(e.getSubject())) throw new ServiceException("请输入会议主题");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 tasks_json 中的每个待办同步生成 sys_oa_task:
|
||||
* - 若条目已有 taskId 且仍存在 → 更新内容/截止/完成状态
|
||||
* - 否则新建并把 taskId 写回 JSON
|
||||
* 将待办同步到 sys_oa_task(通过任务服务,带操作日志和 IM 通知):
|
||||
* - 无内容或无负责人的条目仅作纪要记录,不生成任务
|
||||
* - 已有 taskId 且任务仍存在 → 更新;否则新建并把 taskId 写回 JSON
|
||||
* - 同步失败不阻塞纪要保存,仅记录日志
|
||||
*/
|
||||
private String syncTasks(OaMeetingMinutes meeting) {
|
||||
if (StringUtils.isBlank(meeting.getTasksJson())) return meeting.getTasksJson();
|
||||
@@ -181,61 +245,64 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
JsonNode root = json.readTree(meeting.getTasksJson());
|
||||
if (!root.isArray()) return meeting.getTasksJson();
|
||||
ArrayNode arr = (ArrayNode) root;
|
||||
Long currentUser = LoginHelper.getUserId();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
JsonNode n = arr.get(i);
|
||||
for (JsonNode n : arr) {
|
||||
if (!n.isObject()) continue;
|
||||
ObjectNode o = (ObjectNode) n;
|
||||
String content = textOf(o, "content");
|
||||
if (StringUtils.isBlank(content)) continue;
|
||||
|
||||
Long assignee = longOf(o, "assigneeUserId");
|
||||
String deadline = textOf(o, "deadline");
|
||||
if (StringUtils.isBlank(content) || assignee == null) continue;
|
||||
|
||||
String status = textOf(o, "status");
|
||||
boolean done = "done".equals(status);
|
||||
Long existTaskId = longOf(o, "taskId");
|
||||
SysOaTask exist = existTaskId == null ? null : taskMapper.selectById(existTaskId);
|
||||
|
||||
SysOaTask t = null;
|
||||
if (existTaskId != null) {
|
||||
t = taskMapper.selectById(existTaskId);
|
||||
}
|
||||
boolean isNew = (t == null);
|
||||
if (isNew) {
|
||||
t = new SysOaTask();
|
||||
t.setCreateUserId(currentUser);
|
||||
t.setBeginTime(meeting.getMeetingDate());
|
||||
}
|
||||
SysOaTaskBo t = new SysOaTaskBo();
|
||||
t.setProjectId(meeting.getProjectId());
|
||||
t.setTaskTitle(content.length() > 200 ? content.substring(0, 200) : content);
|
||||
t.setContent("来自会议纪要:" + meeting.getSubject());
|
||||
t.setWorkerId(assignee);
|
||||
if (StringUtils.isNotBlank(deadline)) {
|
||||
try { t.setFinishTime(new SimpleDateFormat("yyyy-MM-dd").parse(deadline)); } catch (Exception ignored) {}
|
||||
}
|
||||
// 任务状态:done=2 已完成;progress=1 进行中;pending=0 待办
|
||||
if ("done".equals(status)) t.setState(2L);
|
||||
else if ("progress".equals(status)) t.setState(1L);
|
||||
else t.setState(0L);
|
||||
|
||||
if (isNew) {
|
||||
taskMapper.insert(t);
|
||||
o.put("taskId", t.getTaskId());
|
||||
t.setTaskTitle(StringUtils.substring(content, 0, 200));
|
||||
t.setContent("来自会议纪要「" + meeting.getSubject() + "」");
|
||||
t.setFinishTime(parseDay(textOf(o, "deadline")));
|
||||
t.setState(done ? TASK_STATE_DONE : TASK_STATE_DOING);
|
||||
if (done) t.setCompletedTime(new Date());
|
||||
if (exist == null) {
|
||||
t.setBeginTime(meeting.getMeetingDate());
|
||||
t.setWorkerIds(String.valueOf(assignee));
|
||||
t.setStatus(0L);
|
||||
taskService.insertByBo(t);
|
||||
if (t.getTaskId() != null) o.put("taskId", t.getTaskId());
|
||||
} else {
|
||||
taskMapper.updateById(t);
|
||||
t.setTaskId(existTaskId);
|
||||
t.setWorkerId(assignee);
|
||||
taskService.updateByBo(t);
|
||||
}
|
||||
}
|
||||
return json.writeValueAsString(arr);
|
||||
} catch (Exception e) {
|
||||
// 同步失败不影响主流程,仅日志
|
||||
log.warn("会议纪要[{}]待办同步OA任务失败", meeting.getMeetingCode(), e);
|
||||
return meeting.getTasksJson();
|
||||
}
|
||||
}
|
||||
|
||||
private String textOf(ObjectNode n, String k) {
|
||||
private Date parseDay(String s) {
|
||||
if (StringUtils.isBlank(s)) return null;
|
||||
try { return new SimpleDateFormat("yyyy-MM-dd").parse(s); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private ArrayNode parseTaskArray(String s) {
|
||||
if (StringUtils.isBlank(s)) return null;
|
||||
try {
|
||||
JsonNode root = json.readTree(s);
|
||||
return root.isArray() ? (ArrayNode) root : null;
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private String textOf(JsonNode n, String k) {
|
||||
JsonNode v = n.get(k);
|
||||
return v == null || v.isNull() ? null : v.asText();
|
||||
}
|
||||
|
||||
private Long longOf(ObjectNode n, String k) {
|
||||
private Long longOf(JsonNode n, String k) {
|
||||
JsonNode v = n.get(k);
|
||||
if (v == null || v.isNull()) return null;
|
||||
try { return v.isNumber() ? v.asLong() : Long.parseLong(v.asText()); }
|
||||
|
||||
@@ -217,6 +217,8 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
add.setOriginFinishTime(add.getFinishTime());
|
||||
add.setWorkerId(workerId);
|
||||
flag = baseMapper.insert(add) > 0;
|
||||
// 回填任务ID,供调用方(如会议纪要待办同步)建立关联
|
||||
bo.setTaskId(add.getTaskId());
|
||||
if (flag) {
|
||||
operationLogService.recordLog(add.getProjectId(), 3, add.getTaskId(),
|
||||
add.getTaskTitle(), 1, "新增任务: " + add.getTaskTitle(), null, null);
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<el-button size="small" :type="isRecording ? 'danger' : 'warning'"
|
||||
:icon="isRecording ? 'el-icon-video-pause' : 'el-icon-microphone'"
|
||||
@click="cmdRecord">{{ isRecording ? '停止录音' : '语音录入' }}</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-document-checked" @click="cmdSave">保存</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-document-checked"
|
||||
:loading="saving" @click="cmdSave">保存</el-button>
|
||||
<el-button size="small" icon="el-icon-download" @click="cmdExport">导出</el-button>
|
||||
<el-button size="small" icon="el-icon-printer" @click="cmdPrint">打印</el-button>
|
||||
</div>
|
||||
@@ -37,22 +38,22 @@
|
||||
<el-button type="text" icon="el-icon-plus" @click="cmdNew">新建</el-button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<el-input v-model="historyQuery.keyword" size="mini" placeholder="搜索主题 / 地点"
|
||||
<el-input v-model="historyQuery.keyword" size="mini" placeholder="搜索编号 / 主题 / 地点"
|
||||
clearable prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="loadHistory" @clear="loadHistory" />
|
||||
@keyup.enter.native="searchHistory" @clear="searchHistory" />
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<project-select v-model="historyQuery.projectId" placeholder="项目筛选" clearable
|
||||
size="mini" style="flex:1" @input="loadHistory" />
|
||||
size="mini" style="flex:1" @input="searchHistory" />
|
||||
<el-select v-model="historyQuery.meetingType" size="mini" clearable placeholder="类型"
|
||||
style="width:90px;margin-left:4px" @change="loadHistory">
|
||||
<el-option v-for="t in meetingTypes" :key="t.value" :value="t.value" :label="t.label" />
|
||||
style="width:90px;margin-left:4px" @change="searchHistory">
|
||||
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<el-date-picker v-model="historyQuery.dateRange" type="daterange" size="mini" range-separator="~"
|
||||
start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd"
|
||||
style="width:100%" @change="loadHistory" />
|
||||
style="width:100%" @change="searchHistory" />
|
||||
</div>
|
||||
|
||||
<div v-loading="historyLoading" class="hist-list">
|
||||
@@ -64,15 +65,13 @@
|
||||
@click="loadMinutes(m.id)">
|
||||
<div class="hc-top">
|
||||
<span class="hc-code">{{ m.meetingCode }}</span>
|
||||
<el-tag size="mini" :type="typeTag(m.meetingType)" effect="plain">
|
||||
{{ typeLabel(m.meetingType) }}
|
||||
</el-tag>
|
||||
<dict-tag :options="dict.type.oa_meeting_type" :value="m.meetingType" />
|
||||
<el-button type="text" icon="el-icon-delete" class="hc-del"
|
||||
@click.stop="removeMinutes(m)" />
|
||||
</div>
|
||||
<div class="hc-subject">{{ m.subject }}</div>
|
||||
<div class="hc-line"><i class="el-icon-date" /> {{ m.meetingDate }}
|
||||
<span v-if="m.projectNum" class="hc-proj">· {{ m.projectNum }}</span>
|
||||
<span v-if="m.projectName" class="hc-proj">· {{ m.projectName }}</span>
|
||||
</div>
|
||||
<div v-if="m.hostUserName" class="hc-line"><i class="el-icon-s-custom" /> 主持:{{ m.hostUserName }}</div>
|
||||
<div v-if="m.location" class="hc-line"><i class="el-icon-location-outline" /> {{ m.location }}</div>
|
||||
@@ -106,33 +105,35 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="项目">
|
||||
<project-select v-model="form.projectId" placeholder="选择项目" clearable style="width:100%" />
|
||||
<project-select v-model="form.projectId" placeholder="不选则为非项目会议" clearable style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="会议类型">
|
||||
<el-select v-model="form.meetingType" style="width:100%">
|
||||
<el-option v-for="t in meetingTypes" :key="t.value" :value="t.value" :label="t.label" />
|
||||
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="会议主题" required>
|
||||
<el-input v-model="form.subject" placeholder="输入会议主题" />
|
||||
<el-input v-model="form.subject" maxlength="200" placeholder="输入会议主题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="会议地点">
|
||||
<el-input v-model="form.location" placeholder="会议室 / 线上" />
|
||||
<el-input v-model="form.location" maxlength="100" placeholder="会议室 / 线上" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="主持人">
|
||||
<user-select :value="hostCsv" @input="onHostInput" />
|
||||
<div class="help-tip">仅取首位为主持人;如要换人请先点 ×。</div>
|
||||
<el-tag v-if="form.hostUserId" closable @close="clearHost">
|
||||
{{ form.hostUserName || ('#' + form.hostUserId) }}
|
||||
</el-tag>
|
||||
<el-button type="text" @click="pickHost">{{ form.hostUserId ? '更换' : '点击选择' }}</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="参会人员">
|
||||
<user-select v-model="form.attendeeUserIds" />
|
||||
</el-form-item>
|
||||
@@ -172,7 +173,7 @@
|
||||
<div class="sec-block">
|
||||
<div class="sec-hd">
|
||||
<span class="sec-num">四</span> 待办事项
|
||||
<span class="sec-tip">保存时按上方开关自动同步到 OA 任务</span>
|
||||
<span class="sec-tip">填了负责人和内容的待办,保存时按上方开关同步为 OA 任务并通知负责人</span>
|
||||
<el-button type="text" icon="el-icon-plus" class="add-task" @click="addTask">添加待办</el-button>
|
||||
</div>
|
||||
|
||||
@@ -181,11 +182,18 @@
|
||||
<div class="task-line">
|
||||
<div class="tf tf-assignee">
|
||||
<label>负责人</label>
|
||||
<user-select :value="t._assigneeCsv" @input="onTaskAssigneeChange(t, $event)" />
|
||||
<div>
|
||||
<el-tag v-if="t.assigneeUserId" size="small" closable @close="clearAssignee(t)">
|
||||
{{ t.assigneeName || ('#' + t.assigneeUserId) }}
|
||||
</el-tag>
|
||||
<el-button type="text" size="mini" @click="pickAssignee(i)">
|
||||
{{ t.assigneeUserId ? '更换' : '选择' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tf tf-content">
|
||||
<label>任务内容</label>
|
||||
<el-input v-model="t.content" size="mini" placeholder="任务描述..." />
|
||||
<el-input v-model="t.content" size="mini" maxlength="200" placeholder="任务描述..." />
|
||||
</div>
|
||||
<div class="tf tf-deadline">
|
||||
<label>截止日期</label>
|
||||
@@ -195,7 +203,8 @@
|
||||
<div class="tf tf-status">
|
||||
<label>状态</label>
|
||||
<el-select v-model="t.status" size="mini" style="width:100%">
|
||||
<el-option v-for="o in taskStatusOpts" :key="o.value" :value="o.value" :label="o.label" />
|
||||
<el-option v-for="o in dict.type.oa_meeting_task_status" :key="o.value"
|
||||
:value="o.value" :label="o.label" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="tf tf-act">
|
||||
@@ -209,6 +218,9 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 人员单选弹窗(主持人 / 待办负责人共用) -->
|
||||
<user-single-select ref="userPicker" v-model="userPickerVisible" @onSelected="onUserPicked" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -218,33 +230,28 @@ import {
|
||||
updateMeetingMinutes, delMeetingMinutes
|
||||
} from '@/api/oa/meetingMinutes'
|
||||
import UserSelect from '@/components/UserSelect'
|
||||
import UserSingleSelect from '@/components/UserSelect/single'
|
||||
import ProjectSelect from '@/components/fad-service/ProjectSelect'
|
||||
|
||||
const MEETING_TYPES = [
|
||||
{ value: 'tech', label: '技术评审' },
|
||||
{ value: 'project', label: '项目推进' },
|
||||
{ value: 'client', label: '客户沟通' },
|
||||
{ value: 'weekly', label: '周例会' },
|
||||
{ value: 'other', label: '其他' }
|
||||
]
|
||||
const TASK_STATUS = [
|
||||
{ value: 'pending', label: '待办' },
|
||||
{ value: 'progress', label: '进行中' },
|
||||
{ value: 'done', label: '已完成' }
|
||||
]
|
||||
const TYPE_TAG = { tech: 'info', project: 'primary', client: 'warning', weekly: 'success', other: '' }
|
||||
function localToday () {
|
||||
const d = new Date()
|
||||
const p = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
||||
}
|
||||
|
||||
function emptyForm () {
|
||||
return {
|
||||
id: null,
|
||||
meetingCode: '',
|
||||
meetingDate: new Date().toISOString().slice(0, 10),
|
||||
meetingDate: localToday(),
|
||||
projectId: null,
|
||||
meetingType: 'tech',
|
||||
meetingType: 'other',
|
||||
subject: '',
|
||||
location: '',
|
||||
hostUserId: null,
|
||||
hostUserName: '',
|
||||
attendeeUserIds: '',
|
||||
attendeeUserNames: '',
|
||||
topic: '',
|
||||
discussion: '',
|
||||
decision: '',
|
||||
@@ -255,15 +262,14 @@ function emptyForm () {
|
||||
|
||||
export default {
|
||||
name: 'OaMeeting',
|
||||
components: { UserSelect, ProjectSelect },
|
||||
components: { UserSelect, UserSingleSelect, ProjectSelect },
|
||||
dicts: ['oa_meeting_type', 'oa_meeting_task_status'],
|
||||
data () {
|
||||
return {
|
||||
meetingTypes: MEETING_TYPES,
|
||||
taskStatusOpts: TASK_STATUS,
|
||||
|
||||
statusType: 'success',
|
||||
statusText: '就绪',
|
||||
protoWarn: false,
|
||||
saving: false,
|
||||
|
||||
form: emptyForm(),
|
||||
|
||||
@@ -280,8 +286,9 @@ export default {
|
||||
voiceFinalText: '',
|
||||
voiceInterim: '',
|
||||
|
||||
/** 主持人 CSV — 直接绑 UserSelect。@input 单向同步到 form.hostUserId(取首位) */
|
||||
hostCsv: ''
|
||||
// 人员单选弹窗当前服务对象:'host' 或待办行下标
|
||||
userPickerVisible: false,
|
||||
userPickerTarget: 'host'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@@ -296,29 +303,58 @@ export default {
|
||||
const local = host === 'localhost' || host === '127.0.0.1'
|
||||
this.protoWarn = !(location.protocol === 'https:' || local)
|
||||
},
|
||||
typeLabel (v) { return (MEETING_TYPES.find(t => t.value === v) || {}).label || '其他' },
|
||||
typeTag (v) { return TYPE_TAG[v] || '' },
|
||||
dictLabel (dictKey, v) {
|
||||
const hit = (this.dict.type[dictKey] || []).find(t => t.value === v)
|
||||
return hit ? hit.label : (v || '-')
|
||||
},
|
||||
|
||||
/** UserSelect 是多选;主持人只保留首位 */
|
||||
onHostInput (val) {
|
||||
const arr = typeof val === 'string' ? val.split(',').filter(Boolean) : (val || [])
|
||||
this.hostCsv = arr.length ? String(arr[0]) : ''
|
||||
this.form.hostUserId = arr.length ? Number(arr[0]) : null
|
||||
// ============ 人员选择 ============
|
||||
pickHost () {
|
||||
this.userPickerTarget = 'host'
|
||||
this.userPickerVisible = true
|
||||
},
|
||||
clearHost () {
|
||||
this.form.hostUserId = null
|
||||
this.form.hostUserName = ''
|
||||
},
|
||||
pickAssignee (i) {
|
||||
this.userPickerTarget = i
|
||||
this.userPickerVisible = true
|
||||
},
|
||||
clearAssignee (t) {
|
||||
t.assigneeUserId = null
|
||||
t.assigneeName = ''
|
||||
},
|
||||
onUserPicked (row) {
|
||||
if (!row) return
|
||||
if (this.userPickerTarget === 'host') {
|
||||
this.form.hostUserId = row.userId
|
||||
this.form.hostUserName = row.nickName
|
||||
} else {
|
||||
const t = this.form.tasks[this.userPickerTarget]
|
||||
if (t) {
|
||||
t.assigneeUserId = row.userId
|
||||
t.assigneeName = row.nickName
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 待办 ============
|
||||
addTask () {
|
||||
this.form.tasks.push({
|
||||
assigneeUserId: null, _assigneeCsv: '',
|
||||
assigneeUserId: null, assigneeName: '',
|
||||
content: '', deadline: '', status: 'pending', taskId: null
|
||||
})
|
||||
},
|
||||
removeTask (i) { this.form.tasks.splice(i, 1) },
|
||||
/** 待办负责人 UserSelect 多选 → 取首位 */
|
||||
onTaskAssigneeChange (task, val) {
|
||||
const arr = typeof val === 'string' ? val.split(',').filter(Boolean) : (val || [])
|
||||
task.assigneeUserId = arr.length ? Number(arr[0]) : null
|
||||
task._assigneeCsv = arr.length ? String(arr[0]) : ''
|
||||
removeTask (i) {
|
||||
const t = this.form.tasks[i]
|
||||
if (t.taskId) {
|
||||
this.$modal.confirm('该待办已同步为 OA 任务,移除后任务本身不会删除,仅与纪要解除关联。继续?')
|
||||
.then(() => this.form.tasks.splice(i, 1))
|
||||
.catch(() => {})
|
||||
} else {
|
||||
this.form.tasks.splice(i, 1)
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 新建 / 保存 ============
|
||||
@@ -326,16 +362,14 @@ export default {
|
||||
const keepProject = this.form.projectId
|
||||
this.form = emptyForm()
|
||||
if (keepProject) this.form.projectId = keepProject
|
||||
this.hostCsv = ''
|
||||
this.clearVoice()
|
||||
this.$modal.msgSuccess('已新建纪要')
|
||||
},
|
||||
async cmdSave () {
|
||||
if (!this.form.meetingDate) return this.$modal.msgError('请选择会议日期')
|
||||
if (!this.form.subject) return this.$modal.msgError('请输入会议主题')
|
||||
// 序列化时清掉 _assigneeCsv 临时字段
|
||||
const cleanTasks = (this.form.tasks || []).map(t => ({
|
||||
assigneeUserId: t.assigneeUserId,
|
||||
assigneeName: t.assigneeName,
|
||||
content: t.content,
|
||||
deadline: t.deadline,
|
||||
status: t.status,
|
||||
@@ -343,20 +377,32 @@ export default {
|
||||
}))
|
||||
const payload = { ...this.form, tasksJson: JSON.stringify(cleanTasks) }
|
||||
delete payload.tasks
|
||||
this.saving = true
|
||||
try {
|
||||
if (this.form.id) await updateMeetingMinutes(payload)
|
||||
else await addMeetingMinutes(payload)
|
||||
let id = this.form.id
|
||||
if (id) {
|
||||
await updateMeetingMinutes(payload)
|
||||
} else {
|
||||
const res = await addMeetingMinutes(payload)
|
||||
id = res.data
|
||||
}
|
||||
this.setStatus('已保存', 'success')
|
||||
setTimeout(() => this.setStatus('就绪', 'success'), 2000)
|
||||
this.$modal.msgSuccess('纪要已保存' + (this.form.syncTask ? ',待办已同步到 OA 任务' : ''))
|
||||
await this.loadHistory()
|
||||
if (this.form.id) await this.loadMinutes(this.form.id, true)
|
||||
if (id) await this.loadMinutes(id, true)
|
||||
} catch (err) {
|
||||
this.$modal.msgError(err.msg || '保存失败')
|
||||
this.setStatus('保存失败', 'warning')
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 历史 ============
|
||||
searchHistory () {
|
||||
this.historyQuery.pageNum = 1
|
||||
this.loadHistory()
|
||||
},
|
||||
async loadHistory () {
|
||||
this.historyLoading = true
|
||||
try {
|
||||
@@ -387,14 +433,15 @@ export default {
|
||||
subject: m.subject || '',
|
||||
location: m.location || '',
|
||||
hostUserId: m.hostUserId || null,
|
||||
hostUserName: m.hostUserName || '',
|
||||
attendeeUserIds: m.attendeeUserIds || '',
|
||||
attendeeUserNames: m.attendeeUserNames || '',
|
||||
topic: m.topic || '',
|
||||
discussion: m.discussion || '',
|
||||
decision: m.decision || '',
|
||||
tasks: this.parseTasks(m.tasksJson),
|
||||
syncTask: m.syncTask == null ? 1 : m.syncTask
|
||||
}
|
||||
this.hostCsv = m.hostUserId ? String(m.hostUserId) : ''
|
||||
if (!silent) this.$modal.msgSuccess('已加载:' + m.subject)
|
||||
},
|
||||
parseTasks (s) {
|
||||
@@ -404,7 +451,7 @@ export default {
|
||||
if (!Array.isArray(a)) return []
|
||||
return a.map(t => ({
|
||||
assigneeUserId: t.assigneeUserId || null,
|
||||
_assigneeCsv: t.assigneeUserId ? String(t.assigneeUserId) : '',
|
||||
assigneeName: t.assigneeName || '',
|
||||
content: t.content || '',
|
||||
deadline: t.deadline || '',
|
||||
status: t.status || 'pending',
|
||||
@@ -413,10 +460,10 @@ export default {
|
||||
} catch (e) { return [] }
|
||||
},
|
||||
removeMinutes (m) {
|
||||
this.$modal.confirm(`确认删除「${m.subject}」?此操作不可恢复。`).then(async () => {
|
||||
this.$modal.confirm(`确认删除「${m.subject}」?已同步的 OA 任务不受影响。`).then(async () => {
|
||||
await delMeetingMinutes(m.id)
|
||||
this.$modal.msgSuccess('已删除')
|
||||
if (this.form.id === m.id) this.form = emptyForm()
|
||||
if (this.form.id === m.id) this.cmdNew()
|
||||
await this.loadHistory()
|
||||
}).catch(() => {})
|
||||
},
|
||||
@@ -494,14 +541,15 @@ export default {
|
||||
cmdExport () {
|
||||
if (!this.form.subject) return this.$modal.msgError('无内容可导出')
|
||||
const d = this.form
|
||||
const typeLabel = this.typeLabel(d.meetingType)
|
||||
const statusMap = { pending: '待办', progress: '进行中', done: '已完成' }
|
||||
const lines = []
|
||||
lines.push('德睿福成套设备有限公司 · 会议纪要')
|
||||
lines.push('='.repeat(50))
|
||||
lines.push('日期: ' + d.meetingDate + ' 类型: ' + typeLabel)
|
||||
lines.push('编号: ' + (d.meetingCode || '-'))
|
||||
lines.push('日期: ' + d.meetingDate + ' 类型: ' + this.dictLabel('oa_meeting_type', d.meetingType))
|
||||
lines.push('主题: ' + d.subject)
|
||||
lines.push('地点: ' + (d.location || '-'))
|
||||
lines.push('主持: ' + (d.hostUserName || '-'))
|
||||
lines.push('参会: ' + (d.attendeeUserNames || '-'))
|
||||
lines.push('='.repeat(50))
|
||||
lines.push('')
|
||||
lines.push('一、会议议题'); lines.push('-'.repeat(30)); lines.push(d.topic || '(无)'); lines.push('')
|
||||
@@ -510,8 +558,8 @@ export default {
|
||||
lines.push('四、待办事项'); lines.push('-'.repeat(30))
|
||||
if (d.tasks && d.tasks.length) {
|
||||
d.tasks.forEach(t => {
|
||||
lines.push(' • [#' + (t.assigneeUserId || '-') + '] ' + (t.content || '') +
|
||||
' | 截止:' + (t.deadline || '-') + ' | 状态:' + (statusMap[t.status] || t.status))
|
||||
lines.push(' • [' + (t.assigneeName || '未指派') + '] ' + (t.content || '') +
|
||||
' | 截止:' + (t.deadline || '-') + ' | 状态:' + this.dictLabel('oa_meeting_task_status', t.status))
|
||||
})
|
||||
} else { lines.push('(无)') }
|
||||
lines.push(''); lines.push('='.repeat(50))
|
||||
@@ -528,12 +576,11 @@ export default {
|
||||
const d = this.form
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
|
||||
const statusMap = { pending: '待办', progress: '进行中', done: '已完成' }
|
||||
let taskHtml = '(无)'
|
||||
if (d.tasks && d.tasks.length) {
|
||||
const rows = d.tasks.map(t =>
|
||||
`<tr><td>#${esc(t.assigneeUserId || '-')}</td><td>${esc(t.content)}</td>` +
|
||||
`<td>${esc(t.deadline || '-')}</td><td>${esc(statusMap[t.status] || t.status)}</td></tr>`
|
||||
`<tr><td>${esc(t.assigneeName || '未指派')}</td><td>${esc(t.content)}</td>` +
|
||||
`<td>${esc(t.deadline || '-')}</td><td>${esc(this.dictLabel('oa_meeting_task_status', t.status))}</td></tr>`
|
||||
).join('')
|
||||
taskHtml = `<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
|
||||
<tr style="background:#eee"><th>负责人</th><th>任务内容</th><th>截止</th><th>状态</th></tr>${rows}</table>`
|
||||
@@ -547,10 +594,13 @@ export default {
|
||||
.meta span{margin-right:18px}.sect{font-size:14px;margin:14px 0 6px;font-weight:700}
|
||||
.body{white-space:pre-wrap;font-size:13px;margin-bottom:16px}
|
||||
@media print{body{padding:20px}}</style></head><body>
|
||||
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要</div>
|
||||
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要 ${esc(d.meetingCode || '')}</div>
|
||||
<div class="meta"><span>📅 ${esc(d.meetingDate)}</span>
|
||||
<span>📝 ${esc(d.subject)}</span></div>
|
||||
<div class="meta"><span>📍 ${esc(d.location || '-')}</span></div>
|
||||
<span>📝 ${esc(d.subject)}</span>
|
||||
<span>🏷 ${esc(this.dictLabel('oa_meeting_type', d.meetingType))}</span></div>
|
||||
<div class="meta"><span>📍 ${esc(d.location || '-')}</span>
|
||||
<span>🎤 主持:${esc(d.hostUserName || '-')}</span></div>
|
||||
<div class="meta"><span>👥 参会:${esc(d.attendeeUserNames || '-')}</span></div>
|
||||
<div class="sect">一、会议议题</div><div class="body">${esc(d.topic || '(无)')}</div>
|
||||
<div class="sect">二、讨论内容</div><div class="body">${esc(d.discussion || '(无)')}</div>
|
||||
<div class="sect">三、决议事项</div><div class="body">${esc(d.decision || '(无)')}</div>
|
||||
@@ -627,7 +677,6 @@ export default {
|
||||
|
||||
.meta-form {
|
||||
::v-deep .el-form-item { margin-bottom: 8px; }
|
||||
.help-tip { color: #909399; font-size: 11px; line-height: 1.4; }
|
||||
}
|
||||
|
||||
.voice-card {
|
||||
@@ -670,7 +719,7 @@ export default {
|
||||
}
|
||||
.task-line {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 140px 110px 100px;
|
||||
grid-template-columns: 170px 1fr 140px 110px 100px;
|
||||
gap: 8px; align-items: start;
|
||||
.tf {
|
||||
label { display: block; font-size: 11px; color: #909399; margin-bottom: 2px; }
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
-- =====================================================
|
||||
-- 智能会议纪要 (Smart Meeting Minutes)
|
||||
-- - 弃用 oa_meeting_project,改为绑定 sys_oa_project.project_id
|
||||
-- - 会议可绑定 sys_oa_project.project_id,也可不绑定(非项目会议)
|
||||
-- - 主持人/参会/待办负责人 统一存 user_id
|
||||
-- - 待办在保存时可同步生成 sys_oa_task
|
||||
-- - 待办在保存时可同步生成 sys_oa_task(带操作日志和 IM 通知)
|
||||
-- 本脚本可重复执行(幂等)。
|
||||
-- 注意:sys_dict_type/sys_dict_data/sys_menu 主键为雪花ID(非自增),必须显式指定。
|
||||
-- =====================================================
|
||||
|
||||
-- 清理上一版(如有)
|
||||
DROP TABLE IF EXISTS `oa_meeting_project`;
|
||||
|
||||
-- ---------------- 会议纪要 ----------------
|
||||
DROP TABLE IF EXISTS `oa_meeting_minutes`;
|
||||
CREATE TABLE `oa_meeting_minutes` (
|
||||
-- ---------------- 会议纪要表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `oa_meeting_minutes` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`meeting_code` varchar(32) NOT NULL COMMENT '会议编号 MT-yyyyMMddHHmmss',
|
||||
`meeting_code` varchar(32) NOT NULL COMMENT '会议编号 MT-yyyyMMddHHmmss+3位随机',
|
||||
`meeting_date` date NOT NULL COMMENT '会议日期',
|
||||
`project_id` bigint(20) DEFAULT NULL COMMENT '关联 sys_oa_project.project_id',
|
||||
`meeting_type` varchar(20) DEFAULT 'other' COMMENT '类型 tech/project/client/weekly/other',
|
||||
`project_id` bigint(20) DEFAULT NULL COMMENT '关联 sys_oa_project.project_id(可空=非项目会议)',
|
||||
`meeting_type` varchar(20) DEFAULT 'other' COMMENT '类型 字典 oa_meeting_type',
|
||||
`subject` varchar(500) NOT NULL COMMENT '会议主题',
|
||||
`location` varchar(255) DEFAULT NULL COMMENT '会议地点',
|
||||
`host_user_id` bigint(20) DEFAULT NULL COMMENT '主持人 sys_user.user_id',
|
||||
@@ -23,13 +21,13 @@ CREATE TABLE `oa_meeting_minutes` (
|
||||
`topic` text COMMENT '会议议题',
|
||||
`discussion` text COMMENT '讨论内容',
|
||||
`decision` text COMMENT '决议事项',
|
||||
`tasks_json` text COMMENT '待办 JSON:[{assigneeUserId,content,deadline,status,taskId}]',
|
||||
`tasks_json` text COMMENT '待办 JSON:[{assigneeUserId,assigneeName,content,deadline,status,taskId}]',
|
||||
`sync_task` tinyint(1) DEFAULT 1 COMMENT '是否将待办同步为 OA 任务',
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志:0正常 1删除',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志:0正常 2删除(mybatis-plus logicDeleteValue=2)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_meeting_code` (`meeting_code`, `del_flag`),
|
||||
KEY `idx_date` (`meeting_date`),
|
||||
@@ -37,26 +35,60 @@ CREATE TABLE `oa_meeting_minutes` (
|
||||
KEY `idx_type` (`meeting_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会议纪要';
|
||||
|
||||
-- ---------------- 清理早期错误数据(dict_id/dict_code 误插为 0) ----------------
|
||||
DELETE FROM `sys_dict_type` WHERE `dict_id` = 0 AND `dict_type` IN ('oa_meeting_type', 'oa_meeting_task_status');
|
||||
DELETE FROM `sys_dict_data` WHERE `dict_code` = 0 AND `dict_type` IN ('oa_meeting_type', 'oa_meeting_task_status');
|
||||
|
||||
-- ---------------- 字典:会议类型 ----------------
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES ('会议类型', 'oa_meeting_type', '0', 'admin', NOW(), '智能会议纪要-会议类型');
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES (2063900000000000001, '会议类型', 'oa_meeting_type', '0', 'admin', NOW(), '智能会议纪要-会议类型');
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_data`
|
||||
(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES
|
||||
(1, '技术评审', 'tech', 'oa_meeting_type', '', 'info', 'N', '0', 'admin', NOW(), ''),
|
||||
(2, '项目推进', 'project', 'oa_meeting_type', '', 'primary', 'N', '0', 'admin', NOW(), ''),
|
||||
(3, '客户沟通', 'client', 'oa_meeting_type', '', 'warning', 'N', '0', 'admin', NOW(), ''),
|
||||
(4, '周例会', 'weekly', 'oa_meeting_type', '', 'success', 'N', '0', 'admin', NOW(), ''),
|
||||
(5, '其他', 'other', 'oa_meeting_type', '', '', 'Y', '0', 'admin', NOW(), '');
|
||||
(2063900000000000011, 1, '技术评审', 'tech', 'oa_meeting_type', '', 'info', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000012, 2, '项目推进', 'project', 'oa_meeting_type', '', 'primary', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000013, 3, '客户沟通', 'client', 'oa_meeting_type', '', 'warning', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000014, 4, '周例会', 'weekly', 'oa_meeting_type', '', 'success', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000015, 5, '其他', 'other', 'oa_meeting_type', '', 'default', 'Y', '0', 'admin', NOW(), '');
|
||||
|
||||
-- ---------------- 字典:待办状态 ----------------
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES ('会议待办状态', 'oa_meeting_task_status', '0', 'admin', NOW(), '智能会议纪要-待办状态');
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES (2063900000000000002, '会议待办状态', 'oa_meeting_task_status', '0', 'admin', NOW(), '智能会议纪要-待办状态');
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_data`
|
||||
(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES
|
||||
(1, '待办', 'pending', 'oa_meeting_task_status', '', 'info', 'Y', '0', 'admin', NOW(), ''),
|
||||
(2, '进行中', 'progress', 'oa_meeting_task_status', '', 'primary', 'N', '0', 'admin', NOW(), ''),
|
||||
(3, '已完成', 'done', 'oa_meeting_task_status', '', 'success', 'N', '0', 'admin', NOW(), '');
|
||||
(2063900000000000021, 1, '待办', 'pending', 'oa_meeting_task_status', '', 'info', 'Y', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000022, 2, '进行中', 'progress', 'oa_meeting_task_status', '', 'primary', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063900000000000023, 3, '已完成', 'done', 'oa_meeting_task_status', '', 'success', 'N', '0', 'admin', NOW(), '');
|
||||
|
||||
-- ---------------- 菜单:信息 > 会议纪要 ----------------
|
||||
-- 父菜单 1774989374680858626 = 「信息」
|
||||
INSERT IGNORE INTO `sys_menu`
|
||||
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
(2063809716454174722, '会议纪要', 1774989374680858626, 3,
|
||||
'meeting', 'oa/meeting/index', 'C', '0', '0',
|
||||
NULL, 'documentation', 'admin', NOW());
|
||||
|
||||
UPDATE `sys_menu` SET `icon` = 'documentation' WHERE `menu_id` = 2063809716454174722 AND (`icon` = '#' OR `icon` IS NULL);
|
||||
|
||||
-- ---------------- 角色授权(与「信息」下兄弟菜单一致的角色集) ----------------
|
||||
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||
VALUES
|
||||
(1743186990678077442, 2063809716454174722), -- 总经理
|
||||
(1743204526291349506, 2063809716454174722), -- 技术总监
|
||||
(1743205028123045890, 2063809716454174722), -- 信息化部
|
||||
(1852970465740505090, 2063809716454174722), -- 普通员工
|
||||
(1859257980152692738, 2063809716454174722), -- 职工
|
||||
(1859548445766717441, 2063809716454174722), -- 后勤
|
||||
(1893987128812761089, 2063809716454174722), -- 新员工临时身份
|
||||
(1914212623781187585, 2063809716454174722), -- 技术总工
|
||||
(1914213026883162113, 2063809716454174722), -- 设计主任
|
||||
(1925062159919448065, 2063809716454174722); -- 外贸专责
|
||||
|
||||
-- ---------------- 校验 ----------------
|
||||
SELECT menu_id, menu_name, path, component, icon FROM sys_menu WHERE menu_id = 2063809716454174722;
|
||||
SELECT dict_type, dict_label, dict_value FROM sys_dict_data
|
||||
WHERE dict_type IN ('oa_meeting_type', 'oa_meeting_task_status') ORDER BY dict_type, dict_sort;
|
||||
|
||||
Reference in New Issue
Block a user