gaoluyang
10 天以前 1c6c29ef2305f8365854a8a5ee03396a3ac69932
通知公告、会议看板、预警联动机制功能添加
已修改1个文件
已添加5个文件
1869 ■■■■■ 文件已修改
src/api/collaborativeApproval/noticeManagement.js 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/noticeManagement/index.vue 705 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/warningSystem/index.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/fakePage/index.vue 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/noticeManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
import request from '@/utils/request'
// æŸ¥è¯¢å…¬å‘Šåˆ—表
export function listNotice(query) {
  return request({
    url: '/collaborativeApproval/notice/list',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢å…¬å‘Šè¯¦ç»†
export function getNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'get'
  })
}
// æ–°å¢žå…¬å‘Š
export function addNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹å…¬å‘Š
export function updateNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'put',
    data: data
  })
}
// åˆ é™¤å…¬å‘Š
export function delNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'delete'
  })
}
// æ‰¹é‡åˆ é™¤å…¬å‘Š
export function delNoticeBatch(noticeIds) {
  return request({
    url: '/collaborativeApproval/notice/batch',
    method: 'delete',
    data: noticeIds
  })
}
// å‘布公告
export function publishNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/publish/' + noticeId,
    method: 'put'
  })
}
// ä¸‹çº¿å…¬å‘Š
export function offlineNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/offline/' + noticeId,
    method: 'put'
  })
}
src/router/index.js
@@ -71,33 +71,6 @@
      }
    ]
  },
  // {
  //   path: '/equipment',
  //   component: Layout,
  //   redirect: '/equipment/iot-monitor',
  //   children: [
  //     {
  //       path: 'iot-monitor',
  //       component: () => import('@/views/equipmentManagement/iotMonitor/index.vue'),
  //       name: 'IoTMonitor',
  //       meta: { title: 'IoT监控', icon: 'monitor', noCache: true }
  //     }
  //   ]
  // },
  // {
  //   path: '/main/MobileChat',
  //   component: Layout,
  //   redirect: '',
  //   hidden: true,
  //   children: [
  //     {
  //       path: '',
  //       component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
  //       name: 'MobileChat',
  //       meta: { title: 'AI对话', icon: 'dashboard', affix: true}
  //     }
  //   ]
  // },
  {
    path: '/user',
    component: Layout,
@@ -131,6 +104,21 @@
    ]
  },
  {
    path: '/main/MobileChat',
    component: Layout,
    redirect: '',
    hidden: true,
    permissions: ['MobileChat:edit'],
    children: [
      {
        path: '',
        component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
        name: 'MobileChat',
        meta: { title: 'AI对话', activeMenu: '/chatHome/chatHomeIndex'}
      }
    ]
  },
  {
    path: '/system/role-auth',
    component: Layout,
    hidden: true,
src/views/collaborativeApproval/meetingBoard/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议看板</h2>
<!--      <el-button type="primary" @click="createMeeting">创建会议</el-button>-->
    </div>
    <!-- ä¼šè®®ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.total }}</div>
          <div class="stat-label">总会议数</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.ongoing }}</div>
          <div class="stat-label">进行中</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.completed }}</div>
          <div class="stat-label">已完成</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.upcoming }}</div>
          <div class="stat-label">即将开始</div>
        </div>
      </el-card>
    </div>
    <!-- ä¼šè®®åˆ—表 -->
    <div class="meeting-list">
      <el-card v-for="meeting in meetings" :key="meeting.id" class="meeting-card">
        <div class="meeting-header">
          <div class="meeting-title">
            <h3>{{ meeting.title }}</h3>
            <el-tag :type="getStatusType(meeting.status)" size="small">
              {{ getStatusText(meeting.status) }}
            </el-tag>
          </div>
          <div class="meeting-time">
            <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>
            <span>{{ meeting.location }}</span>
          </div>
          <div class="info-item">
            <el-icon><User /></el-icon>
            <span>主持人: {{ meeting.host }}</span>
          </div>
          <div class="info-item">
            <el-icon><UserFilled /></el-icon>
            <span>参会人数: {{ meeting.participants.length }}人</span>
          </div>
        </div>
        <div class="meeting-agenda">
          <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>
          </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>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
// ç»Ÿè®¡æ•°æ®
const stats = reactive({
  total: 12,
  ongoing: 3,
  completed: 7,
  upcoming: 2
})
// ä¼šè®®æ•°æ®
const meetings = ref([
  {
    id: 1,
    title: '产品开发周会',
    status: 'ongoing',
    startTime: '2024-01-15 09:00:00',
    endTime: '2024-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: '2024-01-15 14:00:00',
    endTime: '2024-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: '2024-01-14 16:00:00',
    endTime: '2024-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' }
    ]
  }
])
// å¯¹è¯æ¡†ç›¸å…³
const dialogVisible = ref(false)
const meetingForm = reactive({
  title: '',
  timeRange: [],
  location: '',
  host: '',
  description: ''
})
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    'ongoing': 'success',
    'upcoming': 'warning',
    'completed': 'info'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    'ongoing': '进行中',
    'upcoming': '即将开始',
    'completed': '已完成'
  }
  return statusMap[status] || '未知'
}
// èŽ·å–è®®ç¨‹çŠ¶æ€ç±»åž‹
const getAgendaStatusType = (status) => {
  const statusMap = {
    'completed': 'success',
    'active': 'warning',
    'pending': 'info'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–è®®ç¨‹çŠ¶æ€æ–‡æœ¬
const getAgendaStatusText = (status) => {
  const statusMap = {
    'completed': '已完成',
    'active': '进行中',
    'pending': '待开始'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (timeStr) => {
  const date = new Date(timeStr)
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// åˆ›å»ºä¼šè®®
const createMeeting = () => {
  dialogVisible.value = true
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm, {
    title: '',
    timeRange: [],
    location: '',
    host: '',
    description: ''
  })
}
// æäº¤ä¼šè®®
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>
<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;
}
.stats-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}
.stat-card {
  text-align: center;
}
.stat-content {
  padding: 10px;
}
.stat-number {
  font-size: 32px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}
.stat-label {
  font-size: 14px;
  color: #606266;
}
.meeting-list {
  display: grid;
  gap: 20px;
}
.meeting-card {
  border-radius: 8px;
}
.meeting-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 15px;
}
.meeting-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
.meeting-title h3 {
  margin: 0;
  color: #303133;
}
.meeting-time {
  display: flex;
  align-items: center;
  gap: 5px;
  color: #606266;
  font-size: 14px;
}
.meeting-info {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.info-item {
  display: flex;
  align-items: center;
  gap: 5px;
  color: #606266;
  font-size: 14px;
}
.meeting-agenda {
  margin-bottom: 20px;
}
.meeting-agenda h4 {
  margin: 0 0 15px 0;
  color: #303133;
  font-size: 16px;
}
.agenda-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.agenda-item {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 10px;
  border-radius: 6px;
  background-color: #f5f7fa;
}
.agenda-item.active {
  background-color: #fdf6ec;
  border-left: 3px solid #e6a23c;
}
.agenda-item.completed {
  background-color: #f0f9ff;
  border-left: 3px solid #409eff;
}
.agenda-time {
  font-weight: bold;
  color: #606266;
  min-width: 80px;
}
.agenda-content {
  flex: 1;
  color: #303133;
}
.meeting-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
@media (max-width: 768px) {
  .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>
src/views/collaborativeApproval/noticeManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,705 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <div class="search_form">
      <div>
        <span class="search_title">公告标题:</span>
        <el-input
          v-model="searchForm.noticeTitle"
          style="width: 240px"
          placeholder="请输入公告标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">公告类型:</span>
        <el-select v-model="searchForm.noticeType" clearable @change="handleQuery" style="width: 240px">
          <el-option label="放假通知" value="1" />
          <el-option label="设备维修通知" value="2" />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
          <el-option label="草稿" value="0" />
          <el-option label="已发布" value="1" />
          <el-option label="已下线" value="2" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
        <el-button @click="resetQuery" style="margin-left: 10px">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增公告</el-button>
        <el-button type="danger" plain @click="handleDelete" :disabled="!selectedIds.length">删除</el-button>
      </div>
    </div>
    <!-- é€šçŸ¥å…¬å‘Šæ¿ -->
    <div class="notice-board">
      <!-- æ”¾å‡é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="holidayNotices.length > 0">
        <div class="section-header">
          <h3>📅 æ”¾å‡é€šçŸ¥</h3>
          <span class="section-count">{{ holidayNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in holidayNotices"
            :key="notice.id"
            class="notice-card holiday-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="holiday-icon"><Calendar /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            </div>
          </div>
        </div>
      </div>
      <!-- è®¾å¤‡ç»´ä¿®é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="maintenanceNotices.length > 0">
        <div class="section-header">
          <h3>🔧 è®¾å¤‡ç»´ä¿®é€šçŸ¥</h3>
          <span class="section-count">{{ maintenanceNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in maintenanceNotices"
            :key="notice.id"
            class="notice-card maintenance-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="maintenance-icon"><Tools /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            </div>
          </div>
        </div>
      </div>
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-if="filteredNotices.length === 0">
        <el-empty description="暂无通知公告" />
      </div>
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="800px"
      append-to-body
      @close="resetForm"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="公告标题" prop="noticeTitle">
              <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公告类型" prop="noticeType">
              <el-select v-model="form.noticeType" placeholder="请选择公告类型" style="width: 100%">
                <el-option label="放假通知" value="1" />
                <el-option label="设备维修通知" value="2" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="状态">
              <el-radio-group v-model="form.status">
                <el-radio value="0">草稿</el-radio>
                <el-radio value="1">已发布</el-radio>
                <el-radio value="2">已下线</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="优先级">
              <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
                <el-option label="普通" value="1" />
                <el-option label="重要" value="2" />
                <el-option label="紧急" value="3" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="公告内容" prop="noticeContent">
              <el-input
                v-model="form.noticeContent"
                type="textarea"
                :rows="6"
                placeholder="请输入公告内容"
                maxlength="500"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="3"
                placeholder="请输入备注信息"
                maxlength="200"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search, Calendar, Tools, InfoFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
// å“åº”式数据
const data = reactive({
  searchForm: {
    noticeTitle: "",
    noticeType: "",
    status: "",
  },
  form: {
    id: undefined,
    noticeTitle: "",
    noticeType: "",
    noticeContent: "",
    status: "0",
    priority: "1",
    remark: "",
    createBy: "",
    createTime: "",
  },
  rules: {
    noticeTitle: [
      { required: true, message: "公告标题不能为空", trigger: "blur" }
    ],
    noticeType: [
      { required: true, message: "请选择公告类型", trigger: "change" }
    ],
    noticeContent: [
      { required: true, message: "公告内容不能为空", trigger: "blur" }
    ]
  }
});
const { searchForm, form, rules } = toRefs(data);
// é¡µé¢çŠ¶æ€
const dialogVisible = ref(false);
const dialogTitle = ref("");
const selectedIds = ref([]);
const formRef = ref();
// æ¨¡æ‹Ÿæ•°æ® - æ ¹æ®æ³•定节假日设计
const mockData = [
  {
    id: 1,
    noticeTitle: "2024年春节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年春节放假安排如下:2月10日(初一)至2月17日(初八)放假调休,共8天。2月4日(星期日)、2月18日(星期日)上班。请各部门提前做好工作安排。",
    remark: "放假期间请保持手机畅通,如有紧急事务及时联系",
    createBy: "人事部",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: 2,
    noticeTitle: "2024年清明节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年清明节放假安排如下:4月4日(星期四)至4月6日(星期六)放假调休,共3天。4月7日(星期日)上班。",
    remark: "请各部门做好值班安排,确保节日期间各项工作正常运转",
    createBy: "行政部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 3,
    noticeTitle: "2024年劳动节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年劳动节放假安排如下:5月1日(星期三)至5月5日(星期日)放假调休,共5天。4月28日(星期日)、5月11日(星期六)上班。",
    remark: "放假前请关闭电源,锁好门窗,注意安全",
    createBy: "行政部",
    createTime: "2024-01-13 09:15:00"
  },
  {
    id: 4,
    noticeTitle: "2024年端午节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年端午节放假安排如下:6月8日(星期六)至6月10日(星期一)放假调休,共3天。6月11日(星期二)上班。",
    remark: "祝大家端午节快乐,阖家幸福!",
    createBy: "行政部",
    createTime: "2024-01-12 16:30:00"
  },
  {
    id: 5,
    noticeTitle: "2024年中秋节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年中秋节放假安排如下:9月15日(星期日)至9月17日(星期二)放假调休,共3天。9月14日(星期六)上班。",
    remark: "中秋佳节,祝大家团圆美满,幸福安康!",
    createBy: "行政部",
    createTime: "2024-01-11 11:20:00"
  },
  {
    id: 6,
    noticeTitle: "2024年国庆节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年国庆节放假安排如下:10月1日(星期二)至10月7日(星期一)放假调休,共7天。9月29日(星期日)、10月12日(星期六)上班。",
    remark: "国庆期间请各部门做好值班安排,确保安全稳定",
    createBy: "行政部",
    createTime: "2024-01-10 15:45:00"
  },
  {
    id: 7,
    noticeTitle: "A车间生产线年度检修通知",
    noticeType: "2",
    priority: "2",
    status: "1",
    noticeContent: "A车间生产线将于2024å¹´1月20日(周六)进行年度检修维护,预计停工8小时。检修内容包括:设备清洁、润滑保养、安全装置检查等。请生产部门提前调整生产计划。",
    remark: "维修期间请相关人员配合,确保检修工作安全顺利进行",
    createBy: "设备部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 8,
    noticeTitle: "B车间设备预防性维护通知",
    noticeType: "2",
    priority: "1",
    status: "1",
    noticeContent: "B车间关键设备将于2024å¹´1月25日进行预防性维护,预计停工4小时。维护内容包括:设备检查、零件更换、性能测试等。请相关部门配合。",
    remark: "维护完成后将进行试运行,确保设备正常运行",
    createBy: "设备部",
    createTime: "2024-01-13 09:15:00"
  }
];
// è®¡ç®—属性
const filteredNotices = computed(() => {
  let filtered = [...mockData];
  if (searchForm.value.noticeTitle) {
    filtered = filtered.filter(item =>
      item.noticeTitle.includes(searchForm.value.noticeTitle)
    );
  }
  if (searchForm.value.noticeType) {
    filtered = filtered.filter(item =>
      item.noticeType === searchForm.value.noticeType
    );
  }
  if (searchForm.value.status !== "") {
    filtered = filtered.filter(item =>
      item.status === searchForm.value.status
    );
  }
  return filtered;
});
const holidayNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "1");
});
const maintenanceNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "2");
});
// æ–¹æ³•定义
const handleQuery = () => {
  // æœç´¢åŠŸèƒ½ä¿æŒä¸å˜ï¼Œä½†æ•°æ®é€šè¿‡è®¡ç®—å±žæ€§è‡ªåŠ¨è¿‡æ»¤
};
const resetQuery = () => {
  searchForm.value = {
    noticeTitle: "",
    noticeType: "",
    status: ""
  };
};
const getPriorityText = (priority) => {
  const priorityMap = { "1": "普通", "2": "重要", "3": "紧急" };
  return priorityMap[priority] || "普通";
};
const getStatusText = (status) => {
  const statusMap = { "0": "草稿", "1": "已发布", "2": "已下线" };
  return statusMap[status] || "未知";
};
const openForm = (type) => {
  if (type === 'add') {
    dialogTitle.value = "新增公告";
    form.value = {
      id: undefined,
      noticeTitle: "",
      noticeType: "",
      noticeContent: "",
      status: "0",
      priority: "1",
      remark: "",
      createBy: userStore.name || "当前用户",
      createTime: new Date().toLocaleString()
    };
  }
  dialogVisible.value = true;
};
const handleEdit = (row) => {
  dialogTitle.value = "编辑公告";
  form.value = { ...row };
  dialogVisible.value = true;
};
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
const handleDelete = (id) => {
  ElMessageBox.confirm(
    "确认删除这条公告吗?",
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    const index = mockData.findIndex(item => item.id === id);
    if (index > -1) {
      mockData.splice(index, 1);
      ElMessage.success("删除成功");
    }
  });
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (form.value.id) {
        // ç¼–辑模式
        const index = mockData.findIndex(item => item.id === form.value.id);
        if (index > -1) {
          mockData[index] = { ...form.value };
        }
        ElMessage.success("修改成功");
      } else {
        // æ–°å¢žæ¨¡å¼
        const newId = Math.max(...mockData.map(item => item.id)) + 1;
        const newNotice = {
          ...form.value,
          id: newId,
          createTime: new Date().toLocaleString()
        };
        mockData.unshift(newNotice);
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
    }
  });
};
const resetForm = () => {
  formRef.value?.resetFields();
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // é¡µé¢åŠ è½½å®Œæˆ
});
</script>
<style scoped>
.search_form {
  background: #fff;
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.search_title {
  font-weight: 500;
  color: #333;
  margin-right: 8px;
}
.ml10 {
  margin-left: 10px;
}
.notice-board {
  background: #f5f7fa;
  padding: 20px;
  border-radius: 8px;
}
.notice-section {
  margin-bottom: 30px;
}
.section-header {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
  padding: 0 10px;
}
.section-header h3 {
  margin: 0;
  color: #303133;
  font-size: 18px;
  font-weight: 600;
}
.section-count {
  margin-left: 10px;
  background: #409eff;
  color: white;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
}
.notice-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  gap: 20px;
}
.notice-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
  border-left: 4px solid transparent;
}
.notice-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.holiday-card {
  border-left-color: #67c23a;
}
.maintenance-card {
  border-left-color: #e6a23c;
}
.urgent {
  border-left-color: #f56c6c;
  background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 15px;
}
.card-title {
  display: flex;
  align-items: center;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  flex: 1;
}
.holiday-icon {
  color: #67c23a;
  margin-right: 8px;
  font-size: 18px;
}
.maintenance-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.card-actions {
  display: flex;
  gap: 8px;
}
.card-content {
  margin-bottom: 15px;
}
.card-content p {
  margin: 0;
  color: #606266;
  line-height: 1.6;
  font-size: 14px;
}
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.card-meta {
  display: flex;
  gap: 8px;
}
.priority, .status {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}
.priority-1 { background: #f0f9ff; color: #0369a1; }
.priority-2 { background: #fef3c7; color: #d97706; }
.priority-3 { background: #fef2f2; color: #dc2626; }
.status-0 { background: #f3f4f6; color: #6b7280; }
.status-1 { background: #d1fae5; color: #059669; }
.status-2 { background: #fef3c7; color: #d97706; }
.card-info {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  font-size: 12px;
  color: #909399;
}
.creator {
  font-weight: 500;
  margin-bottom: 2px;
}
.card-remark {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  background: #f8f9fa;
  border-radius: 6px;
  font-size: 12px;
  color: #606266;
  border-left: 3px solid #409eff;
}
.empty-state {
  text-align: center;
  padding: 60px 20px;
}
.dialog-footer {
  text-align: right;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .notice-cards {
    grid-template-columns: 1fr;
  }
  .search_form {
    flex-direction: column;
    gap: 15px;
  }
  .search_form > div {
    width: 100%;
  }
}
</style>
src/views/collaborativeApproval/warningSystem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
<template>
  <div class="warning-system">
    <h2>预警联动机制</h2>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats">
      <div class="stat-card red">
        <span class="number">2</span>
        <span class="label">红色预警</span>
      </div>
      <div class="stat-card orange">
        <span class="number">1</span>
        <span class="label">橙色预警</span>
      </div>
      <div class="stat-card yellow">
        <span class="number">1</span>
        <span class="label">黄色预警</span>
      </div>
      <div class="stat-card green">
        <span class="number">1</span>
        <span class="label">绿色预警</span>
      </div>
    </div>
    <!-- é¢„警列表 -->
    <div class="warning-list">
      <h3>预警列表</h3>
      <table>
        <thead>
          <tr>
            <th>编号</th>
            <th>标题</th>
            <th>类型</th>
            <th>等级</th>
            <th>状态</th>
            <th>责任人</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="warning in warnings" :key="warning.id">
            <td>{{ warning.id }}</td>
            <td>{{ warning.title }}</td>
            <td>{{ warning.type }}</td>
            <td>
              <span :class="['level-tag', warning.level]">
                {{ warning.levelText }}
              </span>
            </td>
            <td>
              <span :class="['status-tag', warning.status]">
                {{ warning.statusText }}
              </span>
            </td>
            <td>{{ warning.responsible }}</td>
            <td>
              <button @click="viewDetail(warning)">查看详情</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <!-- è¯¦æƒ…对话框 -->
    <div v-if="showDetail" class="modal">
      <div class="modal-content">
        <h3>预警详情</h3>
        <div v-if="currentWarning">
          <p><strong>编号:</strong>{{ currentWarning.id }}</p>
          <p><strong>标题:</strong>{{ currentWarning.title }}</p>
          <p><strong>类型:</strong>{{ currentWarning.type }}</p>
          <p><strong>等级:</strong>{{ currentWarning.levelText }}</p>
          <p><strong>描述:</strong>{{ currentWarning.description }}</p>
          <p><strong>影响:</strong>{{ currentWarning.impact }}</p>
          <p><strong>建议:</strong>{{ currentWarning.suggestions }}</p>
        </div>
        <button @click="showDetail = false">关闭</button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'WarningSystem',
  data() {
    return {
      showDetail: false,
      currentWarning: null,
      warnings: [
        {
          id: 'W001',
          title: '项目预算超支预警',
          type: '财务预警',
          level: 'red',
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '张经理',
          description: 'A项目预算执行率已达95%,预计将超出预算范围。',
          impact: '影响项目整体财务指标,可能导致项目亏损',
          suggestions: '暂停非必要支出,优化资源配置,申请预算调整'
        },
        {
          id: 'W002',
          title: '合同到期预警',
          type: '合规预警',
          level: 'orange',
          levelText: '橙色预警',
          status: 'processing',
          statusText: '处理中',
          responsible: '李主管',
          description: '与供应商B的合同将于2024å¹´1月25日到期。',
          impact: '影响供应链稳定性,可能导致服务中断',
          suggestions: '评估供应商表现,准备续签材料,制定备选方案'
        },
        {
          id: 'W003',
          title: '设备维护预警',
          type: '运营预警',
          level: 'yellow',
          levelText: '黄色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '王工程师',
          description: '生产线设备C已运行8000小时,接近维护周期。',
          impact: '可能影响生产效率和产品质量',
          suggestions: '安排维护时间,准备备件,制定维护计划'
        },
        {
          id: 'W004',
          title: '人员配置预警',
          type: '运营预警',
          level: 'green',
          levelText: '绿色预警',
          status: 'resolved',
          statusText: '已解决',
          responsible: 'èµµHR',
          description: '技术部门人员配置充足,项目进度正常。',
          impact: '无负面影响',
          suggestions: '继续监控人员配置情况'
        },
        {
          id: 'W005',
          title: '质量事故预警',
          type: '运营预警',
          level: 'red',
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '陈总监',
          description: '产品D在客户现场出现质量问题。',
          impact: '影响客户满意度,可能造成经济损失',
          suggestions: '立即召回问题产品,分析原因,制定改进措施'
        }
      ]
    }
  },
  methods: {
    viewDetail(warning) {
      this.currentWarning = warning
      this.showDetail = true
    }
  }
}
</script>
<style scoped>
.warning-system {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
h2 {
  color: #333;
  margin-bottom: 30px;
}
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}
.stat-card {
  padding: 20px;
  border-radius: 8px;
  color: white;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card.red { background: linear-gradient(135deg, #ff6b6b, #ee5a52); }
.stat-card.orange { background: linear-gradient(135deg, #ffa726, #ff9800); }
.stat-card.yellow { background: linear-gradient(135deg, #ffd54f, #ffc107); }
.stat-card.green { background: linear-gradient(135deg, #66bb6a, #4caf50); }
.stat-card .number {
  display: block;
  font-size: 32px;
  font-weight: bold;
  margin-bottom: 8px;
}
.stat-card .label {
  font-size: 14px;
  opacity: 0.9;
}
.warning-list h3 {
  margin-bottom: 20px;
  color: #333;
}
table {
  width: 100%;
  border-collapse: collapse;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
th, td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}
th {
  background: #f8f9fa;
  font-weight: 600;
  color: #333;
}
.level-tag, .status-tag {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  color: white;
}
.level-tag.red { background: #f56c6c; }
.level-tag.orange { background: #e6a23c; }
.level-tag.yellow { background: #e6a23c; }
.level-tag.green { background: #67c23a; }
.status-tag.pending { background: #f56c6c; }
.status-tag.processing { background: #e6a23c; }
.status-tag.resolved { background: #67c23a; }
button {
  padding: 6px 12px;
  margin: 0 4px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  background: #409eff;
  color: white;
}
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-content {
  background: white;
  padding: 30px;
  border-radius: 8px;
  max-width: 600px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}
.modal-content h3 {
  margin-bottom: 20px;
  color: #333;
}
.modal-content p {
  margin-bottom: 15px;
  line-height: 1.6;
}
.modal-content strong {
  color: #333;
}
.modal-content button {
  background: #409eff;
  color: white;
  padding: 10px 20px;
  font-size: 14px;
}
</style>
src/views/demo/fakePage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,248 @@
<template>
  <div class="app-container">
    <el-card shadow="never">
      <div class="toolbar">
        <el-input
          v-model="query.keyword"
          placeholder="搜索名称/类别"
          clearable
          style="width: 240px"
          @keyup.enter="handleSearch"
        />
        <el-select
          v-model="query.status"
          placeholder="状态"
          clearable
          style="width: 140px; margin-left: 12px"
        >
          <el-option label="启用" value="启用" />
          <el-option label="停用" value="停用" />
        </el-select>
        <el-button type="primary" style="margin-left: 12px" @click="handleSearch">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
        <el-button type="success" plain style="float: right" @click="openCreate">新增</el-button>
      </div>
      <el-table :data="pagedList" border style="width: 100%" height="480">
        <el-table-column prop="id" label="编号" width="90" sortable />
        <el-table-column prop="name" label="名称" min-width="140" />
        <el-table-column prop="category" label="类别" width="120" />
        <el-table-column prop="stock" label="库存" width="100" sortable />
        <el-table-column prop="price" label="单价(Â¥)" width="120">
          <template #default="scope">{{ formatPrice(scope.row.price) }}</template>
        </el-table-column>
        <el-table-column label="状态" width="120">
          <template #default="scope">
            <el-tag :type="scope.row.status === '启用' ? 'success' : 'info'">{{ scope.row.status }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="updatedAt" label="更新时间" min-width="160" />
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination">
        <el-pagination
          background
          layout="total, sizes, prev, pager, next, jumper"
          :total="filteredList.length"
          :page-sizes="[5, 10, 20, 50]"
          :page-size="pager.pageSize"
          :current-page="pager.pageNum"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑' : '新增'" width="520px">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="90px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入名称" />
        </el-form-item>
        <el-form-item label="类别" prop="category">
          <el-select v-model="form.category" placeholder="请选择类别" style="width: 100%">
            <el-option label="原料" value="原料" />
            <el-option label="半成品" value="半成品" />
            <el-option label="成品" value="成品" />
          </el-select>
        </el-form-item>
        <el-form-item label="库存" prop="stock">
          <el-input v-model.number="form.stock" type="number" min="0" />
        </el-form-item>
        <el-form-item label="单价(Â¥)" prop="price">
          <el-input v-model.number="form.price" type="number" min="0" step="0.01" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="启用">启用</el-radio>
            <el-radio label="停用">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({ name: 'FakePage' })
const query = reactive({
  keyword: '',
  status: ''
})
const pager = reactive({
  pageNum: 1,
  pageSize: 10
})
const allList = ref(generateMockData())
const filteredList = computed(() => {
  const keyword = (query.keyword || '').trim()
  const status = query.status
  return allList.value.filter(item => {
    const hitKeyword = !keyword || item.name.includes(keyword) || item.category.includes(keyword)
    const hitStatus = !status || item.status === status
    return hitKeyword && hitStatus
  })
})
const pagedList = computed(() => {
  const start = (pager.pageNum - 1) * pager.pageSize
  const end = start + pager.pageSize
  return filteredList.value.slice(start, end)
})
function handleSearch() {
  pager.pageNum = 1
}
function resetQuery() {
  query.keyword = ''
  query.status = ''
  pager.pageNum = 1
}
function handleSizeChange(size) {
  pager.pageSize = size
  pager.pageNum = 1
}
function handleCurrentChange(page) {
  pager.pageNum = page
}
function formatPrice(val) {
  return Number(val || 0).toFixed(2)
}
// æ–°å¢ž/编辑
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = reactive({ id: null, name: '', category: '', stock: 0, price: 0, status: '启用' })
const rules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  category: [{ required: true, message: '请选择类别', trigger: 'change' }],
  stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
  price: [{ required: true, message: '请输入单价', trigger: 'blur' }]
}
function openCreate() {
  isEdit.value = false
  Object.assign(form, { id: null, name: '', category: '', stock: 0, price: 0, status: '启用' })
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate?.())
}
function openEdit(row) {
  isEdit.value = true
  Object.assign(form, JSON.parse(JSON.stringify(row)))
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate?.())
}
function submitForm() {
  formRef.value?.validate?.((valid) => {
    if (!valid) return
    if (isEdit.value) {
      const index = allList.value.findIndex(x => x.id === form.id)
      if (index > -1) {
        allList.value[index] = { ...form, updatedAt: nowString() }
        ElMessage.success('已保存')
      }
    } else {
      const newId = Date.now()
      allList.value.unshift({ ...form, id: newId, updatedAt: nowString() })
      ElMessage.success('已新增')
    }
    dialogVisible.value = false
  })
}
function handleDelete(row) {
  ElMessageBox.confirm(`确认删除【${row.name}】吗?`, '提示', { type: 'warning' })
    .then(() => {
      allList.value = allList.value.filter(x => x.id !== row.id)
      ElMessage.success('已删除')
    })
    .catch(() => {})
}
function generateMockData() {
  const categories = ['原料', '半成品', '成品']
  const statusOptions = ['启用', '停用']
  const list = []
  for (let i = 1; i <= 36; i++) {
    list.push({
      id: i,
      name: `物料-${i.toString().padStart(3, '0')}`,
      category: categories[i % categories.length],
      stock: Math.floor(Math.random() * 1000),
      price: (Math.random() * 500 + 10).toFixed(2),
      status: statusOptions[i % 2],
      updatedAt: nowString()
    })
  }
  return list
}
function nowString() {
  const d = new Date()
  const yyyy = d.getFullYear()
  const MM = String(d.getMonth() + 1).padStart(2, '0')
  const dd = String(d.getDate()).padStart(2, '0')
  const hh = String(d.getHours()).padStart(2, '0')
  const mm = String(d.getMinutes()).padStart(2, '0')
  const ss = String(d.getSeconds()).padStart(2, '0')
  return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`
}
</script>
<style scoped>
.toolbar {
  margin-bottom: 12px;
}
.pagination {
  margin-top: 12px;
  text-align: right;
}
</style>