buhuazhen
2025-09-17 cf82be63b9416bcb5d235336eeb067397fe14ec5
final
已添加8个文件
已修改1个文件
3153 ■■■■■ 文件已修改
src/api/collaborativeApproval/meeting.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 398 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue 418 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue 416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/summary/index.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/meeting.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
import request from "@/utils/request";
export function getMeetingRoomList(data) {
    return request({
        url: "/meeting/roomList",
        method: "post",
        data: data,
    });
}
export function saveRoom(data) {
    return request({
        url: "/meeting/saveRoom",
        method: "post",
        data: data,
    });
}
export function delRoom(id) {
    return request({
        url: "/meeting/delRoom/"+id,
        method: "delete",
    });
}
export function getRoomEnum() {
    return request({
        url: "/meeting/roomEnum",
        method: "get",
    });
}
export function getDraftList(data){
    return request({
        url: "/meeting/draftList",
        method: "post",
        data: data,
    });
}
export function saveDraft(data) {
    return request({
        url: "/meeting/saveDraft",
        method: "post",
        data: data,
    });
}
export function delDraft(id) {
    return request({
        url: "/meeting/delDraft/"+id,
        method: "delete",
    });
}
export function saveMeetingApplication(data){
    return request({
        url: "/meeting/saveMeetingApplication",
        method: "post",
        data: data,
    });
}
export function getExamineList(data) {
    return request({
        url: "/meeting/applicationList",
        method: "post",
        data: data,
    });
}
export function getMeetingUseList(data){
    return request({
        url: "/meeting/meetingUseList",
        method: "post",
        data: data,
    });
}
export function getMeetingPublish(data){
    return request({
        url: "/meeting/meetingPublishList",
        method: "post",
        data: data
    });
}
export function getMeetingMinutesByMeetingId(id){
    return request({
        url: "/meeting/getMeetingMinutesByMeetingId/"+id,
        method: "get",
    });
}
export function saveMeetingMinutes(data){
    return request({
        url: "/meeting/saveMeetingMinutes",
        method: "post",
        data: data,
    });
}
export function getMeetSummary(){
    return request({
        url: "/meeting/getMeetSummary",
        method: "get",
    });
}
export function getMeetSummaryItems(){
    return request({
        url: "/meeting/getMeetSummaryItems",
        method: "get",
    });
}
src/views/collaborativeApproval/meetingBoard/index.vue
@@ -16,7 +16,7 @@
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.ongoing }}</div>
          <div class="stat-number">{{ stats.underWay }}</div>
          <div class="stat-label">进行中</div>
        </div>
      </el-card>
@@ -28,7 +28,7 @@
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.upcoming }}</div>
          <div class="stat-number">{{ stats.toStart }}</div>
          <div class="stat-label">即将开始</div>
        </div>
      </el-card>
@@ -45,11 +45,11 @@
            </el-tag>
          </div>
          <div class="meeting-time">
            <el-icon><Clock /></el-icon>
            {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
             {{dayjs(meeting.startTime).format("YYYY-MM-DD")}}<el-icon><Clock /></el-icon>
           {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
          </div>
        </div>
        <div class="meeting-info">
          <div class="info-item">
            <el-icon><Location /></el-icon>
@@ -66,79 +66,18 @@
        </div>
        <div class="meeting-agenda">
          <h4>议程安排</h4>
          <h4>会议纪要</h4>
          <div class="agenda-list">
            <div
              v-for="(agenda, index) in meeting.agenda"
              :key="index"
              class="agenda-item"
              :class="{ 'active': agenda.status === 'active', 'completed': agenda.status === 'completed' }"
            >
              <span class="agenda-time">{{ agenda.time }}</span>
              <span class="agenda-content">{{ agenda.content }}</span>
              <el-tag
                :type="getAgendaStatusType(agenda.status)"
                size="small"
              >
                {{ getAgendaStatusText(agenda.status) }}
              </el-tag>
            <div class="editor-container">
              <div
                  v-html="meeting.content"
              />
            </div>
          </div>
        </div>
<!--        <div class="meeting-actions">-->
<!--          <el-button type="primary" size="small" @click="joinMeeting(meeting)">-->
<!--            åŠ å…¥ä¼šè®®-->
<!--          </el-button>-->
<!--          <el-button type="info" size="small" @click="viewDetails(meeting)">-->
<!--            æŸ¥çœ‹è¯¦æƒ…-->
<!--          </el-button>-->
<!--          <el-button type="warning" size="small" @click="editMeeting(meeting)">-->
<!--            ç¼–辑-->
<!--          </el-button>-->
<!--        </div>-->
      </el-card>
    </div>
    <!-- åˆ›å»ºä¼šè®®å¯¹è¯æ¡† -->
    <el-dialog v-model="dialogVisible" title="创建会议" width="600px">
      <el-form :model="meetingForm" label-width="100px">
        <el-form-item label="会议标题">
          <el-input v-model="meetingForm.title" placeholder="请输入会议标题" />
        </el-form-item>
        <el-form-item label="会议时间">
          <el-date-picker
            v-model="meetingForm.timeRange"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DD HH:mm:ss"
          />
        </el-form-item>
        <el-form-item label="会议地点">
          <el-input v-model="meetingForm.location" placeholder="请输入会议地点" />
        </el-form-item>
        <el-form-item label="主持人">
          <el-input v-model="meetingForm.host" placeholder="请输入主持人姓名" />
        </el-form-item>
        <el-form-item label="会议描述">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入会议描述"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitMeeting">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
@@ -146,63 +85,21 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
import Editor from "@/components/Editor/index.vue";
import {getMeetSummaryItems,getMeetSummary} from '@/api/collaborativeApproval/meeting.js'
import dayjs from "dayjs";
// ç»Ÿè®¡æ•°æ®
const stats = reactive({
  total: 12,
  ongoing: 3,
  completed: 7,
  upcoming: 2
const stats = ref({
  total: 0,
  underWay: 0,
  completed: 0,
  toStart: 0
})
// ä¼šè®®æ•°æ®
const meetings = ref([
  {
    id: 1,
    title: '产品开发周会',
    status: 'ongoing',
    startTime: '2025-01-15 09:00:00',
    endTime: '2025-01-15 10:30:00',
    location: '会议室A',
    host: '陈志强',
    participants: ['陈志强', '刘雅婷', '王建国', '赵丽华'],
    agenda: [
      { time: '09:00-09:15', content: '上周工作总结', status: 'completed' },
      { time: '09:15-09:45', content: '本周开发计划', status: 'active' },
      { time: '09:45-10:00', content: '技术难点讨论', status: 'pending' },
      { time: '10:00-10:30', content: '问题反馈与解决', status: 'pending' }
    ]
  },
  {
    id: 2,
    title: '客户需求评审会',
    status: 'upcoming',
    startTime: '2025-01-15 14:00:00',
    endTime: '2025-01-15 15:00:00',
    location: '线上会议',
    host: '陈志强',
    participants: ['陈志强', '刘雅婷', '孙明华', '客户代表'],
    agenda: [
      { time: '14:00-14:20', content: '需求背景介绍', status: 'pending' },
      { time: '14:20-14:40', content: '功能需求分析', status: 'pending' },
      { time: '14:40-15:00', content: '技术可行性评估', status: 'pending' }
    ]
  },
  {
    id: 3,
    title: '团队建设活动',
    status: 'completed',
    startTime: '2025-01-14 16:00:00',
    endTime: '2025-01-14 18:00:00',
    location: '公司大厅',
    host: '人事部',
    participants: ['全体员工'],
    agenda: [
      { time: '16:00-16:30', content: '团队游戏', status: 'completed' },
      { time: '16:30-17:00', content: '经验分享', status: 'completed' },
      { time: '17:00-18:00', content: '自由交流', status: 'completed' }
    ]
  }
])
// å¯¹è¯æ¡†ç›¸å…³
@@ -218,9 +115,9 @@
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    'ongoing': 'success',
    'upcoming': 'warning',
    'completed': 'info'
    '2': 'success',
    '1': 'warning',
    '0': 'info'
  }
  return statusMap[status] || 'info'
}
@@ -228,9 +125,9 @@
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    'ongoing': '进行中',
    'upcoming': '即将开始',
    'completed': '已完成'
    '2': '进行中',
    '1': '即将开始',
    '0': '已完成'
  }
  return statusMap[status] || '未知'
}
@@ -261,65 +158,16 @@
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// åˆ›å»ºä¼šè®®
const createMeeting = () => {
  dialogVisible.value = true
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm, {
    title: '',
    timeRange: [],
    location: '',
    host: '',
    description: ''
onMounted( async () => {
  let [resp1,resp2] = await Promise.all([getMeetSummary(),getMeetSummaryItems()])
  stats.value = resp1.data
  meetings.value = resp2.data.map(item => {
    return {
      ...item,
      participants: JSON.parse(item.participants)
    }
  })
}
// æäº¤ä¼šè®®
const submitMeeting = () => {
  if (!meetingForm.title || !meetingForm.timeRange.length || !meetingForm.location || !meetingForm.host) {
    ElMessage.warning('请填写完整的会议信息')
    return
  }
  // åˆ›å»ºæ–°ä¼šè®®
  const newMeeting = {
    id: Date.now(),
    title: meetingForm.title,
    status: 'upcoming',
    startTime: meetingForm.timeRange[0],
    endTime: meetingForm.timeRange[1],
    location: meetingForm.location,
    host: meetingForm.host,
    participants: [meetingForm.host],
    agenda: [
      { time: '待定', content: '议程待定', status: 'pending' }
    ]
  }
  meetings.value.unshift(newMeeting)
  stats.total++
  stats.upcoming++
  ElMessage.success('会议创建成功')
  dialogVisible.value = false
}
// åŠ å…¥ä¼šè®®
const joinMeeting = (meeting) => {
  ElMessage.success(`已加入会议:${meeting.title}`)
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetails = (meeting) => {
  ElMessage.info(`查看会议详情:${meeting.title}`)
}
// ç¼–辑会议
const editMeeting = (meeting) => {
  ElMessage.info(`编辑会议:${meeting.title}`)
}
onMounted(() => {
  console.log('会议看板页面加载完成')
})
</script>
@@ -480,19 +328,19 @@
  .stats-cards {
    grid-template-columns: repeat(2, 1fr);
  }
  .meeting-header {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-info {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-actions {
    flex-direction: column;
  }
}
</style>
</style>
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,398 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议申请</h2>
    </div>
    <!-- ç”³è¯·ç±»åž‹é€‰æ‹© -->
    <el-card class="type-card">
      <div class="type-selector">
        <div
            v-for="type in applicationTypes"
            :key="type.value"
            class="type-item"
            :class="{ active: currentType === type.value }"
            @click="changeType(type.value)"
        >
          <div class="type-icon">
            <el-icon :size="24"><component :is="type.icon"/></el-icon>
          </div>
          <div class="type-info">
            <div class="type-name">{{ type.name }}</div>
            <div class="type-desc">{{ type.desc }}</div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- ä¼šè®®ç”³è¯·è¡¨å• -->
    <el-card>
      <div class="form-header">
        <h3>{{ getCurrentTypeName() }}申请</h3>
      </div>
      <el-form
          ref="meetingFormRef"
          :model="meetingForm"
          :rules="rules"
          label-width="100px"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议主题" prop="title">
              <el-input v-model="meetingForm.title" placeholder="请输入会议主题"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议室" prop="roomId">
              <el-select v-model="meetingForm.roomId" placeholder="请选择会议室" style="width: 100%">
                <el-option
                    v-for="room in meetingRooms"
                    :key="room.id"
                    :label="`${room.name} (${room.location})`"
                    :value="room.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="主持人" prop="host">
              <el-input v-model="meetingForm.host" placeholder="请输入主持人姓名"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议日期" prop="meetingDate">
              <el-date-picker
                  v-model="meetingForm.meetingDate"
                  type="date"
                  placeholder="请选择会议日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  :disabled-date="disabledDate"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <!-- ç©ºåˆ—,保持布局 -->
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-select
                  v-model="meetingForm.startTime"
                  placeholder="请选择开始时间"
                  style="width: 100%"
              >
                <el-option
                    v-for="time in timeOptions"
                    :key="time.value"
                    :label="time.label"
                    :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-select
                  v-model="meetingForm.endTime"
                  placeholder="请选择结束时间"
                  style="width: 100%"
              >
                <el-option
                    v-for="time in timeOptions"
                    :key="time.value"
                    :label="time.label"
                    :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="参会人员" prop="participants">
          <el-select
              v-model="meetingForm.participants"
              multiple
              filterable
              placeholder="请选择参会人员"
              style="width: 100%"
          >
            <el-option
                v-for="person in employees"
                :key="person.id"
                :label="`${person.staffName} (${person.postJob})`"
                :value="person.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="会议说明" prop="description">
          <el-input
              v-model="meetingForm.description"
              type="textarea"
              :rows="4"
              placeholder="请输入会议说明"
          />
        </el-form-item>
      </el-form>
      <div class="form-footer">
        <el-button @click="resetForm">重置</el-button>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </div>
    </el-card>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue'
import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
// å½“前申请类型
const currentType = ref('department') // approval: å®¡æ‰¹æµç¨‹, department: éƒ¨é—¨çº§, notification: é€šçŸ¥å‘布
// ç”³è¯·ç±»åž‹é€‰é¡¹
const applicationTypes = ref([
  {
    value: 'approval',
    name: '审批流程会议',
    desc: '需要经过多级审批的会议申请',
    icon: Document
  },
  {
    value: 'department',
    name: '部门级会议',
    desc: '部门内部会议申请流程',
    icon: Promotion
  },
  {
    value: 'notification',
    name: '会议通知',
    desc: '无需审批直接发布的会议通知',
    icon: Bell
  }
])
// è¡¨å•数据
const meetingForm = reactive({
  title: '',
  type: '',
  roomId: '',
  host: '',
  meetingDate: '',
  startTime: '',
  endTime: '',
  participants: [],
  description: ''
})
// è¡¨å•校验规则
const rules = {
  title: [{required: true, message: '请输入会议主题', trigger: 'blur'}],
  roomId: [{required: true, message: '请选择会议室', trigger: 'change'}],
  host: [{required: true, message: '请输入主持人', trigger: 'blur'}],
  meetingDate: [{required: true, message: '请选择会议日期', trigger: 'change'}],
  startTime: [{required: true, message: '请选择开始时间', trigger: 'change'}],
  endTime: [{required: true, message: '请选择结束时间', trigger: 'change'}],
  participants: [{required: true, message: '请选择参会人员', trigger: 'change'}]
}
// è¡¨å•引用
const meetingFormRef = ref(null)
// ä¼šè®®å®¤åˆ—表
const meetingRooms = ref([])
// å‘˜å·¥åˆ—表
const employees = ref([])
// æ—¶é—´é€‰é¡¹ï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”)
const timeOptions = ref([])
// åˆå§‹åŒ–时间选项
const initTimeOptions = () => {
  const options = []
  for (let hour = 8; hour <= 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªé€‰é¡¹ï¼šæ•´ç‚¹å’ŒåŠç‚¹
    options.push({
      value: `${hour.toString().padStart(2, '0')}:00`,
      label: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // 18:00之后没有半点选项
      options.push({
        value: `${hour.toString().padStart(2, '0')}:30`,
        label: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeOptions.value = options
}
// ç¦ç”¨æ—¥æœŸï¼ˆç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸï¼‰
const disabledDate = (time) => {
  // ç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸ
  return time.getTime() < Date.now() - 86400000
}
// åˆ‡æ¢ç”³è¯·ç±»åž‹
const changeType = (type) => {
  currentType.value = type
}
// èŽ·å–å½“å‰ç±»åž‹åç§°
const getCurrentTypeName = () => {
  const type = applicationTypes.value.find(t => t.value === currentType.value)
  return type ? type.name : ''
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  meetingFormRef.value?.resetFields()
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingFormRef.value?.validate((valid) => {
    if (valid) {
      let formData = {...meetingForm}
      formData.applicationType = currentType.value
      formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00`
      formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00`
      formData.participants = JSON.stringify(formData.participants)
      console.log(formData)
      saveMeetingApplication(formData).then(() => {
        // æ¨¡æ‹Ÿæäº¤æ“ä½œ
        ElMessage.success(`${getCurrentTypeName()}提交成功`)
        // æ ¹æ®ä¸åŒç±»åž‹æ‰§è¡Œä¸åŒæ“ä½œ
        switch (currentType.value) {
          case 'approval':
            ElMessage.info('会议已提交审批流程')
            break
          case 'department':
            ElMessage.info('部门级会议申请已提交')
            break
          case 'notification':
            ElMessage.info('会议通知已发布')
            break
        }
        resetForm()
      })
    }
  })
}
// é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–
onMounted(() => {
  initTimeOptions()
  getRoomEnum().then(res => {
    meetingRooms.value = res.data
  })
  getStaffOnJob().then(res => {
    employees.value = res.data.sort((a, b) => a.postJob.localeCompare(b.postJob))
  })
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.type-card {
  margin-bottom: 20px;
}
.type-selector {
  display: flex;
  gap: 20px;
}
.type-item {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}
.type-item:hover {
  border-color: #409eff;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.type-item.active {
  border-color: #409eff;
  background-color: #ecf5ff;
}
.type-icon {
  margin-right: 15px;
  color: #409eff;
}
.type-name {
  font-size: 16px;
  font-weight: 500;
  color: #303133;
  margin-bottom: 5px;
}
.type-desc {
  font-size: 14px;
  color: #909399;
}
.form-header {
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 1px solid #ebeef5;
}
.form-header h3 {
  margin: 0;
  color: #303133;
}
.form-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #ebeef5;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,495 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议草稿</h2>
      <el-button type="primary" @click="handleAdd">
        <el-icon><Plus /></el-icon>
        æ–°å»ºè‰ç¨¿
      </el-button>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" label-width="100px" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable />
        </el-form-item>
        <el-form-item label="会议日期">
          <el-date-picker
            v-model="searchForm.meetingDate"
            type="date"
            placeholder="请选择会议日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- è‰ç¨¿åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="draftList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip />
        <el-table-column prop="room" label="会议室" align="center" width="120" />
        <el-table-column prop="host" label="主持人" align="center" width="120" />
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants }}人
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" align="center" width="180" />
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDraft(scope.row)">查看</el-button>
            <el-button type="primary" link @click="editDraft(scope.row)">编辑</el-button>
            <el-button type="danger" link @click="deleteDraft(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è‰ç¨¿è¯¦æƒ…对话框 -->
    <el-dialog
      title="会议草稿详情"
      v-model="detailDialogVisible"
      width="800px"
    >
      <div v-if="currentDraft">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentDraft.title }}</el-descriptions-item>
          <el-descriptions-item label="会议编号">{{ currentDraft.meetingId }}</el-descriptions-item>
          <el-descriptions-item label="会议室">{{ currentDraft.room }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentDraft.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentDraft.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ currentDraft.createTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            {{ currentDraft.participantList }}
          </div>
        </div>
        <div class="content-section mt-20">
          <h4>会议说明</h4>
          <div class="meeting-description">{{ currentDraft.description }}</div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ–°å»º/编辑草稿对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="editDialogVisible"
      width="700px"
    >
      <el-form :model="meetingForm" :rules="rules" ref="meetingFormRef" label-width="100px">
        <el-form-item label="会议主题" prop="title">
          <el-input v-model="meetingForm.title" placeholder="请输入会议主题" />
        </el-form-item>
        <el-form-item label="会议室" prop="room">
          <el-select v-model="meetingForm.roomId" placeholder="请选择会议室" style="width: 100%">
            <el-option v-for="(v,k) in roomList" :label="v.name" :value="v.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="主持人" prop="host">
          <el-input v-model="meetingForm.host" placeholder="请输入主持人" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议日期" prop="meetingDate">
              <el-date-picker
                v-model="meetingForm.meetingDate"
                type="date"
                placeholder="请选择会议日期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                :disabled-date="disabledDate"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <!-- ç©ºåˆ—,保持布局 -->
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-select
                v-model="meetingForm.startTime"
                placeholder="请选择开始时间"
                style="width: 100%"
              >
                <el-option
                  v-for="time in timeOptions"
                  :key="time.value"
                  :label="time.label"
                  :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-select
                v-model="meetingForm.endTime"
                placeholder="请选择结束时间"
                style="width: 100%"
              >
                <el-option
                  v-for="time in timeOptions"
                  :key="time.value"
                  :label="time.label"
                  :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="参会人数" prop="participants">
          <el-input
              v-model="meetingForm.participants"
              type="number"
              placeholder="请输入参会人数"
          />
        </el-form-item>
        <el-form-item label="参会人员" prop="participants">
          <el-input
            v-model="meetingForm.participantList"
            type="textarea"
            :rows="3"
            placeholder="请输入参会人员,用逗号分隔"
          />
        </el-form-item>
        <el-form-item label="会议说明">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="4"
            placeholder="请输入会议说明"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="editDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitForm">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum,getDraftList,saveDraft,delDraft} from '@/api/collaborativeApproval/meeting.js'
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
// è‰ç¨¿åˆ—表数据
const draftList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  meetingDate: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const editDialogVisible = ref(false)
const roomList = ref([])
// å¯¹è¯æ¡†æ ‡é¢˜
const dialogTitle = ref('')
// å½“前查看的草稿
const currentDraft = ref(null)
// è¡¨å•引用
const meetingFormRef = ref(null)
// æ—¶é—´é€‰é¡¹ï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”,工作时间8:00-18:00)
const timeOptions = ref([])
// è¡¨å•数据
const meetingForm = reactive({
  id: '',
  meetingId: '',
  title: '',
  roomId: '',
  host: '',
  meetingDate: '',
  startTime: '',
  endTime: '',
  participants: 0,
  participantList: '',
  description: '',
  createTime: ''
})
// è¡¨å•校验规则
const rules = {
  title: [{ required: true, message: '请输入会议主题', trigger: 'blur' }],
  roomId: [{ required: true, message: '请选择会议室', trigger: 'change' }],
  host: [{ required: true, message: '请输入主持人', trigger: 'blur' }],
  meetingDate: [{ required: true, message: '请选择会议日期', trigger: 'change' }],
  startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
  endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
}
// åˆå§‹åŒ–时间选项(以半小时为间隔,工作时间8:00-18:00)
const initTimeOptions = () => {
  const options = []
  for (let hour = 8; hour <= 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªé€‰é¡¹ï¼šæ•´ç‚¹å’ŒåŠç‚¹
    options.push({
      value: `${hour.toString().padStart(2, '0')}:00`,
      label: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // 18:00之后没有半点选项
      options.push({
        value: `${hour.toString().padStart(2, '0')}:30`,
        label: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeOptions.value = options
}
// ç¦ç”¨æ—¥æœŸï¼ˆç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸï¼‰
const disabledDate = (time) => {
  // ç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸ
  return time.getTime() < Date.now() - 86400000
}
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getDraftList({...queryParams,...searchForm})
  queryParams.current = resp.data.current
  draftList.value = resp.data.records.map(it=>{
    it.room = roomList.value.find(room=>it.roomId===room.id).name ?? ""
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format("HH:mm")} ~ ${dayjs(it.endTime).format("HH:mm")}`
    return it
  })
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    createTime: []
  })
  handleSearch()
}
// æ·»åŠ æŒ‰é’®æ“ä½œ
const handleAdd = () => {
  dialogTitle.value = '新建草稿'
  resetForm()
  editDialogVisible.value = true
}
// æŸ¥çœ‹è‰ç¨¿è¯¦æƒ…
const viewDraft = (row) => {
  currentDraft.value = row
  detailDialogVisible.value = true
}
// ç¼–辑草稿
const editDraft = (row) => {
  dialogTitle.value = '编辑草稿'
  Object.assign(meetingForm, {
    id: row.id,
    meetingId: row.meetingId,
    title: row.title,
    room: row.room,
    roomId: row.id,
    host: row.host,
    meetingDate: row.meetingTime.split(' ')[0],
    startTime: row.meetingTime.split(' ')[1],
    endTime: row.meetingTime.split(' ')[3],
    participants: row.participants,
    participantList: row.participantList,
    description: row.description,
    createTime: row.createTime
  })
  editDialogVisible.value = true
}
// åˆ é™¤è‰ç¨¿
const deleteDraft = (row) => {
  ElMessageBox.confirm(
    `确认删除会议草稿 "${row.title}"?`,
    '删除草稿',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    delDraft(row.id).then(resp=>{
      ElMessage.success('草稿删除成功')
      getList()
    })
  }).catch(() => {})
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  Object.assign(meetingForm, {
    id: '',
    meetingId: '',
    title: '',
    room: '',
    host: '',
    meetingDate: '',
    startTime: '',
    endTime: '',
    participants: 0,
    participantList: '',
    description: '',
    createTime: ''
  })
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingFormRef.value.validate((valid) => {
    if (valid) {
      let formData = {...meetingForm}
      formData.startTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.startTime).format("YYYY-MM-DD HH:mm:ss")
      formData.endTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.endTime).format("YYYY-MM-DD HH:mm:ss")
      saveDraft(formData).then(()=>{
        ElMessage.success('保存成功')
        editDialogVisible.value = false
        getList()
      })
    }
  })
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  initTimeOptions()
  getList()
  getRoomEnum().then((res) => {
    roomList.value = res.data
  })
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.meeting-description {
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
  white-space: pre-wrap;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,418 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议审批</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable/>
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable/>
        </el-form-item>
        <el-form-item label="审批状态">
          <el-select style="width: 100px" v-model="searchForm.status" placeholder="请选择审批状态" clearable>
            <el-option label="待审批" value="0"/>
            <el-option label="已通过" value="1"/>
            <el-option label="未审批" value="2"/>
            <el-option label="已取消" value="3"/>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¡æ‰¹åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="approvalList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip/>
        <el-table-column prop="applicant" label="申请人" align="center" width="120"/>
        <el-table-column prop="host" label="主理人" align="center" width="120"/>
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150"/>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column prop="status" label="审批状态" align="center" width="120">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
                v-if="scope.row.status == '0'"
                type="primary"
                link
                @click="handleApproval(scope.row)"
            >
              å®¡æ‰¹
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
          v-show="total > 0"
          :total="total"
          v-model:page="queryParams.current"
          v-model:limit="queryParams.size"
          @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
         <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
              currentMeeting.title
            }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
              currentMeeting.applicant
            }}</el-descriptions-item>
          <el-descriptions-item label="主理人" label-class-name="nowrap-label">{{
              currentMeeting.host
            }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
              currentMeeting.location
            }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
              currentMeeting.participants.length
            }}人</el-descriptions-item>
          <el-descriptions-item label="审批状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
              currentMeeting.createTime
            }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
                                label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ä¼šè®®å®¡æ‰¹å¯¹è¯æ¡† -->
    <el-dialog
        title="会议审批"
        v-model="approvalDialogVisible"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主理人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
        <div v-show="false" class="approval-opinion mt-20">
          <h4>审批意见</h4>
          <el-input
              v-model="approvalOpinion"
              type="textarea"
              placeholder="请输入审批意见"
              :rows="4"
          />
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="approvalDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="danger" @click="submitApproval('2')">不通过</el-button>
          <el-button type="primary" @click="submitApproval('1')">通 è¿‡</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum, getExamineList,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// å®¡æ‰¹åˆ—表数据
const approvalList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  status: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const approvalDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// å®¡æ‰¹æ„è§
const approvalOpinion = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getExamineList({...searchForm, ...queryParams})
  approvalList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    status: ''
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// å¤„理审批
const handleApproval = (row) => {
  currentMeeting.value = row
  approvalOpinion.value = ''
  approvalDialogVisible.value = true
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…审批
    '1': 'success',  // å·²é€šè¿‡
    '2': 'warning',  // æœªé€šè¿‡
    '3': 'danger'   // å–消
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待审批',
    '1': '已通过',
    '2': '未通过',
    '3': '已取消'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// æäº¤å®¡æ‰¹
const submitApproval = (status) => {
  // if (status === 'approved' && !approvalOpinion.value.trim()) {
  //   ElMessage.warning('请填写审批意见')
  //   return
  // }
  ElMessageBox.confirm(
      `确认${status === '1' ? '通过' : '不通过'}该会议申请?`,
      '审批确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
  ).then(() => {
    saveMeetingApplication({
      id: currentMeeting.value.id,
      status: status
    }).then(resp=>{
      // æ›´æ–°ä¼šè®®çŠ¶æ€
      currentMeeting.value.status = status
      ElMessage.success('审批提交成功')
      approvalDialogVisible.value = false
      getList()
    })
  }).catch(() => {
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2]= await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.approval-opinion h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.nowrap-label {
  white-space: nowrap !important;
}
.description-content {
  white-space: pre-wrap;
  word-wrap: break-word;
  line-height: 1.6;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
  min-height: 60px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,371 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议室使用查询</h2>
    </div>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form :model="queryForm" label-width="80px" inline>
        <el-form-item label="查询日期">
          <el-date-picker
              v-model="queryForm.meetingDate"
              type="date"
              placeholder="请选择日期"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              :clearable="false"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¤ä½¿ç”¨æƒ…况 -->
    <el-card class="table-container" :loading="loading">
      <div class="time-table">
        <!-- è¡¨å¤´ -->
        <div class="table-header">
          <div class="header-cell room-header">会议室</div>
          <div
              v-for="timeSlot in timeSlots"
              :key="timeSlot.value"
              class="header-cell time-header"
          >
            {{ timeSlot.label }}
          </div>
        </div>
        <!-- è¡¨æ ¼å†…容 -->
        <div class="table-body">
          <div
              v-for="room in roomUsage"
              :key="room.id"
              class="table-row"
          >
            <div class="cell room-cell">{{ room.name }}</div>
            <div class="cells-container">
              <template v-for="(cell, index) in generateMeetingCells(room)" :key="index">
                <div
                    class="cell content-cell"
                    :class="[cell.type, `status-${cell.meeting?.status || '0'}`]"
                    :style="{ flex: cell.span-0.2 }"
                    @click="viewMeetingDetails(cell)"
                >
                  <div v-if="cell.type === 'meeting'" class="meeting-content">
                    <div class="meeting-title">{{ cell.meeting.title }}</div>
                    <div class="meeting-time">{{ cell.startTime }}-{{ cell.endTime }}</div>
                  </div>
                  <div v-else class="free-content">
                    ç©ºé—²
                  </div>
                </div>
              </template>
            </div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="会议室">{{ currentMeeting.room }}</el-descriptions-item>
          <el-descriptions-item label="会议时间">{{ currentMeeting.time }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants }}人</el-descriptions-item>
          <el-descriptions-item label="会议说明">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getMeetingUseList} from "@/api/collaborativeApproval/meeting.js"
import dayjs from "dayjs";
// æŸ¥è¯¢è¡¨å•
const queryForm = reactive({
  meetingDate: dayjs().format('YYYY-MM-DD')
})
let loading = ref(false)
// æ—¶é—´æ®µï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”)
const timeSlots = ref([])
// ä¼šè®®å®¤ä½¿ç”¨æƒ…况
const roomUsage = ref([])
// å½“前查看的会议
const currentMeeting = ref(null)
// æ˜¯å¦æ˜¾ç¤ºè¯¦æƒ…对话框
const detailDialogVisible = ref(false)
// åˆå§‹åŒ–时间槽(以半小时为间隔,从8:00到18:00)
const initTimeSlots = () => {
  const slots = []
  for (let hour = 8; hour < 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªæ—¶é—´æ®µï¼šæ•´ç‚¹å’ŒåŠç‚¹
    slots.push({
      label: `${hour.toString().padStart(2, '0')}:00`,
      value: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // åˆ°17:30为止
      slots.push({
        label: `${hour.toString().padStart(2, '0')}:30`,
        value: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeSlots.value = slots
}
// ç”Ÿæˆä¼šè®®å®¤çš„æ—¶é—´å•元格
const generateMeetingCells = (room) => {
  const cells = []
  const meetings = room.meetings || []
  const occupiedSlots = new Set()
  // å¤„理每个会议
  for (const meeting of meetings) {
    const startIdx = timeSlots.value.findIndex(slot => slot.value === meeting.startTime)
    let endIdx = timeSlots.value.findIndex(slot => slot.value === meeting.endTime)
    if (endIdx === -1) {
      endIdx = timeSlots.value.length
    }
    console.log('endIdx111', endIdx)
    if (startIdx !== -1 && endIdx !== -1) {
      // æ ‡è®°è¢«å ç”¨çš„æ—¶é—´æ®µ
      for (let i = startIdx; i < endIdx; i++) {
        occupiedSlots.add(timeSlots.value[i].value)
      }
      // åˆ›å»ºä¼šè®®å•元格
      cells.push({
        type: 'meeting',
        meeting: meeting,
        span: endIdx - startIdx,
        startTime: meeting.startTime,
        endTime: meeting.endTime
      })
    }
  }
  // å¤„理空闲时间段
  for (let i = 0; i < timeSlots.value.length; i++) {
    const slot = timeSlots.value[i]
    if (!occupiedSlots.has(slot.value)) {
      // æŸ¥æ‰¾è¿žç»­çš„空闲时间段
      let span = 1
      while (i + span < timeSlots.value.length &&
      !occupiedSlots.has(timeSlots.value[i + span].value)) {
        occupiedSlots.add(timeSlots.value[i + span].value)
        span++
      }
      cells.push({
        type: 'free',
        span: span,
        time: slot.value
      })
    }
  }
  // æŒ‰æ—¶é—´æŽ’序
  cells.sort((a, b) => {
    const timeA = a.startTime || a.time
    const timeB = b.startTime || b.time
    return timeSlots.value.findIndex(s => s.value === timeA) -
        timeSlots.value.findIndex(s => s.value === timeB)
  })
  console.log('cells', cells)
  return cells
}
// æŸ¥çœ‹ä¼šè®®è¯¦æƒ…
const viewMeetingDetails = (cell) => {
  if (cell && cell.type === 'meeting') {
    currentMeeting.value = cell.meeting
    detailDialogVisible.value = true
  } else {
    ElMessage.info('该时间段会议室空闲')
  }
}
// æŸ¥è¯¢æŒ‰é’®æ“ä½œ
const handleSearch = async () => {
  loading.value = true
  let resp = await getMeetingUseList({...queryForm})
  roomUsage.value = resp.data
  loading.value = false
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  queryForm.date = dayjs().format('YYYY-MM-DD')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  // åˆå§‹åŒ–æ—¶é—´æ§½
  initTimeSlots()
  // é»˜è®¤æŸ¥è¯¢ä»Šå¤©çš„æ•°æ®
  const today = new Date()
  queryForm.date = today.toISOString().split('T')[0]
  handleSearch()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.table-container {
  padding: 0;
}
.time-table {
  width: 100%;
  border-collapse: collapse;
}
.table-header {
  display: flex;
  border: 1px solid;
}
.table-row {
  display: flex;
  border: 1px solid #ebeef5;
  border-top: none;
}
.header-cell {
  padding: 12px 5px;
  text-align: center;
  font-weight: bold;
  border-right: 1px solid;
  min-height: 20px;
}
.room-header {
  width: 120px;
}
.time-header {
  flex: 1;
}
.cell {
  padding: 15px 5px;
  text-align: center;
  border-right: 1px solid;
  min-height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  word-break: break-word;
  line-height: 1.2;
}
.room-cell {
  width: 120px;
  font-weight: bold;
}
.cells-container {
  flex: 1;
  display: flex;
}
.content-cell {
  min-height: 60px;
  cursor: pointer;
  transition: all 0.3s;
}
.content-cell:hover {
  opacity: 0.8;
}
.free {
  color: #f56c6c;
}
.meeting {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.status-1 {
  background-color: #fef0f0;
  color: #d14646;
}
.status-0 {
  background-color: #c7ddc8;
  color: rgba(230, 162, 60, 0.29);
}
.meeting-content {
  width: 100%;
}
.meeting-title {
  font-weight: bold;
  margin-bottom: 5px;
}
.meeting-time {
  font-size: 12px;
}
.free-content {
  color: #909399;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,416 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议发布</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable/>
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable/>
        </el-form-item>
        <el-form-item label="发布状态">
          <el-select style="width: 100px" v-model="searchForm.status" placeholder="请选择发布状态" clearable>
            <el-option label="待发布" value="0"/>
            <el-option label="已发布" value="1"/>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å‘布列表 -->
    <el-card>
      <el-table v-loading="loading" :data="approvalList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip/>
        <el-table-column prop="applicant" label="申请人" align="center" width="120"/>
        <el-table-column prop="host" label="主理人" align="center" width="120"/>
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150"/>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column prop="status" label="发布状态" align="center" width="120">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
                v-if="scope.row.status == '0'"
                type="primary"
                link
                @click="handleApproval(scope.row)"
            >
              å‘布
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
          v-show="total > 0"
          :total="total"
          v-model:page="queryParams.current"
          v-model:limit="queryParams.size"
          @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
         <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
              currentMeeting.title
            }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
              currentMeeting.applicant
            }}</el-descriptions-item>
          <el-descriptions-item label="主理人" label-class-name="nowrap-label">{{
              currentMeeting.host
            }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
              currentMeeting.location
            }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
              currentMeeting.participants.length
            }}人</el-descriptions-item>
          <el-descriptions-item label="发布状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
              currentMeeting.createTime
            }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
                                label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ä¼šè®®å‘布对话框 -->
    <el-dialog
        title="会议发布"
        v-model="approvalDialogVisible"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主理人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
        <div class="approval-opinion mt-20">
          <h4>发布意见</h4>
          <el-input
              v-model="publishComment"
              type="textarea"
              placeholder="请输入发布意见"
              :rows="4"
          />
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="approvalDialogVisible = false">取 æ¶ˆ</el-button>
<!--          <el-button type="danger" @click="submitApproval('2')">不通过</el-button>-->
          <el-button type="primary" @click="submitApproval('1')">发 å¸ƒ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum, getMeetingPublish,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// å‘布列表数据
const approvalList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  status: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const approvalDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// å‘布意见
const publishComment = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingPublish({...searchForm, ...queryParams})
  approvalList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.status = it.publishStatus
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    status: ''
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// å¤„理发布
const handleApproval = (row) => {
  currentMeeting.value = row
  publishComment.value = ''
  approvalDialogVisible.value = true
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…发布
    '1': 'success',  // å·²é€šè¿‡
    '2': 'danger',  // æœªé€šè¿‡
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待发布',
    '1': '已发布',
    '2': '已取消',
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// æäº¤å‘布
const submitApproval = (status) => {
  // if (status === 'approved' && !publishComment.value.trim()) {
  //   ElMessage.warning('请填写发布意见')
  //   return
  // }
  ElMessageBox.confirm(
      `确认${status === '1' ? '发布' : '取消'}该会议?`,
      '发布确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
  ).then(() => {
    saveMeetingApplication({
      id: currentMeeting.value.id,
      publishStatus: status,
      publishComment: publishComment.value
    }).then(resp=>{
      // æ›´æ–°ä¼šè®®çŠ¶æ€
      currentMeeting.value.status = status
      ElMessage.success('发布提交成功')
      approvalDialogVisible.value = false
      getList()
    })
  }).catch(() => {
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2]= await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.approval-opinion h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.nowrap-label {
  white-space: nowrap !important;
}
.description-content {
  white-space: pre-wrap;
  word-wrap: break-word;
  line-height: 1.6;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
  min-height: 60px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议室设置</h2>
      <el-button type="primary" @click="handleAdd">
        <el-icon><Plus /></el-icon>
        æ–°å¢žä¼šè®®å®¤
      </el-button>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" label-width="100px" inline>
        <el-form-item label="会议室名称">
          <el-input v-model="searchForm.name" placeholder="请输入会议室名称" clearable />
        </el-form-item>
        <el-form-item label="位置">
          <el-input v-model="searchForm.location" placeholder="请输入位置" clearable />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¤åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="meetingRoomList" border>
        <el-table-column prop="name" label="会议室名称" align="center" />
        <el-table-column prop="location" label="位置" align="center" />
        <el-table-column prop="capacity" label="容纳人数" align="center" />
        <el-table-column prop="equipment" label="设备配置" align="center">
          <template #default="scope">
            <el-tag v-for="item in scope.row.equipment" :key="item" style="margin-right: 5px; margin-bottom: 5px;">
              {{ item }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" align="center" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
              {{ scope.row.status === 1 ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200">
          <template #default="scope">
            <el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
            <el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- æ·»åŠ /编辑对话框 -->
    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" @close="cancel">
      <el-form ref="meetingRoomFormRef" :model="meetingRoomForm" :rules="rules" label-width="100px">
        <el-form-item label="会议室名称" prop="name">
          <el-input v-model="meetingRoomForm.name" placeholder="请输入会议室名称" />
        </el-form-item>
        <el-form-item label="位置" prop="location">
          <el-input v-model="meetingRoomForm.location" placeholder="请输入会议室位置" />
        </el-form-item>
        <el-form-item label="容纳人数" prop="capacity">
          <el-input-number v-model="meetingRoomForm.capacity" :min="1" placeholder="请输入容纳人数" />
        </el-form-item>
        <el-form-item label="设备配置" prop="equipment">
          <el-select v-model="meetingRoomForm.equipment" multiple placeholder="请选择设备配置" style="width: 100%">
            <el-option
              v-for="item in equipmentOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="meetingRoomForm.status">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="meetingRoomForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'
import {getMeetingRoomList,saveRoom,delRoom} from '@/api/collaborativeApproval/meeting.js'
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
// ä¼šè®®å®¤åˆ—表数据
const meetingRoomList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  name: '',
  location: ''
})
// å¯¹è¯æ¡†æ ‡é¢˜
const dialogTitle = ref('')
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const dialogVisible = ref(false)
// è®¾å¤‡é…ç½®é€‰é¡¹
const equipmentOptions = ref([
  { value: '投影仪', label: '投影仪' },
  { value: '电视', label: '电视' },
  { value: '音响', label: '音响' },
  { value: '电话', label: '电话' },
  { value: '视频会议系统', label: '视频会议系统' },
  { value: '白板', label: '白板' },
  { value: '写字板', label: '写字板' },
  { value: '无线网络', label: '无线网络' }
])
// è¡¨å•数据
const meetingRoomForm = reactive({
  id: undefined,
  name: '',
  location: '',
  capacity: 10,
  equipment: [],
  status: 1,
  remark: ''
})
// è¡¨å•校验规则
const rules = {
  name: [{ required: true, message: '会议室名称不能为空', trigger: 'blur' }],
  location: [{ required: true, message: '位置不能为空', trigger: 'blur' }],
  capacity: [{ required: true, message: '容纳人数不能为空', trigger: 'blur' }]
}
// è¡¨å•引用
const meetingRoomFormRef = ref(null)
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingRoomList({...searchForm,...queryParams})
  meetingRoomList.value = resp.data.records.map(it=>{
    it.equipment = it.equipment.split(',')
    return it;
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.current = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    name: '',
    location: ''
  })
  handleSearch()
}
// æ·»åŠ æŒ‰é’®æ“ä½œ
const handleAdd = () => {
  dialogTitle.value = '添加会议室'
  dialogVisible.value = true
}
// ä¿®æ”¹æŒ‰é’®æ“ä½œ
const handleEdit = (row) => {
  dialogTitle.value = '修改会议室'
  Object.assign(meetingRoomForm, row)
  dialogVisible.value = true
}
// åˆ é™¤æŒ‰é’®æ“ä½œ
const handleDelete = (row) => {
  ElMessageBox.confirm(
    `是否确认删除会议室 "${row.name}"?`,
    '警告',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    // æ¨¡æ‹Ÿåˆ é™¤æ“ä½œ
    delRoom(row.id).then(resp=>{
      ElMessage.success('删除成功')
      getList()
    })
  }).catch(() => {})
}
// å–消按钮
const cancel = () => {
  dialogVisible.value = false
  reset()
}
// è¡¨å•重置
const reset = () => {
  Object.assign(meetingRoomForm, {
    id: undefined,
    name: '',
    location: '',
    capacity: 10,
    equipment: [],
    status: 1,
    remark: ''
  })
  meetingRoomFormRef.value?.resetFields()
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingRoomFormRef.value?.validate((valid) => {
    if (valid) {
      // æ¨¡æ‹Ÿæäº¤æ“ä½œ
      let formData = {...  meetingRoomForm}
      formData.equipment = formData.equipment.join(',')
      saveRoom(formData).then(resp=>{
        ElMessage.success('保存成功')
        dialogVisible.value = false
        getList()
      })
    }
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/notificationManagement/summary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议纪要</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable />
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="meetingList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip />
        <el-table-column prop="applicant" label="申请人" align="center" width="120" />
        <el-table-column prop="host" label="主持人" align="center" width="120" />
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150" />
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
              type="primary"
              link
              @click="addMinutes(scope.row)"
            >
              æ·»åŠ çºªè¦
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
      title="会议详情"
      v-model="detailDialogVisible"
      width="800px"
    >
      <div v-if="currentMeeting">
        <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
            currentMeeting.title
          }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
            currentMeeting.applicant
          }}</el-descriptions-item>
          <el-descriptions-item label="主持人" label-class-name="nowrap-label">{{
            currentMeeting.host
          }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
            currentMeeting.location
          }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
            currentMeeting.participants.length
          }}人</el-descriptions-item>
          <el-descriptions-item label="审批状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
            currentMeeting.createTime
          }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
            label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
              v-for="participant in currentMeeting.participants"
              :key="participant.id"
              style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ·»åŠ ä¼šè®®çºªè¦å¯¹è¯æ¡† -->
    <el-dialog
      title="添加会议纪要"
      v-model="minutesDialogVisible"
      width="80%"
      @close="handleCloseMinutesDialog"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>会议纪要内容</h4>
          <div class="editor-container">
            <Editor
              v-model="minutesContent"
              :min-height="400"
            />
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="minutesDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitMinutes">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import Editor from '@/components/Editor/index.vue'
import { getRoomEnum, getMeetingPublish ,getMeetingMinutesByMeetingId,saveMeetingMinutes} from '@/api/collaborativeApproval/meeting.js'
import { getStaffOnJob } from "@/api/personnelManagement/onboarding.js"
import dayjs from "dayjs"
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// ä¼šè®®åˆ—表数据
const meetingList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  // status: '1' // é»˜è®¤åªæ˜¾ç¤ºå·²é€šè¿‡å®¡æ‰¹çš„会议
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const minutesDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// ä¼šè®®çºªè¦å†…容
const minutesContent = ref('')
const minutesContentId = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingPublish({ ...searchForm, ...queryParams })
  meetingList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.current = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    // status: '1'
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// æ·»åŠ ä¼šè®®çºªè¦
const addMinutes = async (row) => {
  let resp = await getMeetingMinutesByMeetingId(row.id)
  currentMeeting.value = row
  if (resp.data){
    minutesContent.value = resp.data.content
    minutesContentId.value = resp.data.id
  }else {
    minutesContent.value = `<h2>${row.title}会议纪要</h2>
<p><strong>会议时间:</strong>${row.meetingTime}</p>
<p><strong>会议地点:</strong>${row.location}</p>
<p><strong>主持人:</strong>${row.host}</p>
<p><strong>参会人员:</strong></p>
<ol>
  ${row.participants.map(p => `<li>${p.name}</li>`).join('')}
</ol>
<p><strong>会议内容:</strong></p>
<ol>
  <li>议题一:
    <ul>
      <li>讨论内容:</li>
      <li>决议事项:</li>
    </ul>
  </li>
  <li>议题二:
    <ul>
      <li>讨论内容:</li>
      <li>决议事项:</li>
    </ul>
  </li>
</ol>
<p><strong>备注:</strong></p>`
  }
  minutesDialogVisible.value = true
}
// æäº¤ä¼šè®®çºªè¦
const submitMinutes = () => {
  if (!minutesContent.value) {
    ElMessage.warning('请输入会议纪要内容')
    return
  }
  saveMeetingMinutes({
    id: minutesContentId.value,
    content: minutesContent.value,
    meetingId: currentMeeting.value.id,
    title: currentMeeting.value.title
  }).then(resp=>{
    console.log('会议纪要内容:', minutesContent.value)
    ElMessage.success('会议纪要保存成功')
    minutesDialogVisible.value = false
  })
}
// å…³é—­ä¼šè®®çºªè¦å¯¹è¯æ¡†
const handleCloseMinutesDialog = () => {
  minutesContent.value = ''
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…审批
    '1': 'success',  // å·²é€šè¿‡
    '2': 'warning',  // æœªé€šè¿‡
    '3': 'danger'   // å–消
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待审批',
    '1': '已通过',
    '2': '未通过',
    '3': '已取消'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2] = await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.nowrap-label {
  white-space: nowrap !important;
}
.editor-container {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}
</style>